diff --git a/.github/workflows/dev_cicd.yml b/.github/workflows/dev_cicd.yml new file mode 100644 index 00000000..6c968918 --- /dev/null +++ b/.github/workflows/dev_cicd.yml @@ -0,0 +1,50 @@ +name: CI/CD for Development + +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +jobs: + cicd: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20.10.0 + cache: yarn + - run: yarn install + + - name: Create .env file + run: | + echo "VITE_NEW_API_URL=${{ secrets.VITE_NEW_API_URL }}" > .env + echo "VITE_DOMAIN=${{ secrets.VITE_DEV_DOMAIN }}" >> .env + echo "VITE_FIREBASE_API_KEY=${{ secrets.VITE_FIREBASE_API_KEY }}" >> .env + echo "VITE_FIREBASE_AUTH_DOMAIN=${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}" >> .env + echo "VITE_FIREBASE_PROJECT_ID=${{ secrets.VITE_FIREBASE_PROJECT_ID }}" >> .env + echo "VITE_FIREBASE_STORAGE_BUCKET=${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}" >> .env + echo "VITE_FIREBASE_MESSAGIN_ID=${{ secrets.VITE_FIREBASE_MESSAGIN_ID }}" >> .env + echo "VITE_FIREBASE_APP_ID=${{ secrets.VITE_FIREBASE_APP_ID }}" >> .env + echo "VITE_FIREBASE_MEASURENMENT_ID=${{ secrets.VITE_FIREBASE_MEASURENMENT_ID }}" >> .env + echo "VITE_KAKAO_REST_API_KEY=${{ secrets.VITE_KAKAO_REST_API_KEY }}" >> .env + echo "VITE_NAVER_CLIENT_ID=${{ secrets.VITE_NAVER_CLIENT_ID }}" >> .env + echo "VITE_NAVER_CLIENT_SECRET=${{ secrets.VITE_NAVER_CLIENT_SECRET }}" >> .env + echo "VITE_GOOGLE_CLIENT_ID=${{ secrets.VITE_GOOGLE_CLIENT_ID }}" >> .env + echo "VITE_GOOGLE_CLIENT_SECRET=${{ secrets.VITE_GOOGLE_CLIENT_SECRET }}" >> .env + + - run: yarn build-dev + + - name: deploy to s3 + uses: jakejarvis/s3-sync-action@master + with: + args: --delete + env: + AWS_S3_BUCKET: ${{ secrets.DEV_AWS_S3_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SOURCE_DIR: ${{ secrets.DEV_SOURCE_DIR }} diff --git a/.github/workflows/prod_cicd.yml b/.github/workflows/prod_cicd.yml new file mode 100644 index 00000000..05aa7722 --- /dev/null +++ b/.github/workflows/prod_cicd.yml @@ -0,0 +1,50 @@ +name: CI/CD for Production + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + cicd: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20.10.0 + cache: yarn + - run: yarn install + + - name: Create .env file + run: | + echo "VITE_NEW_API_URL=${{ secrets.VITE_NEW_API_URL }}" > .env + echo "VITE_DOMAIN=${{ secrets.VITE_PROD_DOMAIN }}" >> .env + echo "VITE_FIREBASE_API_KEY=${{ secrets.VITE_FIREBASE_API_KEY }}" >> .env + echo "VITE_FIREBASE_AUTH_DOMAIN=${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}" >> .env + echo "VITE_FIREBASE_PROJECT_ID=${{ secrets.VITE_FIREBASE_PROJECT_ID }}" >> .env + echo "VITE_FIREBASE_STORAGE_BUCKET=${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}" >> .env + echo "VITE_FIREBASE_MESSAGIN_ID=${{ secrets.VITE_FIREBASE_MESSAGIN_ID }}" >> .env + echo "VITE_FIREBASE_APP_ID=${{ secrets.VITE_FIREBASE_APP_ID }}" >> .env + echo "VITE_FIREBASE_MEASURENMENT_ID=${{ secrets.VITE_FIREBASE_MEASURENMENT_ID }}" >> .env + echo "VITE_KAKAO_REST_API_KEY=${{ secrets.VITE_KAKAO_REST_API_KEY }}" >> .env + echo "VITE_NAVER_CLIENT_ID=${{ secrets.VITE_NAVER_CLIENT_ID }}" >> .env + echo "VITE_NAVER_CLIENT_SECRET=${{ secrets.VITE_NAVER_CLIENT_SECRET }}" >> .env + echo "VITE_GOOGLE_CLIENT_ID=${{ secrets.VITE_GOOGLE_CLIENT_ID }}" >> .env + echo "VITE_GOOGLE_CLIENT_SECRET=${{ secrets.VITE_GOOGLE_CLIENT_SECRET }}" >> .env + + - run: yarn build-prod + + - name: deploy to s3 + uses: jakejarvis/s3-sync-action@master + with: + args: --delete + env: + AWS_S3_BUCKET: ${{ secrets.PROD_AWS_S3_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SOURCE_DIR: ${{ secrets.PROD_SOURCE_DIR }} diff --git a/package.json b/package.json index c8ceffe4..6d4869c4 100644 --- a/package.json +++ b/package.json @@ -19,19 +19,21 @@ "dayjs": "^1.11.12", "express": "^4.19.2", "firebase": "^10.13.0", + "heic2any": "^0.0.4", "js-cookie": "^3.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-overlay": "6.0.9", + "react-responsive": "^10.0.0", "react-router-dom": "^6.24.1", + "react-scripts": "4.0.3", "recoil": "^0.7.7", "recoil-persist": "^5.1.0", "request": "^2.88.2", "socket.io-client": "^4.7.5", "styled-components": "^6.1.11", "styled-reset": "^4.5.2", - "swiper": "^11.1.8", - "react-scripts": "4.0.3", - "react-error-overlay": "6.0.9" + "swiper": "^11.1.8" }, "devDependencies": { "@types/express": "^4.17.21", diff --git a/public/ProfileImg.svg b/public/ProfileImg.svg deleted file mode 100644 index 3002c2a5..00000000 --- a/public/ProfileImg.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/public/Send.svg b/public/Send.svg deleted file mode 100644 index 1d399fb8..00000000 --- a/public/Send.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 3ac46f60..119f4515 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,47 +1,92 @@ import React from 'react'; -import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'; import Home from './pages/Home'; -import MyPage from './pages/Mypage'; -import MyPost from './pages/MyPost'; +import Login from './pages/Login'; +import SignUp from './pages/SignUp'; +import LoginComplete from './pages/Login/components/LoginComplete'; +import TermsAgreement from './pages/TermsAgreement'; +import KakaoCallback from './pages/Login/components/Kakao/KakaoCallback'; +import NaverCallback from './pages/Login/components/Naver/NaverCallback'; + +import MyPage from './pages/MyPage'; import ProfileEdit from './pages/ProfileEdit'; import AccountSetting from './pages/AccountSetting'; import AccountEdit from './pages/AccountEdit'; import AccountCancel from './pages/AccountCancel'; import Verification from './pages/verification'; -import Login from './pages/Login'; -import SignUp from './pages/SignUp'; + import ProfileViewer from './pages/ProfileViewer'; + +import Post from './pages/Post'; +import MyPost from './pages/MyPost'; +import PostUpload from './pages/PostUpload'; +import PostImageSelect from './pages/PostImageSelect'; +import PostInstaConnect from './pages/PostInstaConnect'; +import PostInstaFeedSelect from './pages/PostInstaFeedSelect'; + import Chats from './pages/Chats'; import ChatRoom from './pages/Chats/ChatRoom'; -import Upload from './pages/Upload'; -import Post from './pages/Post'; -import KakaoCallback from './pages/Login/components/Kakao/KakaoCallback'; -import NaverCallback from './pages/Login/components/Naver/NaverCallback'; -import GoogleCallback from './pages/Login/components/Google/GoogleCallback'; + +import NotFound from './pages/NotFound'; + +const ProtectedRoute = ({ children }: { children: JSX.Element }) => { + const isAuthenticated = Boolean(localStorage.getItem('new_jwt_token')); + return isAuthenticated ? children : ; +}; + +// 인증이 필요한 페이지 배열 +const protectedRoutes = [ + { path: '/', element: }, + + // 사용자 프로필 및 계정 관리 + { path: '/mypage', element: }, + { path: '/profile/edit', element: }, + { path: '/account-setting', element: }, + { path: '/account-edit', element: }, + { path: '/account-cancel', element: }, + { path: '/verification', element: }, + { path: '/users/:userId', element: }, + + { path: '/post/:postId', element: }, + { path: '/my-post/:postId', element: }, + { path: '/upload', element: }, + { path: '/image-select', element: }, + { path: '/insta-connect', element: }, + { path: '/insta-feed-select', element: }, + + // 메시지/채팅 + { path: '/chats', element: }, + { path: '/chats/:chatRoomId', element: }, +]; + +// 인증이 필요 없는 페이지 배열 +const publicRoutes = [ + { path: '/login', element: }, + { path: '/signup', element: }, + { path: '/login/complete', element: }, + { path: '/terms-agreement', element: }, + + // 콜백 + { path: '/auth/kakao/callback', element: }, + { path: '/auth/naver/callback', element: }, +]; + const App: React.FC = () => { return ( - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - }> - }> - }> + {/* 인증이 필요한 페이지 */} + {protectedRoutes.map(({ path, element }) => ( + {element}} /> + ))} + + {/* 인증이 필요 없는 페이지 */} + {publicRoutes.map(({ path, element }) => ( + + ))} + + {/* 없는 페이지에 대한 처리 */} + } /> ); diff --git a/src/apis/auth/dto.ts b/src/apis/auth/dto.ts new file mode 100644 index 00000000..4e28685b --- /dev/null +++ b/src/apis/auth/dto.ts @@ -0,0 +1,14 @@ +import { BaseSuccessResponse } from '../core/dto'; + +// jwt를 이용한 사용자 정보 조회 응답 +export type getUserInfoByJwtResponse = BaseSuccessResponse; +// jwt를 이용한 사용자 정보 조회 응답 데이터 +export interface getUserInfoByJwtData { + userId: string; + name: string; + phoneNumber: string; + email: string; + nickname: string; + profilePictureUrl: string; + bio: string; +} diff --git a/src/apis/auth/index.ts b/src/apis/auth/index.ts new file mode 100644 index 00000000..fea044da --- /dev/null +++ b/src/apis/auth/index.ts @@ -0,0 +1,5 @@ +import { getUserInfoByJwtResponse } from './dto'; +import { newRequest } from '../core'; + +// jwt로 사용자 정보 조회 api /auth/me +export const getUserInfoByJwtApi = () => newRequest.get('/auth/me'); diff --git a/src/apis/chat-room/dto.ts b/src/apis/chat-room/dto.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/apis/chatting/dto.ts b/src/apis/chatting/dto.ts new file mode 100644 index 00000000..a200b48a --- /dev/null +++ b/src/apis/chatting/dto.ts @@ -0,0 +1,43 @@ +// 채팅방 리스트 조회 +// base response 형태를 따르지 않으므로 data 접미사를 사용했습니다. +// response +export interface ChatRoomData { + chatRoomId: number; + otherUser: OtherUserDto; + latestMessage: LatestMessageDto; +} + +export interface OtherUserDto { + id: number; + nickname: string; + profilePictureUrl: string; +} + +export interface LatestMessageDto { + content: string; + createdAt: string; +} + +// 채팅방 전체 대화 내역 조회 +// 최근 메시지 수신 +// response +export interface chatRoomMessagesData { + id: number; + content: string; + fromUser: FromUserDto; + toUser: ToUserDto; + createdAt: string; + toUserReadAt: any; +} + +export interface FromUserDto { + id: number; + nickname: string; + profilePictureUrl: string; +} + +export interface ToUserDto { + id: number; + nickname: string; + profilePictureUrl: string; +} diff --git a/src/apis/core/dto.ts b/src/apis/core/dto.ts new file mode 100644 index 00000000..75621511 --- /dev/null +++ b/src/apis/core/dto.ts @@ -0,0 +1,9 @@ +// 새로운 서버 응답 타입 +export type BaseSuccessResponse = { + isSuccess: boolean; + code: string; + data: T; +}; + +// 응답 body가 없을 경우 +export type EmptySuccessResponse = BaseSuccessResponse; diff --git a/src/apis/core/index.ts b/src/apis/core/index.ts index c69dd4ea..3a66718e 100644 --- a/src/apis/core/index.ts +++ b/src/apis/core/index.ts @@ -5,8 +5,9 @@ import axios, { AxiosResponse, InternalAxiosRequestConfig, } from 'axios'; -import { JWT_KEY } from '../../config/constant'; +import { JWT_KEY, NEW_JWT_KEY } from '../../config/constant'; +// 기존 서버 응답 타입 export type BaseResponse = { isSuccess: boolean; message: string; @@ -35,6 +36,43 @@ interface CustomInstance extends AxiosInstance { patch(url: string, data?: any, config?: AxiosRequestConfig): Promise; } +// 새로운 서버 axios 인스턴스 +export const newRequest: CustomInstance = axios.create({ + baseURL: import.meta.env.VITE_NEW_API_URL, + timeout: 20000, + headers: { + accept: 'application/json', + Authorization: `Bearer ${NEW_JWT_KEY}`, + }, +}); + +newRequest.interceptors.request.use( + (config) => { + const jwt = window.localStorage.getItem(NEW_JWT_KEY); + config.headers.Authorization = `Bearer ${jwt}`; + return config; + }, + (error) => { + return Promise.reject(error); + }, +); + +newRequest.interceptors.response.use( + (response) => { + // 2XX 범위 + // response body 반환 + console.log('network log', response); + return response.data; + }, + (error) => { + // 그 외 + // error로 AxiosError 타입 반환 + console.error(error); + return Promise.reject(error); + }, +); + +// 기존 서버 axios 인스턴스 export const request: CustomInstance = axios.create({ baseURL: import.meta.env.VITE_API_URL, timeout: 20000, diff --git a/src/apis/matching/dto.ts b/src/apis/matching/dto.ts new file mode 100644 index 00000000..3a79b40b --- /dev/null +++ b/src/apis/matching/dto.ts @@ -0,0 +1,67 @@ +import { BaseSuccessResponse } from '../core/dto'; + +// 매칭 요청 +// request +export interface CreateMatchingRequest { + requesterId: number; + targetId: number; + message: string; +} + +// response +export type CreateMatchingResponse = BaseSuccessResponse; + +export interface CreateMatchingData { + chatRoomId: number; + fromUserId: number; + toUserId: number; +} + +// 매칭 리스트 조회 +// response +export type GetMatchingListResponse = BaseSuccessResponse; + +export interface GetMatchingListData { + isMatching: boolean; // 매칭 요청 존재 여부 + matchingsCount: number; // 매칭 요청 개수 + matching: MatchingDto[]; +} + +export interface MatchingDto { + matchingId: number; + requester: RequesterDto; + requesterPost: RequesterPostDto; +} + +export interface RequesterDto { + requesterId: number; + nickname: string; + profilePictureUrl: string; +} + +export interface RequesterPostDto { + postImages: PostImageDto[]; // 대표 게시글 이미지 + styleTags: string[]; // 게시글 스타일 태그 +} + +export interface PostImageDto { + url: string; + orderNum: number; +} + +// 매칭 요청 수락 및 거절 +// request +export interface ModifyMatchingStatusRequest { + requestStatus: 'accept' | 'reject'; +} + +// response +export type ModifyMatchingStatusResponse = BaseSuccessResponse; + +export interface ModifyMatchingStatusData { + matchingId: number; + requesterId: number; + targetId: number; + requestStatus: string; + chatRoomId: number; +} diff --git a/src/apis/matching/index.ts b/src/apis/matching/index.ts new file mode 100644 index 00000000..62e2b595 --- /dev/null +++ b/src/apis/matching/index.ts @@ -0,0 +1,19 @@ +import { newRequest } from '../core'; +import { + CreateMatchingRequest, + CreateMatchingResponse, + GetMatchingListResponse, + ModifyMatchingStatusRequest, + ModifyMatchingStatusResponse, +} from './dto'; + +// 매칭 생성 +export const createMatchingApi = (data: CreateMatchingRequest) => + newRequest.post('/matching', data); + +// 매칭 리스트 조회 +export const getMatchingListApi = () => newRequest.get('/matching'); + +// 매칭 요청 수락 및 거절 +export const modifyMatchingStatusApi = (matchingId: number, data: ModifyMatchingStatusRequest) => + newRequest.patch(`/matching/${matchingId}`, data); diff --git a/src/apis/post-comment/dto.ts b/src/apis/post-comment/dto.ts new file mode 100644 index 00000000..5dcc4b1b --- /dev/null +++ b/src/apis/post-comment/dto.ts @@ -0,0 +1,41 @@ +import { BaseSuccessResponse } from '../core/dto'; + +// 댓글 작성 +// Request +export type CreateCommentRequest = Content; +// Response +export type CreateCommentResponse = BaseSuccessResponse; + +//댓글 리스트 조회 +// Response +export type GetCommentListResponse = BaseSuccessResponse; + +// 댓글 삭제 +// Response : empty + +interface Content { + content: string; +} + +interface CreateCommentData extends Content {} + +export interface Comment { + id: number; + user: CommentUser; + content: string; + createdAt: string; + isCommentWriter: boolean; +} + +// 댓글 관련 User 정보 +export interface CommentUser { + userId: number; + nickname: string; + profilePictureUrl: string; +} + +// 게시글 댓글 리스트 조회 응답 +export interface GetCommentListData { + comments: Comment[]; + totalCount: number; +} diff --git a/src/apis/post-comment/index.ts b/src/apis/post-comment/index.ts new file mode 100644 index 00000000..f130e959 --- /dev/null +++ b/src/apis/post-comment/index.ts @@ -0,0 +1,15 @@ +import { newRequest } from '../core'; +import { CreateCommentRequest, CreateCommentResponse, GetCommentListResponse } from './dto'; +import { EmptySuccessResponse } from '../core/dto'; + +// 게시글 댓글 생성 API +export const createCommentApi = (postId: number, data: CreateCommentRequest) => + newRequest.post(`/post-comment?postId=${postId}`, data); + +// 게시글 댓글 리스트 조회 +export const getCommentListApi = (postId: number) => + newRequest.get(`/post-comment?postId=${postId}`); + +// 게시글 댓글 삭제 +export const deleteCommentApi = (commentId: number) => + newRequest.delete(`/post-comment/${commentId}`); diff --git a/src/apis/post-like/dto.ts b/src/apis/post-like/dto.ts new file mode 100644 index 00000000..299bf209 --- /dev/null +++ b/src/apis/post-like/dto.ts @@ -0,0 +1,31 @@ +import { BaseSuccessResponse } from '../core/dto'; +import { PaginationMeta } from '../util/dto'; + +// 좋아요 누르기/취소 +export type TogglePostLikeStatusResponse = BaseSuccessResponse; + +// 게시물 좋아요 리스트 조회 +export type GetPostLikeListResponse = BaseSuccessResponse; + +export interface GetPostLikeListData { + totalCount: number; + likes: Like[]; + meta: PaginationMeta; +} + +export interface User { + id: number; + nickname: string; + profilePictureUrl: string; +} + +export interface Like { + user: User; + createdAt: string; +} + +export interface TogglePostLikeStatusData { + id: number; + isPostLike: boolean; + postLikesCount: number; +} diff --git a/src/apis/post-like/index.ts b/src/apis/post-like/index.ts new file mode 100644 index 00000000..de332922 --- /dev/null +++ b/src/apis/post-like/index.ts @@ -0,0 +1,12 @@ +import { newRequest } from '../core'; +import { TogglePostLikeStatusResponse, GetPostLikeListResponse } from './dto'; + +// 게시글 좋아요 누르기/취소 +export const togglePostLikeStatusApi = (postId: number) => + newRequest.post(`/post-like/${postId}`); + +// 게시글 좋아요 리스트 조회 (페이지네이션 포함) +export const getPostLikeListApi = (postId: number, page: number = 1, take: number = 10) => + newRequest.get(`/post-like/${postId}`, { + params: { page, take }, + }); diff --git a/src/apis/post-report/dto.ts b/src/apis/post-report/dto.ts new file mode 100644 index 00000000..10c915db --- /dev/null +++ b/src/apis/post-report/dto.ts @@ -0,0 +1,27 @@ +import { BaseSuccessResponse } from '../core/dto'; + +interface BaseReport { + id: number; // 신고 ID + userId: number; // 신고 생성 사용자 ID + postId: number; // 신고된 게시글 ID + content: string; // 신고된 게시글 내용 + repostReason: string; // 신고 사유 +} + +// 게시글 신고 요청 DTO +export interface SendPostReportRequest { + requesterId: number; // 신고하는 사용자 ID + postId: number; // 신고 대상 게시글 ID + reason: string; // 신고 사유 +} + +// 게시물 신고 응답 데이터 +export interface SendPostReportData extends BaseReport { + requesterId: number; // 신고하는 사용자 ID + postId: number; // 신고 대상 게시글 ID + reason: string; // 신고 사유 +} + +// 게시물 신고 응답 타입 +export type SendPostReportResponse = BaseSuccessResponse; + diff --git a/src/apis/post-report/index.ts b/src/apis/post-report/index.ts new file mode 100644 index 00000000..29aee5fd --- /dev/null +++ b/src/apis/post-report/index.ts @@ -0,0 +1,6 @@ +import { newRequest } from "../core"; +import { SendPostReportRequest, SendPostReportResponse } from "./dto"; + +// 게시글 신고 API +export const sendPostReportApi = (data: SendPostReportRequest) => + newRequest.post('/post-report', data); diff --git a/src/apis/post/dto.ts b/src/apis/post/dto.ts new file mode 100644 index 00000000..8c5f1c84 --- /dev/null +++ b/src/apis/post/dto.ts @@ -0,0 +1,89 @@ +import { BaseSuccessResponse } from '../core/dto'; +import { PaginationMeta } from '../util/dto'; + +// 게시글 생성 +//request +export type CreatePostRequest = PostBase; +//response +export type CreatePostResponse = BaseSuccessResponse; +// 게시글 리스트 조회 +export type GetPostListResponse = BaseSuccessResponse; +export type GetUserPostListResponse = BaseSuccessResponse; +// 게시글 상세 조회 +export type GetPostDetailResponse = BaseSuccessResponse; +// 게시글 수정 +//request +export type ModifyPostRequest = PostBase; +//response +export type ModifyPostResponse = BaseSuccessResponse; +// 게시글 삭제 +// response : empty +// 대표 게시글 지정 +// response : empty + +export interface PostBase { + content: string; + postImages: PostImage[]; + postStyletags: string[]; + postClothings: PostClothing[] | null; + isRepresentative: boolean; +} +export interface CreatePostData extends PostBase {} +export interface PostSummary { + postId: number; + content: string; + postImages: PostImage[]; + createdAt: Date; + isPostLike: boolean; + user: User; +} +export interface GetPostListData { + post: PostSummary[]; + meta: PaginationMeta; +} +export interface UserPostSummary { + userId: number; + postId: number; + createdAt: Date; + imageUrl: string; + postLikesCount: number; + postCommentsCount?: number; + isPostLike: boolean; + isPostComment?: boolean; + isRepresentative: boolean; +} +export interface GetUserPostListData { + post: UserPostSummary[]; + totalPostsCount: number; + totalPostLikesCount: number; + totalPostCommentsCount?: number; + meta: PaginationMeta; +} +export interface ModifyPostData extends PostBase { + postId: number; + userId: number; +} +export interface GetPostDetailData extends PostBase { + createdAt: string; + user: User; + postCommentsCount: number; + postLikesCount: number; + isPostLike: boolean; + isPostWriter: boolean; +} +export interface User { + userId: number; + nickname: string; + profilePictureUrl: string; +} +export interface PostImage { + imageUrl: string; + orderNum: number; +} +export interface PostClothing { + imageUrl: string; + brandName: string; + modelName: string; + modelNumber: string; + url: string; +} diff --git a/src/apis/post/index.ts b/src/apis/post/index.ts new file mode 100644 index 00000000..ad5c2f0f --- /dev/null +++ b/src/apis/post/index.ts @@ -0,0 +1,36 @@ +import { newRequest } from '../core'; +import { + CreatePostRequest, + CreatePostResponse, + GetPostListResponse, + GetUserPostListResponse, + GetPostDetailResponse, + ModifyPostRequest, + ModifyPostResponse, +} from './dto'; +import { EmptySuccessResponse } from '../core/dto'; + +// 게시글 생성 +export const createPostApi = (data: CreatePostRequest) => newRequest.post('/post', data); + +// 게시글 리스트 조회 +// 전체 게시글 리스트 +export const getPostListApi = (page: number = 1, take: number = 10) => + newRequest.get(`/post`, { params: { page, take } }); +// 유저 게시글 리스트 +export const getUserPostListApi = (page: number = 1, take: number = 10, userId: number) => + newRequest.get(`/post`, { params: { page, take, userId } }); + +// 게시글 상세 조회 +export const getPostDetailApi = (postId: number) => newRequest.get(`/post/${postId}`); + +// 게시글 수정 +export const modifyPostApi = (postId: number, data: ModifyPostRequest) => + newRequest.patch(`/post/${postId}`, data); + +// 게시글 삭제 +export const deletePostApi = (postId: number) => newRequest.delete(`/post/${postId}`); + +// 대표 게시글 지정 +export const modifyPostRepresentativeStatusApi = (postId: number) => + newRequest.patch(`/post/${postId}/is-representative`); diff --git a/src/apis/user/dto.ts b/src/apis/user/dto.ts new file mode 100644 index 00000000..5603737b --- /dev/null +++ b/src/apis/user/dto.ts @@ -0,0 +1,55 @@ +import { BaseSuccessResponse } from '../core/dto'; + +// 사용자 정보 공통 인터페이스 +export interface UserInfoData { + userId: string; + name: string; + phoneNumber: string; + email: string; + nickname: string; + profilePictureUrl: string; + bio: string; + joinedAt?: string; // user 공통 인터페이스에 이 두 개는 안 나와있어서 일단 이것들만 optional 처리했습니다... + isFriend?: boolean; +} + +// 사용자 정보 조회 응답 +export type GetUserInfoResponse = BaseSuccessResponse; + +// 사용자 신고 요청 데이터 +export interface PostUserReportRequest { + fromUserId: number; + toUserId: number; + reason: string; +} + +// 차단/해제 요청 데이터 +export interface PostUserBlockRequest { + fromUserId: number; + toUserId: number; + action: 'block' | 'unblock'; // 차단 또는 해제 +} + +// 사용자 정보 수정 응답 +export type PatchUserInfoResponse = BaseSuccessResponse; + +// 사용자 정보 수정 요청 데이터 +export interface PatchUserInfoRequest { + name: string; + phoneNumber: string; + birthDate: string; + email: string; + nickname: string; + profilePictureUrl: string; + bio: string; +} + +// 회원 탈퇴 응답 +export type PatchUserWithDrawResponse = BaseSuccessResponse; + +// 회원 탈퇴 응답 데이터 +export interface PatchUserWithdrawData { + isSuccess: boolean; + code: string; + data: Record; // 탈퇴 성공 시 항상 빈 객체가 응답으로 온다면 Record으로 타입 안정성 높일 수 있답니다 +} diff --git a/src/apis/user/index.ts b/src/apis/user/index.ts new file mode 100644 index 00000000..e39579f8 --- /dev/null +++ b/src/apis/user/index.ts @@ -0,0 +1,28 @@ +import { + PostUserBlockRequest, + PostUserReportRequest, + PatchUserInfoRequest, + PatchUserInfoResponse, + PatchUserWithDrawResponse, +} from './dto'; +import { newRequest } from '../core'; +import { EmptySuccessResponse } from '../core/dto'; + +// 유저 정보 수정 api +export const patchUserInfoApi = (data: PatchUserInfoRequest, userId: string) => + newRequest.patch(`/user/${userId}`, data); + +// 유저 탈퇴 api +export const patchUserWithdrawApi = (userId: string) => + newRequest.patch(`/user/${userId}/withdraw`); + +// 유저 차단 api +export const postUserBlockApi = (data: PostUserBlockRequest) => + newRequest.post('/user-block', data); + +// 유저 신고 api +export const postUserReportApi = (data: PostUserReportRequest) => + newRequest.post('/user-report', data); + +// 이용 약관 동의 api +export const postTermsAgreementApi = (userId: string) => newRequest.post(`/user/${userId}`); diff --git a/src/apis/util/dto.ts b/src/apis/util/dto.ts new file mode 100644 index 00000000..ff6003e5 --- /dev/null +++ b/src/apis/util/dto.ts @@ -0,0 +1,22 @@ +export interface BaseApiResponse { + isSuccess: boolean; + code: number; + message: string; + result: T | null; // result가 없는 경우를 위해 null 처리 +} + +export interface PagingResponseType { + currentPage: number; + totalPages: number; + totalItems: number; +} + +export interface PaginationMeta { + total: number; + page: number; + take: number; + last_page: number; + hasPreviousPage: boolean; + hasNextPage: boolean; + totalItems: number; // 추가 +} diff --git a/src/apis/util/errorMessage.ts b/src/apis/util/errorMessage.ts new file mode 100644 index 00000000..11b3cb21 --- /dev/null +++ b/src/apis/util/errorMessage.ts @@ -0,0 +1,49 @@ +const commonErrorMessage: Record = { + 0: '알 수 없는 오류입니다.\n관리자에게 문의해 주세요.', + 400: '잘못된 요청입니다.', + 401: '인증에 실패했습니다.', + 403: '접근이 거부되었습니다.', + 500: '서버 오류가 발생했습니다.\n잠시 후 다시 시도해 주세요.', + 999: '서버 응답이 없습니다.\n잠시 후 다시 시도해 주세요.', +}; + +export const defaultErrorMessage: Record = { + ...commonErrorMessage, + 404: '존재하지 않는 요청입니다.', +}; + +export const userErrorMessage: Record = { + ...commonErrorMessage, + 404: '유저 정보를 찾을 수 없습니다.', +}; + +export const postErrorMessage: Record = { + ...commonErrorMessage, + 404: '게시글 정보를 찾을 수 없습니다.', +}; + +export const postCommentErrorMessage: Record = { + ...commonErrorMessage, + 404: '댓글 정보를 찾을 수 없습니다.', +}; + +export const matchingErrorMessage: Record = { + ...commonErrorMessage, + 404: '요청 정보를 찾을 수 없습니다.', +}; + +export const chatErrorMessage: Record = { + ...commonErrorMessage, + 404: '채팅방 정보를 찾을 수 없습니다.', +}; + +export type ApiDomain = 'user' | 'post' | 'postComment' | 'matching' | 'chat' | 'default'; + +export const errorMessages: Record> = { + user: userErrorMessage, + post: postErrorMessage, + postComment: postCommentErrorMessage, + matching: matchingErrorMessage, + chat: chatErrorMessage, + default: defaultErrorMessage, +}; diff --git a/src/apis/util/handleError.ts b/src/apis/util/handleError.ts new file mode 100644 index 00000000..d6dcf8f4 --- /dev/null +++ b/src/apis/util/handleError.ts @@ -0,0 +1,9 @@ +import { AxiosError } from 'axios'; +import { ApiDomain, errorMessages } from './errorMessage'; + +export const handleError = (error: unknown, domain: ApiDomain = 'default') => { + const message = errorMessages[domain]; + const status = (error as AxiosError).response ? (error as AxiosError).response?.status : 999; + + return message[status || 0] || '알 수 없는 오류입니다.'; +}; diff --git a/src/assets/BottomSheetMenu/Block.svg b/src/assets/BottomSheetMenu/Block.svg deleted file mode 100644 index 9f602a4c..00000000 --- a/src/assets/BottomSheetMenu/Block.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/BottomSheetMenu/Delete.svg b/src/assets/BottomSheetMenu/Delete.svg deleted file mode 100644 index 64a4c48e..00000000 --- a/src/assets/BottomSheetMenu/Delete.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/assets/BottomSheetMenu/Edit.svg b/src/assets/BottomSheetMenu/Edit.svg deleted file mode 100644 index 419bfadd..00000000 --- a/src/assets/BottomSheetMenu/Edit.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/BottomSheetMenu/Exit.svg b/src/assets/BottomSheetMenu/Exit.svg deleted file mode 100644 index a38b39b8..00000000 --- a/src/assets/BottomSheetMenu/Exit.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/BottomSheetMenu/Insta.svg b/src/assets/BottomSheetMenu/Insta.svg deleted file mode 100644 index fa244738..00000000 --- a/src/assets/BottomSheetMenu/Insta.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/BottomSheetMenu/Picture.svg b/src/assets/BottomSheetMenu/Picture.svg deleted file mode 100644 index d34e2fa9..00000000 --- a/src/assets/BottomSheetMenu/Picture.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/BottomSheetMenu/Pin.svg b/src/assets/BottomSheetMenu/Pin.svg deleted file mode 100644 index d098a46d..00000000 --- a/src/assets/BottomSheetMenu/Pin.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/BottomSheetMenu/Report.svg b/src/assets/BottomSheetMenu/Report.svg deleted file mode 100644 index da189c24..00000000 --- a/src/assets/BottomSheetMenu/Report.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/Chats/Back.svg b/src/assets/Chats/Back.svg deleted file mode 100644 index 09de7da1..00000000 --- a/src/assets/Chats/Back.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/Chats/KebabMenu.svg b/src/assets/Chats/KebabMenu.svg deleted file mode 100644 index 752ef21f..00000000 --- a/src/assets/Chats/KebabMenu.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/Home/OOTDImg1.svg b/src/assets/Home/OOTDImg1.svg deleted file mode 100644 index 045b5ae8..00000000 --- a/src/assets/Home/OOTDImg1.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/OOTDImg2.svg b/src/assets/Home/OOTDImg2.svg deleted file mode 100644 index 2046e6e5..00000000 --- a/src/assets/Home/OOTDImg2.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/OOTDImg3.svg b/src/assets/Home/OOTDImg3.svg deleted file mode 100644 index 350e55f2..00000000 --- a/src/assets/Home/OOTDImg3.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/OOTDImg4.svg b/src/assets/Home/OOTDImg4.svg deleted file mode 100644 index a3ec824b..00000000 --- a/src/assets/Home/OOTDImg4.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/button_check.svg b/src/assets/Home/button_check.svg deleted file mode 100644 index 1e6d96d3..00000000 --- a/src/assets/Home/button_check.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Home/button_heart.svg b/src/assets/Home/button_heart.svg deleted file mode 100644 index 53dc7383..00000000 --- a/src/assets/Home/button_heart.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Home/button_reject.svg b/src/assets/Home/button_reject.svg deleted file mode 100644 index 3f4afb62..00000000 --- a/src/assets/Home/button_reject.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Home/button_star.svg b/src/assets/Home/button_star.svg deleted file mode 100644 index b5f72236..00000000 --- a/src/assets/Home/button_star.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Home/casual.svg b/src/assets/Home/casual.svg deleted file mode 100644 index f5614a14..00000000 --- a/src/assets/Home/casual.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/classic.svg b/src/assets/Home/classic.svg deleted file mode 100644 index f12357b6..00000000 --- a/src/assets/Home/classic.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/clicked_bigheart.svg b/src/assets/Home/clicked_bigheart.svg deleted file mode 100644 index 87be3bb5..00000000 --- a/src/assets/Home/clicked_bigheart.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/assets/Home/clicked_bigstar.svg b/src/assets/Home/clicked_bigstar.svg deleted file mode 100644 index 01f4d773..00000000 --- a/src/assets/Home/clicked_bigstar.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/assets/Home/clicked_heart.svg b/src/assets/Home/clicked_heart.svg deleted file mode 100644 index 97834f85..00000000 --- a/src/assets/Home/clicked_heart.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/assets/Home/clicked_star.svg b/src/assets/Home/clicked_star.svg deleted file mode 100644 index dcaf93f7..00000000 --- a/src/assets/Home/clicked_star.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/assets/Home/comment.svg b/src/assets/Home/comment.svg deleted file mode 100644 index 9b374380..00000000 --- a/src/assets/Home/comment.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Home/feedImg.svg b/src/assets/Home/feedImg.svg deleted file mode 100644 index 8f5efde1..00000000 --- a/src/assets/Home/feedImg.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/assets/Home/feminine.svg b/src/assets/Home/feminine.svg deleted file mode 100644 index 84e76a6f..00000000 --- a/src/assets/Home/feminine.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/formal.svg b/src/assets/Home/formal.svg deleted file mode 100644 index a3d2aa8d..00000000 --- a/src/assets/Home/formal.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/grommet-icons_more.svg b/src/assets/Home/grommet-icons_more.svg deleted file mode 100644 index 058e0c60..00000000 --- a/src/assets/Home/grommet-icons_more.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Home/hip.svg b/src/assets/Home/hip.svg deleted file mode 100644 index a7ffbcc2..00000000 --- a/src/assets/Home/hip.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/luxury.svg b/src/assets/Home/luxury.svg deleted file mode 100644 index e1ead49d..00000000 --- a/src/assets/Home/luxury.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/minimal.svg b/src/assets/Home/minimal.svg deleted file mode 100644 index a267a170..00000000 --- a/src/assets/Home/minimal.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/no_profileImg.svg b/src/assets/Home/no_profileImg.svg deleted file mode 100644 index 78a11b45..00000000 --- a/src/assets/Home/no_profileImg.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/Home/noti.svg b/src/assets/Home/noti.svg deleted file mode 100644 index 6f4728bc..00000000 --- a/src/assets/Home/noti.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/Home/outdoor.svg b/src/assets/Home/outdoor.svg deleted file mode 100644 index 8af97b21..00000000 --- a/src/assets/Home/outdoor.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/profileImg1.svg b/src/assets/Home/profileImg1.svg deleted file mode 100644 index 6319f1e7..00000000 --- a/src/assets/Home/profileImg1.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/profileImg2.svg b/src/assets/Home/profileImg2.svg deleted file mode 100644 index 2da51650..00000000 --- a/src/assets/Home/profileImg2.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/Home/profileImg3.svg b/src/assets/Home/profileImg3.svg deleted file mode 100644 index d9d045ac..00000000 --- a/src/assets/Home/profileImg3.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/Home/sporty.svg b/src/assets/Home/sporty.svg deleted file mode 100644 index 43eaa2ee..00000000 --- a/src/assets/Home/sporty.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/street.svg b/src/assets/Home/street.svg deleted file mode 100644 index c101e3de..00000000 --- a/src/assets/Home/street.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Home/tag.svg b/src/assets/Home/tag.svg deleted file mode 100644 index 0f9eaf31..00000000 --- a/src/assets/Home/tag.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Login/google.png b/src/assets/Login/google.png deleted file mode 100644 index f7b6d7cb..00000000 Binary files a/src/assets/Login/google.png and /dev/null differ diff --git a/src/assets/Login/kakao.png b/src/assets/Login/kakao.png deleted file mode 100644 index a556ed67..00000000 Binary files a/src/assets/Login/kakao.png and /dev/null differ diff --git a/src/assets/Login/naver.png b/src/assets/Login/naver.png deleted file mode 100644 index 2d9f324e..00000000 Binary files a/src/assets/Login/naver.png and /dev/null differ diff --git a/src/assets/NavBar/Chat_f.svg b/src/assets/NavBar/Chat_f.svg deleted file mode 100644 index defff519..00000000 --- a/src/assets/NavBar/Chat_f.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/NavBar/Chat_s.svg b/src/assets/NavBar/Chat_s.svg deleted file mode 100644 index 233ae9b1..00000000 --- a/src/assets/NavBar/Chat_s.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/NavBar/Home_f.svg b/src/assets/NavBar/Home_f.svg deleted file mode 100644 index 7bd138dd..00000000 --- a/src/assets/NavBar/Home_f.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/NavBar/Home_s.svg b/src/assets/NavBar/Home_s.svg deleted file mode 100644 index d4b039c4..00000000 --- a/src/assets/NavBar/Home_s.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/NavBar/Profile_f.svg b/src/assets/NavBar/Profile_f.svg deleted file mode 100644 index 1d02b256..00000000 --- a/src/assets/NavBar/Profile_f.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/NavBar/Profile_s.svg b/src/assets/NavBar/Profile_s.svg deleted file mode 100644 index 671242ec..00000000 --- a/src/assets/NavBar/Profile_s.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/assets/OODDlogo.svg b/src/assets/OODDlogo.svg deleted file mode 100644 index ef99ba4b..00000000 --- a/src/assets/OODDlogo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/Post/block.svg b/src/assets/Post/block.svg deleted file mode 100644 index df79cb9d..00000000 --- a/src/assets/Post/block.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/Post/declaration.svg b/src/assets/Post/declaration.svg deleted file mode 100644 index 28b88899..00000000 --- a/src/assets/Post/declaration.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/Post/more.svg b/src/assets/Post/more.svg deleted file mode 100644 index a9a360fb..00000000 --- a/src/assets/Post/more.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Post/next.svg b/src/assets/Post/next.svg deleted file mode 100644 index 05c1cff9..00000000 --- a/src/assets/Post/next.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Post/postImg1.svg b/src/assets/Post/postImg1.svg deleted file mode 100644 index 9da8a9f3..00000000 --- a/src/assets/Post/postImg1.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/Post/productImg.svg b/src/assets/Post/productImg.svg deleted file mode 100644 index 0e5f571c..00000000 --- a/src/assets/Post/productImg.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/Post/profileImg.svg b/src/assets/Post/profileImg.svg deleted file mode 100644 index caa4e929..00000000 --- a/src/assets/Post/profileImg.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/Profile/button_plus.svg b/src/assets/Profile/button_plus.svg deleted file mode 100644 index 192a45c5..00000000 --- a/src/assets/Profile/button_plus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/ProfileViewer/Group 87.svg b/src/assets/ProfileViewer/Group 87.svg deleted file mode 100644 index 025382bf..00000000 --- a/src/assets/ProfileViewer/Group 87.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/assets/ProfileViewer/Vector.svg b/src/assets/ProfileViewer/Vector.svg deleted file mode 100644 index 3274808b..00000000 --- a/src/assets/ProfileViewer/Vector.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/ProfileViewer/backIcon.svg b/src/assets/ProfileViewer/backIcon.svg deleted file mode 100644 index f1d9f05f..00000000 --- a/src/assets/ProfileViewer/backIcon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/ProfileViewer/block.svg b/src/assets/ProfileViewer/block.svg deleted file mode 100644 index 180fb591..00000000 --- a/src/assets/ProfileViewer/block.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/ProfileViewer/carbon_warning.svg b/src/assets/ProfileViewer/carbon_warning.svg deleted file mode 100644 index 2d4d185a..00000000 --- a/src/assets/ProfileViewer/carbon_warning.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/ProfileViewer/heart.svg b/src/assets/ProfileViewer/heart.svg deleted file mode 100644 index c9a9a0a7..00000000 --- a/src/assets/ProfileViewer/heart.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/ProfileViewer/message_send _gray.svg b/src/assets/ProfileViewer/message_send _gray.svg deleted file mode 100644 index f581e148..00000000 --- a/src/assets/ProfileViewer/message_send _gray.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/ProfileViewer/message_send.svg b/src/assets/ProfileViewer/message_send.svg deleted file mode 100644 index c703d25d..00000000 --- a/src/assets/ProfileViewer/message_send.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/ProfileViewer/moreIcon.svg b/src/assets/ProfileViewer/moreIcon.svg deleted file mode 100644 index 0687782a..00000000 --- a/src/assets/ProfileViewer/moreIcon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/ProfileViewer/star.svg b/src/assets/ProfileViewer/star.svg deleted file mode 100644 index 9ea1908e..00000000 --- a/src/assets/ProfileViewer/star.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Upload/back.svg b/src/assets/Upload/back.svg deleted file mode 100644 index b01b35a0..00000000 --- a/src/assets/Upload/back.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/Upload/close.svg b/src/assets/Upload/close.svg deleted file mode 100644 index c25251f9..00000000 --- a/src/assets/Upload/close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/Upload/close2.svg b/src/assets/Upload/close2.svg deleted file mode 100644 index d82eae33..00000000 --- a/src/assets/Upload/close2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/Upload/clothingTag.svg b/src/assets/Upload/clothingTag.svg deleted file mode 100644 index 0d90c371..00000000 --- a/src/assets/Upload/clothingTag.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/Upload/next.svg b/src/assets/Upload/next.svg deleted file mode 100644 index 34c4e602..00000000 --- a/src/assets/Upload/next.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/Upload/next_up.svg b/src/assets/Upload/next_up.svg deleted file mode 100644 index 79879621..00000000 --- a/src/assets/Upload/next_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/Upload/picture.svg b/src/assets/Upload/picture.svg deleted file mode 100644 index fc06b54d..00000000 --- a/src/assets/Upload/picture.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/Upload/picture2.svg b/src/assets/Upload/picture2.svg deleted file mode 100644 index 479061c6..00000000 --- a/src/assets/Upload/picture2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/Upload/pin.svg b/src/assets/Upload/pin.svg deleted file mode 100644 index 045cd098..00000000 --- a/src/assets/Upload/pin.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/Upload/plus.svg b/src/assets/Upload/plus.svg deleted file mode 100644 index 192a45c5..00000000 --- a/src/assets/Upload/plus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/Upload/remove.svg b/src/assets/Upload/remove.svg deleted file mode 100644 index aba26031..00000000 --- a/src/assets/Upload/remove.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/Upload/styleTag.svg b/src/assets/Upload/styleTag.svg deleted file mode 100644 index ab9cf9fa..00000000 --- a/src/assets/Upload/styleTag.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/arrow/bottom.svg b/src/assets/arrow/bottom.svg new file mode 100644 index 00000000..faa0158c --- /dev/null +++ b/src/assets/arrow/bottom.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/arrow/left.svg b/src/assets/arrow/left.svg new file mode 100644 index 00000000..60716501 --- /dev/null +++ b/src/assets/arrow/left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/arrow/min-right.svg b/src/assets/arrow/min-right.svg new file mode 100644 index 00000000..cb2dc1b3 --- /dev/null +++ b/src/assets/arrow/min-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/arrow/right.svg b/src/assets/arrow/right.svg new file mode 100644 index 00000000..c6a7a244 --- /dev/null +++ b/src/assets/arrow/right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/arrow/up.svg b/src/assets/arrow/up.svg new file mode 100644 index 00000000..cbdad7c9 --- /dev/null +++ b/src/assets/arrow/up.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/avatar.png b/src/assets/avatar.png deleted file mode 100644 index 19af2db3..00000000 Binary files a/src/assets/avatar.png and /dev/null differ diff --git a/src/assets/back.svg b/src/assets/back.svg deleted file mode 100644 index f6f2fba5..00000000 --- a/src/assets/back.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/assets/default/accept.svg b/src/assets/default/accept.svg new file mode 100644 index 00000000..ef6a41a6 --- /dev/null +++ b/src/assets/default/accept.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/default/alarm-on.svg b/src/assets/default/alarm-on.svg new file mode 100644 index 00000000..499fcdc9 --- /dev/null +++ b/src/assets/default/alarm-on.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/default/alarm.svg b/src/assets/default/alarm.svg new file mode 100644 index 00000000..60492936 --- /dev/null +++ b/src/assets/default/alarm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/block.svg b/src/assets/default/block.svg new file mode 100644 index 00000000..aab30e97 --- /dev/null +++ b/src/assets/default/block.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/camera.svg b/src/assets/default/camera.svg new file mode 100644 index 00000000..8b29a06a --- /dev/null +++ b/src/assets/default/camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default/clothes-tag.svg b/src/assets/default/clothes-tag.svg new file mode 100644 index 00000000..7213c1e3 --- /dev/null +++ b/src/assets/default/clothes-tag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/imageBasic.svg b/src/assets/default/defaultProfile.svg similarity index 100% rename from src/assets/imageBasic.svg rename to src/assets/default/defaultProfile.svg diff --git a/src/assets/default/delete.svg b/src/assets/default/delete.svg new file mode 100644 index 00000000..d610e95f --- /dev/null +++ b/src/assets/default/delete.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/desktopNavBar/home-fill.svg b/src/assets/default/desktopNavBar/home-fill.svg new file mode 100644 index 00000000..10d1d103 --- /dev/null +++ b/src/assets/default/desktopNavBar/home-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/default/desktopNavBar/home.svg b/src/assets/default/desktopNavBar/home.svg new file mode 100644 index 00000000..ebcb6803 --- /dev/null +++ b/src/assets/default/desktopNavBar/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default/desktopNavBar/message-fill.svg b/src/assets/default/desktopNavBar/message-fill.svg new file mode 100644 index 00000000..96912c0a --- /dev/null +++ b/src/assets/default/desktopNavBar/message-fill.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/default/desktopNavBar/message.svg b/src/assets/default/desktopNavBar/message.svg new file mode 100644 index 00000000..72cf52a0 --- /dev/null +++ b/src/assets/default/desktopNavBar/message.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/desktopNavBar/my-page-fill.svg b/src/assets/default/desktopNavBar/my-page-fill.svg new file mode 100644 index 00000000..d377f0b9 --- /dev/null +++ b/src/assets/default/desktopNavBar/my-page-fill.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/default/desktopNavBar/my-page.svg b/src/assets/default/desktopNavBar/my-page.svg new file mode 100644 index 00000000..29a10af1 --- /dev/null +++ b/src/assets/default/desktopNavBar/my-page.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/edit.svg b/src/assets/default/edit.svg new file mode 100644 index 00000000..90a5d2aa --- /dev/null +++ b/src/assets/default/edit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/default/heart-fill.svg b/src/assets/default/heart-fill.svg new file mode 100644 index 00000000..e8ff4a28 --- /dev/null +++ b/src/assets/default/heart-fill.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/default/heart.svg b/src/assets/default/heart.svg new file mode 100644 index 00000000..9a64fd87 --- /dev/null +++ b/src/assets/default/heart.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/default/home-fill.svg b/src/assets/default/home-fill.svg new file mode 100644 index 00000000..d07762d5 --- /dev/null +++ b/src/assets/default/home-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/default/home.svg b/src/assets/default/home.svg new file mode 100644 index 00000000..399a9fb7 --- /dev/null +++ b/src/assets/default/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default/insta.svg b/src/assets/default/insta.svg new file mode 100644 index 00000000..f35d7ec5 --- /dev/null +++ b/src/assets/default/insta.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/default/leave.svg b/src/assets/default/leave.svg new file mode 100644 index 00000000..8fb2c94e --- /dev/null +++ b/src/assets/default/leave.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/like-fill.svg b/src/assets/default/like-fill.svg new file mode 100644 index 00000000..d466c0ee --- /dev/null +++ b/src/assets/default/like-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/like-white.svg b/src/assets/default/like-white.svg new file mode 100644 index 00000000..b6225850 --- /dev/null +++ b/src/assets/default/like-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default/like.svg b/src/assets/default/like.svg new file mode 100644 index 00000000..b09899f3 --- /dev/null +++ b/src/assets/default/like.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/message-fill.svg b/src/assets/default/message-fill.svg new file mode 100644 index 00000000..d49b0b93 --- /dev/null +++ b/src/assets/default/message-fill.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/default/message-white.svg b/src/assets/default/message-white.svg new file mode 100644 index 00000000..b6b4cebe --- /dev/null +++ b/src/assets/default/message-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/message.svg b/src/assets/default/message.svg new file mode 100644 index 00000000..f688ab24 --- /dev/null +++ b/src/assets/default/message.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/modal-close-white.svg b/src/assets/default/modal-close-white.svg new file mode 100644 index 00000000..62cfcd62 --- /dev/null +++ b/src/assets/default/modal-close-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/default/more.svg b/src/assets/default/more.svg new file mode 100644 index 00000000..90534179 --- /dev/null +++ b/src/assets/default/more.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default/my-page-fill.svg b/src/assets/default/my-page-fill.svg new file mode 100644 index 00000000..1664a268 --- /dev/null +++ b/src/assets/default/my-page-fill.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/default/my-page-white.svg b/src/assets/default/my-page-white.svg new file mode 100644 index 00000000..51fdb1c0 --- /dev/null +++ b/src/assets/default/my-page-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/my-page.svg b/src/assets/default/my-page.svg new file mode 100644 index 00000000..3a548ae9 --- /dev/null +++ b/src/assets/default/my-page.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/oodd-white.svg b/src/assets/default/oodd-white.svg new file mode 100644 index 00000000..bbee923c --- /dev/null +++ b/src/assets/default/oodd-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/oodd.svg b/src/assets/default/oodd.svg new file mode 100644 index 00000000..0591441d --- /dev/null +++ b/src/assets/default/oodd.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/photo-big.svg b/src/assets/default/photo-big.svg new file mode 100644 index 00000000..560c25e2 --- /dev/null +++ b/src/assets/default/photo-big.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/photo-white.svg b/src/assets/default/photo-white.svg new file mode 100644 index 00000000..a7914700 --- /dev/null +++ b/src/assets/default/photo-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default/photo.svg b/src/assets/default/photo.svg new file mode 100644 index 00000000..e24e8825 --- /dev/null +++ b/src/assets/default/photo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/default/pin.svg b/src/assets/default/pin.svg new file mode 100644 index 00000000..fe23fcc2 --- /dev/null +++ b/src/assets/default/pin.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/plus.svg b/src/assets/default/plus.svg new file mode 100644 index 00000000..b1979cc6 --- /dev/null +++ b/src/assets/default/plus.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/default/reject.svg b/src/assets/default/reject.svg new file mode 100644 index 00000000..fd65403a --- /dev/null +++ b/src/assets/default/reject.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/default/report.svg b/src/assets/default/report.svg new file mode 100644 index 00000000..06ec3ca1 --- /dev/null +++ b/src/assets/default/report.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/send-comment.svg b/src/assets/default/send-comment.svg new file mode 100644 index 00000000..9bdea55d --- /dev/null +++ b/src/assets/default/send-comment.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/Chats/Send.svg b/src/assets/default/send-message.svg similarity index 73% rename from src/assets/Chats/Send.svg rename to src/assets/default/send-message.svg index ca09f415..65db90d7 100644 --- a/src/assets/Chats/Send.svg +++ b/src/assets/default/send-message.svg @@ -1,3 +1,4 @@ - - \ No newline at end of file + + diff --git a/src/assets/default/setting.svg b/src/assets/default/setting.svg new file mode 100644 index 00000000..e99910b9 --- /dev/null +++ b/src/assets/default/setting.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/default/snsIcon/kakao.svg b/src/assets/default/snsIcon/kakao.svg new file mode 100644 index 00000000..9724ba4a --- /dev/null +++ b/src/assets/default/snsIcon/kakao.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/default/snsIcon/naver.svg b/src/assets/default/snsIcon/naver.svg new file mode 100644 index 00000000..186eb32e --- /dev/null +++ b/src/assets/default/snsIcon/naver.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/default/style-tag.svg b/src/assets/default/style-tag.svg new file mode 100644 index 00000000..bd205a8c --- /dev/null +++ b/src/assets/default/style-tag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/default/x.svg b/src/assets/default/x.svg new file mode 100644 index 00000000..0d9657b7 --- /dev/null +++ b/src/assets/default/x.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/logo.svg b/src/assets/logo.svg deleted file mode 100644 index 2cb4743a..00000000 --- a/src/assets/logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/postImage.png b/src/assets/postImage.png deleted file mode 100644 index c19a521e..00000000 Binary files a/src/assets/postImage.png and /dev/null differ diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/BottomButton/styles.tsx b/src/components/BottomButton/styles.tsx index 7cc908fa..d31c235f 100644 --- a/src/components/BottomButton/styles.tsx +++ b/src/components/BottomButton/styles.tsx @@ -2,10 +2,11 @@ import styled from 'styled-components'; export const ButtonWrapper = styled.div` display: flex; - position: fixed; + position: absolute; bottom: 0; + left: 50%; + transform: translateX(-50%); width: 100%; - max-width: 32rem; height: 6.25rem; background-color: ${({ theme }) => theme.colors.white}; justify-content: flex-end; @@ -14,7 +15,8 @@ export const ButtonWrapper = styled.div` `; export const Button = styled.button<{ disabled: boolean }>` - background-color: ${({ disabled, theme }) => (disabled ? theme.colors.gray3 : theme.colors.black)}; + background: ${({ disabled, theme }) => + disabled ? 'linear-gradient(93deg, #FFC1D6 1.22%, #F8D4D4 99.73%)' : theme.colors.gradient}; border-radius: 0.625rem; font-size: 1rem; width: calc(100% - 2.5rem); @@ -25,8 +27,4 @@ export const Button = styled.button<{ disabled: boolean }>` div { color: white; } - - &:hover { - background-color: ${({ theme }) => theme.colors.gray3}; - } `; diff --git a/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/dto.ts b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/dto.ts new file mode 100644 index 00000000..84cd09f3 --- /dev/null +++ b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/dto.ts @@ -0,0 +1,6 @@ +export interface ReportBottomSheetMenuProps { + onCloseReportSheet: () => void; + onOpenStatusModal: () => void; + sendReport: (reason: string) => void; + isUserReport: boolean; +} diff --git a/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/index.tsx b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/index.tsx new file mode 100644 index 00000000..6f5f05ec --- /dev/null +++ b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/index.tsx @@ -0,0 +1,137 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import BottomButton from '../../../BottomButton/index.tsx'; +import BottomSheetMenu from '../../../BottomSheetMenu/index.tsx'; +import { SheetItemDto } from '../../../BottomSheetMenu/dto.ts'; +import { ReportBottomSheetMenuProps } from './dto.ts'; +import { InputLayout, ReportBottomSheetMenuWrappar } from './styles.tsx'; + +const ReportBottomSheetMenu: React.FC = React.memo( + ({ onCloseReportSheet, onOpenStatusModal, sendReport, isUserReport }) => { + const [inputValue, setInputValue] = useState(''); + const textareaRef = useRef(null); + const [isVisibleTextarea, setIsTextareaVisible] = useState(false); + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus(); // 마운트 또는 업데이트 시 textarea에 포커스 유지 + } + }, []); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + const handleSubmit = useCallback(() => { + sendReport(inputValue); + }, [onCloseReportSheet, onOpenStatusModal]); + + // 유저 신고 사유 목록 + const userReportItems: SheetItemDto[] = [ + { + text: '욕설 및 비방', + action: () => { + sendReport('욕설 및 비방'); + }, + }, + { + text: '사칭', + action: () => { + sendReport('사칭'); + }, + }, + { + text: '불법 활동', + action: () => { + sendReport('불법 활동'); + }, + }, + { + text: '스팸 및 광고', + action: () => { + sendReport('스팸 및 광고'); + }, + }, + { + text: '차별 및 혐오 발언', + action: () => { + sendReport('차별 및 혐오 발언'); + }, + }, + { + text: '스토킹 및 괴롭힘', + action: () => { + sendReport('스토킹 및 괴롭힘'); + }, + }, + { + text: '기타', + action: () => { + setIsTextareaVisible((prev) => !prev); + }, + }, + ]; + + // 게시글 신고 사유 목록 + const postReportItems: SheetItemDto[] = [ + { + text: '부적절한 내용', + action: () => { + sendReport('부적절한 내용'); + }, + }, + { + text: '사기 및 허위 정보', + action: () => { + sendReport('사기 및 허위 정보'); + }, + }, + { + text: '스팸 및 광고', + action: () => { + sendReport('스팸 및 광고'); + }, + }, + { + text: '타인의 권리 침해', + action: () => { + sendReport('타인의 권리 침해'); + }, + }, + { + text: '불법 거래 관련 내용', + action: () => { + sendReport('불법 거래 관련 내용'); + }, + }, + { + text: '기타', + action: () => { + setIsTextareaVisible((prev) => !prev); + }, + }, + ]; + + return ( + + + {isVisibleTextarea && ( + + + + + + )} + {isLoading && } +
+ + {isBlockModalOpen && } + {isStatusModalOpen && } + + ); +}; +export default LikeCommentBottomSheetContent; diff --git a/src/components/PostBase/LikeCommentBottomSheetContent/styles.tsx b/src/components/PostBase/LikeCommentBottomSheetContent/styles.tsx new file mode 100644 index 00000000..f4fa36de --- /dev/null +++ b/src/components/PostBase/LikeCommentBottomSheetContent/styles.tsx @@ -0,0 +1,123 @@ +import styled from 'styled-components'; +import { StyledText } from '../../Text/StyledText'; +import theme from '../../../styles/theme'; +import { UserProfile } from '../styles'; + +export const TabContainer = styled.div` + display: flex; + justify-content: space-around; +`; +export const Tab = styled.div<{ $active: boolean }>` + flex: 1; + text-align: center; + padding: 16px 0; + cursor: pointer; + position: relative; /* ::after를 위해 필요한 설정 */ + + /* 활성화된 탭의 경우 */ + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; /* 하단 경계선 두께 */ + background: ${(props) => (props.$active ? theme.colors.gradient : 'none')}; + } +`; + +export const ContentContainer = styled.div<{ $isCommentTab: boolean }>` + padding: 16px 0; + min-height: 350px; + max-height: 750px; + display: flex; + gap: 16px; + flex-direction: column; + align-items: start; + overflow-y: auto; + + /* Comment 탭일 때만 padding-bottom 추가 */ + padding-bottom: ${(props) => (props.$isCommentTab ? '100px' : '0')}; + + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } +`; + +export const Content = styled(StyledText)` + text-align: center; + margin-top: 20px; +`; + +export const BigUserProfile = styled(UserProfile)` + width: 52px; + height: 52px; +`; + +export const LikeItem = styled.div` + display: flex; + align-items: center; + width: 100%; + + .name { + margin-left: 8px; + } +`; + +export const InputLayout = styled.div` + position: absolute; + width: calc(100% - 40px); + padding: 20px 0; + bottom: 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 10px; + background-color: white; + border-top: 1px solid ${({ theme }) => theme.colors.gray1}; + + textarea { + flex: 1; + display: block; + width: 100%; + height: 50px; + max-height: 70px; + border-radius: 8px; + border: 0.0625rem solid #ededed; + outline: none; + padding: 0.8125rem 0.9375rem; + font-family: 'Pretendard Variable'; + font-size: 1rem; + font-style: normal; + font-weight: 300; + line-height: 150%; + color: #1d1d1d; + background-color: #f8f8f8; + resize: none; + overflow-y: auto; + + scrollbar-width: none; // Firefox + -ms-overflow-style: none; // IE 10+ + &::-webkit-scrollbar { + display: none; // Safari & Chrome + } + } + + button { + background: ${({ theme }) => theme.colors.gradient}; + width: 50px; + height: 50px; + border-radius: 8px; + color: ${({ theme }) => theme.colors.white}; + border: none; + font-size: 0.875rem; + } + + button:disabled { + background-color: #ccc; /* 비활성화된 버튼의 색상 */ + cursor: not-allowed; + } +`; diff --git a/src/components/PostBase/dto.ts b/src/components/PostBase/dto.ts new file mode 100644 index 00000000..a99e6880 --- /dev/null +++ b/src/components/PostBase/dto.ts @@ -0,0 +1,17 @@ +export interface PostBaseProps { + onClickMenu: () => void; +} + +export interface PostTopBarProps { + userName: string; +} + +export interface ImageSwiperProps { + images: string[]; +} + +export interface LikeCommentBottomSheetProps { + tab: 'likes' | 'comments'; + likeCount: number; + commentCount: number; +} diff --git a/src/components/PostBase/index.tsx b/src/components/PostBase/index.tsx new file mode 100644 index 00000000..d54ea54a --- /dev/null +++ b/src/components/PostBase/index.tsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { useRecoilState } from 'recoil'; +import dayjs from 'dayjs'; +import 'dayjs/locale/ko'; + +import theme from '../../styles/theme'; + +import { postIdAtom, userAtom, isPostRepresentativeAtom } from '../../recoil/Post/PostAtom'; + +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 { + PostLayout, + PostContainer, + PostInfoContainer, + UserProfile, + UserName, + MenuBtn, + PostContentContainer, + ContentSkeleton, + Content, + ShowMoreButton, + ImageSkeleton, + IconRow, + IconWrapper, + Icon, + 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); + const [, setIsPostRepresentative] = useRecoilState(isPostRepresentativeAtom); + const [timeAgo, setTimeAgo] = useState(); + const [isTextOverflowing, setIsTextOverflowing] = useState(false); + const [showFullText, setShowFullText] = useState(false); + const [isLikeCommentBottomSheetOpen, setIsLikeCommentBottomSheetOpen] = useState(false); + const [activeTab, setActiveTab] = useState<'likes' | 'comments'>('likes'); // activeTab state + + const nav = useNavigate(); + + useEffect(() => { + setPostId(Number(postId)); + + // 게시글 정보 가져오기 + const getPost = async () => { + try { + const response = await getPostDetailApi(Number(postId)); + const data = response.data; + setPost(data); + setUser(data.user); + setIsPostRepresentative(data.isRepresentative); + setTimeAgo(dayjs(data.createdAt).locale('ko').fromNow()); + } catch (error) { + console.error('Error fetching post data:', error); + } + }; + + getPost(); + }, [postId]); + + const contentRef = useRef(null); + + useEffect(() => { + if (contentRef.current) { + // 실제 높이와 줄 제한 높이 비교 + const { scrollHeight, clientHeight } = contentRef.current; + setIsTextOverflowing(scrollHeight > clientHeight); + } + }, [post?.content]); + + const toggleTextDisplay = () => { + setShowFullText((prev) => !prev); + }; + + const handleUserClick = () => { + if (post?.isPostWriter) { + // 내 게시물인 경우 + nav('/mypage'); + } else { + // 다른 유저의 게시물인 경우 + nav(`/users/${post?.user.userId}`); + } + }; + + const handleLikeCommentOpen = (tab: 'likes' | 'comments') => { + setActiveTab(tab); // 클릭한 버튼에 따라 activeTab 설정 + setIsLikeCommentBottomSheetOpen(true); + }; + + const likeCommentbottomSheetProps: BottomSheetProps = { + isOpenBottomSheet: isLikeCommentBottomSheetOpen, + isHandlerVisible: true, + Component: LikeCommentBottomSheetContent, + onCloseBottomSheet: () => { + setIsLikeCommentBottomSheetOpen(false); + }, + componentProps: { + tab: activeTab, + likeCount: post?.postLikesCount, + commentCount: post?.postCommentsCount, + }, + }; + + // 게시글 좋아요 누르기/취소하기 + 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 ( + + + + + + + + {post?.user && profileImg} + + + {user.nickname} + + + {timeAgo} + + + more + + + + + {!post ? ( + + ) : ( + <> + + {post.content} + + {isTextOverflowing && ( + + {showFullText ? '간략히 보기' : '더 보기'} + + )} + + )} + + + {!post ? : image.imageUrl)} />} + + + + + {post?.isPostLike ? like : like} + + handleLikeCommentOpen('likes')}>{post?.postLikesCount ?? 0} + + handleLikeCommentOpen('comments')}> + + message + + {post?.postCommentsCount ?? 0} + + + + + {post?.postClothings?.map((clothingObj, index) => ( + + ))} + + + + + + + + ); +}; + +export default PostBase; diff --git a/src/components/PostBase/styles.tsx b/src/components/PostBase/styles.tsx new file mode 100644 index 00000000..688187ba --- /dev/null +++ b/src/components/PostBase/styles.tsx @@ -0,0 +1,193 @@ +import styled, { keyframes } from 'styled-components'; +import { StyledText } from '../Text/StyledText'; + +// 그라데이션 애니메이션 정의 +const shimmer = keyframes` + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +`; + +// 공통된 로딩 스타일 +const LoadingSkeleton = styled.div` + background: linear-gradient( + 90deg, + ${({ theme }) => theme.colors.gray1} 25%, + ${({ theme }) => theme.colors.gray2} 50%, + ${({ theme }) => theme.colors.gray1} 75% + ); + background-size: 200% 100%; + animation: ${shimmer} 2s infinite; +`; + +export const PostLayout = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: calc(100vh - 2.75rem); + overflow-y: scroll; + + scrollbar-width: none; // Firefox + -ms-overflow-style: none; // IE 10+ + &::-webkit-scrollbar { + display: none; // Safari & Chrome + } +`; + +export const PostContainer = styled.div` + width: 100%; + max-width: 450px; + height: 100%; + overflow-y: scroll; + + scrollbar-width: none; // Firefox + -ms-overflow-style: none; // IE 10+ + &::-webkit-scrollbar { + display: none; // Safari & Chrome + } +`; + +export const PostInfoContainer = styled.div` + display: flex; + align-items: center; + margin-top: 8px; + margin-bottom: 16px; + padding: 0 20px; + gap: 8px; + align-self: stretch; + + .timeAgo { + margin-left: auto; + } +`; + +export const UserProfile = styled(LoadingSkeleton)` + cursor: pointer; + width: 32px; + height: 32px; + border-radius: 50%; + overflow: hidden; + border: solid 0.5px ${({ theme }) => theme.colors.gray1}; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +export const UserName = styled(StyledText)``; + +export const MenuBtn = styled.button` + width: 18px; + height: 18px; +`; + +export const PostContentContainer = styled.div` + width: 100%; + padding: 0 20px; + margin-bottom: 16px; +`; + +export const ContentSkeleton = styled(LoadingSkeleton)` + width: 100%; + height: 16px; + border-radius: 4px; +`; + +export const Content = styled(StyledText)<{ $showFullText: boolean }>` + word-wrap: break-word; + word-break: break-all; + overflow: hidden; + width: 100%; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: ${({ $showFullText }) => ($showFullText ? 'unset' : '1')}; // 2줄 초과일 때 ...으로 표시 + text-overflow: ellipsis; + white-space: normal; +`; + +export const ShowMoreButton = styled(StyledText)` + cursor: pointer; + color: ${({ theme }) => theme.colors.gray3}; +`; + +export const ImageSkeleton = styled(LoadingSkeleton)` + width: 100%; + aspect-ratio: 4 / 5; +`; + +export const IconRow = styled.div` + display: flex; + height: 20px; + align-items: center; + padding: 0 20px; + margin: 16px 0; +`; + +export const IconWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + + span { + font-size: 15px; + color: #000; + margin-right: 16px; + } +`; + +export const Icon = styled.div` + width: 20px; + height: 20px; + + img { + width: 20px; + height: 20px; + } +`; + +export const ClothingInfoList = styled.div` + margin-bottom: 88px; + display: flex; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; /* Firefox에서 스크롤바 숨기기 */ + -ms-overflow-style: none; /* Internet Explorer에서 스크롤바 숨기기 */ + + &::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera에서 스크롤바 숨기기 */ + } +`; + +export const InputLayout = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + textarea { + display: block; + width: calc(100% - 3rem); + height: 5.75rem; + border-radius: 0.125rem; + border: 0.0625rem solid ${({ theme }) => theme.colors.gray3}; + margin-bottom: 5.875rem; + z-index: 2; + margin-top: -3.75rem; + outline: none; + padding: 0.8125rem 0.9375rem; + font-family: 'Pretendard Variable'; + font-size: 1rem; + font-style: normal; + font-weight: 300; + line-height: 150%; + color: ${({ theme }) => theme.colors.black}; + resize: none; + } +`; diff --git a/src/components/PostItem/dto.ts b/src/components/PostItem/dto.ts new file mode 100644 index 00000000..23cd3a3c --- /dev/null +++ b/src/components/PostItem/dto.ts @@ -0,0 +1,8 @@ +import { UserPostSummary } from '../../apis/post/dto'; + +export interface Post extends UserPostSummary {} + +export interface PostItemProps { + post: Post; + isMyPost?: boolean; +} diff --git a/src/components/PostItem/index.tsx b/src/components/PostItem/index.tsx new file mode 100644 index 00000000..e56ad732 --- /dev/null +++ b/src/components/PostItem/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import theme from '../../styles/theme'; +import { + PostItemContainer, + PostImageContainer, + PostImage, + LikesCountStyledText, + Icon, + LikesOverlay, + PinSvg, +} from './style'; +import HeartSvg from '../../assets/default/like.svg'; +import MessageSvg from '../../assets/default/message.svg'; +import PinIcon from '../../assets/default/pin.svg'; +import { PostItemProps } from './dto'; + +const PostItem: React.FC = ({ post, isMyPost = true }) => { + const navigate = useNavigate(); + const commentsCount = post.postCommentsCount ?? 0; // 현재 api 응답에 commentsCount가 없어 undefine 오류 해결 위해 설정, 추후 api 수정되면 삭제 해도 되는 행 + const imageUrl = post.imageUrl || 'https://via.placeholder.com/72'; + + const handleClick = () => { + const path = isMyPost ? `/my-post/${post.postId}` : `/post/${post.postId}`; + navigate(path); + }; + + return ( + + + + {post.isRepresentative && } + + + + {post.postLikesCount} + + + + {commentsCount} + + + + + ); +}; + +export default PostItem; diff --git a/src/pages/ProfileViewer/components/PostItem/style.tsx b/src/components/PostItem/style.tsx similarity index 60% rename from src/pages/ProfileViewer/components/PostItem/style.tsx rename to src/components/PostItem/style.tsx index 61730883..83c253f1 100644 --- a/src/pages/ProfileViewer/components/PostItem/style.tsx +++ b/src/components/PostItem/style.tsx @@ -1,28 +1,38 @@ import styled from 'styled-components'; +import { StyledText } from '../Text/StyledText'; -export const PostItemContainer = styled.div` +export const PostItemContainer = styled.article` flex: 1 1 calc(50% - 0.5rem); /* 기본적으로 두 개씩 배치되도록 설정 */ - max-width: 16rem; /* 최대 너비 설정 */ - height: 19.4375rem; - aspect-ratio: 14.2225 / 19.4375; /* 가로 세로 비율 유지 */ + width: 100%; + max-width: 67.5rem; /* 최대 너비 설정 */ + aspect-ratio: 1 / 1; /* 정사각형 유지 */ display: flex; flex-direction: column; position: relative; /* LikesOverlay 위치 조정을 위한 설정 */ box-sizing: border-box; + @media (min-width: 23.4375rem) { + &:nth-child(2n + 1):last-child { + flex: 0 1 calc(50% - 0.5rem); /* 마지막 게시물이 남을 때 왼쪽에 위치 */ + margin-right: auto; /* 오른쪽 여백을 자동으로 설정하여 왼쪽 정렬 */ + } + } + @media (max-width: 33.125rem) { flex: 1 1 calc(50% - 0.5rem); /* 작은 화면에서도 두 개씩 배치되도록 설정 */ max-width: 50%; /* 화면 크기에 따라 너비를 조정하여 두 개씩 배치 */ height: auto; /* 높이 자동 조정 */ - aspect-ratio: auto; /* 비율 유지 없이 자동 조정 */ + aspect-ratio: 1 / 1; /* 작은 화면에서도 정사각형 유지 */ } `; -export const PostImageContainer = styled.div` +export const PostImageContainer = styled.figure` width: 100%; height: 100%; + border-radius: 0.5rem; position: relative; overflow: hidden; /* 이미지 잘리지 않도록 */ + cursor: pointer; `; export const PostImage = styled.img` @@ -36,19 +46,17 @@ export const LikesOverlay = styled.div` bottom: 0; /* 하단에 배치 */ width: 100%; display: flex; - align-items: flex-end; + align-items: center; justify-content: flex-end; box-sizing: border-box; `; -export const HeartIcon = styled.img` - margin: 14.25rem 0 0.75rem 8.75rem; // 228px 0px 12px 140px - width: 1.25rem; - height: 1.25rem; +export const Icon = styled.img` + margin-bottom: 0.5rem; `; -export const LikesCount = styled.div` - margin: 0 1.3125rem 0.625rem 0.375rem; // 0 21px 10px 6px +export const LikesCountStyledText = styled(StyledText)` + margin: 0 8px 0.5rem 4px; `; export const PinSvg = styled.img` diff --git a/src/components/Text/StyledText.tsx b/src/components/Text/StyledText.tsx index d5535730..331135a2 100644 --- a/src/components/Text/StyledText.tsx +++ b/src/components/Text/StyledText.tsx @@ -1,17 +1,48 @@ import styled from 'styled-components'; import theme from '../../styles/theme'; +import { useMediaQuery } from 'react-responsive'; + +export type FontStyleKey = keyof typeof theme.fontStyles; + +// 플랫폼 별 폰트가 다른 경우 +interface FontStylesByPlatform { + mobile: FontStyleKey; + tablet: FontStyleKey; + desktop: FontStyleKey; +} export interface StyledTextProps { $textTheme: { - style: keyof typeof theme.fontStyles; - lineHeight: number; + style: FontStyleKey | FontStylesByPlatform; + lineHeight?: number; }; color?: string; + children: any; } export const StyledText = styled.div` - ${(props) => props.theme.fontStyles[props.$textTheme.style]}; - line-height: ${(props) => props.$textTheme.lineHeight}rem; - color: ${(props) => (props.color ? props.color : theme.colors.black)}; + color: ${(props) => props.color || theme.colors.black}; white-space: pre-line; + line-height: ${(props) => props.$textTheme.lineHeight || 1.5}; + ${(props) => { + const isMobile = useMediaQuery({ maxWidth: '767px' }); + const isTabletPortrait = useMediaQuery({ minWidth: '768px', maxWidth: '991px' }); + const isTabletLandscape = useMediaQuery({ minWidth: '992px', maxWidth: '1219px' }); + const isDesktop = useMediaQuery({ minWidth: '1220px' }); + + let fontStyle; + if (typeof props.$textTheme.style === 'string') { + fontStyle = theme.fontStyles[props.$textTheme.style]; + } else if (typeof props.$textTheme.style === 'object') { + if (isMobile) { + fontStyle = theme.fontStyles[props.$textTheme.style.mobile]; + } else if (isTabletPortrait || isTabletLandscape) { + fontStyle = theme.fontStyles[props.$textTheme.style.tablet]; + } else if (isDesktop) { + fontStyle = theme.fontStyles[props.$textTheme.style.desktop]; + } + } + + return fontStyle; + }}; `; diff --git a/src/components/TopBar/dto.ts b/src/components/TopBar/dto.ts index 3398c4be..1f7579f0 100644 --- a/src/components/TopBar/dto.ts +++ b/src/components/TopBar/dto.ts @@ -1,5 +1,4 @@ export interface TopBarProps { - ID?: string; // 사용자 ID, optional prop text?: string; // 텍스트, optional prop RightButtonSrc?: string; // KebabMenuButton src의 Optional prop LeftButtonSrc?: string; // BackButton src의 Optional prop diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index 0d3baabd..0da1c560 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -1,11 +1,9 @@ -import { StyledText } from '../../components/Text/StyledText'; import theme from '../../styles/theme'; -import { TopbarLayout, TextLayout, BackButton, KebabMenuButton } from './styles'; +import { TopbarLayout, StyledTextLayout, LeftButton, RightButton } from './styles'; import { useNavigate } from 'react-router-dom'; import { TopBarProps } from './dto'; const TopBar: React.FC = ({ - ID = '', text = '', RightButtonSrc, LeftButtonSrc, @@ -16,36 +14,35 @@ const TopBar: React.FC = ({ const nav = useNavigate(); return ( - - {' '} - {/*border-bottom 유무*/} - { - if (onLeftClick) { - onLeftClick(); - } else { - nav(-1); - } - }} - /> - - - {ID} - - {text} - - { - if (onRightClick) { - onRightClick(); - } - }} - /> - + <> + + { + if (onLeftClick) { + onLeftClick(); + } else { + nav(-1); + } + }} + > + 뒤로가기 + + + {text} + + { + if (onRightClick) { + onRightClick(); + } + }} + > + 메뉴 + + + ); }; diff --git a/src/components/TopBar/styles.tsx b/src/components/TopBar/styles.tsx index 440b6979..f234b98b 100644 --- a/src/components/TopBar/styles.tsx +++ b/src/components/TopBar/styles.tsx @@ -1,44 +1,47 @@ import styled from 'styled-components'; import { TopbarLayoutProps } from './dto'; +import { StyledText } from '../Text/StyledText'; -export const TopbarLayout = styled.div` - background-color: ${({ theme }) => theme.colors.white}; +export const TopbarLayout = styled.header` display: flex; - width: 100%; - max-width: 32rem; + position: sticky; + top: 0; /* 부모 요소의 상단에 붙도록 설정 */ + z-index: 1; + background-color: white; + width: 100%; /* 부모 너비에 맞춤 */ height: 2.75rem; - justify-content: space-evenly; align-items: center; + padding: 0.5rem 1.25rem; ${({ $withBorder, theme }) => $withBorder && ` - border-bottom: solid 1px ${theme.colors.gray2}; + border-bottom: solid 0.0625rem ${theme.colors.gray2}; `} `; -export const TextLayout = styled.div` - display: flex; +export const StyledTextLayout = styled(StyledText)` flex-direction: column; align-items: center; `; -export const BackButton = styled.img` - width: 1.4rem; - height: 1.4rem; +export const LeftButton = styled.button<{ src?: string }>` + display: ${({ src }) => (src ? 'flex' : 'none')}; /* src가 없으면 버튼 숨김 */ + width: 1.5rem; + height: 1.5rem; + align-items: center; + justify-content: center; + margin-right: 0.5rem; + padding: 0; cursor: pointer; - background-size: cover; - background-position: center; - margin: auto; - margin-left: 1.25rem; `; -export const KebabMenuButton = styled.img` - width: 1.4rem; - height: 1.4rem; +export const RightButton = styled.button<{ src?: string }>` + display: ${({ src }) => (src ? 'flex' : 'none')}; /* src가 없으면 버튼 숨김 */ + width: 1.5rem; + height: 1.5rem; + align-items: center; + justify-content: center; + padding: 0; + margin-left: auto; cursor: pointer; - background-size: cover; - background-position: center; - margin: auto; - margin-right: 1.125rem; - visibility: ${(props) => (props.src ? 'visible' : 'hidden')}; `; diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx new file mode 100644 index 00000000..04df42f0 --- /dev/null +++ b/src/components/UserProfile/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { StyledText } from '../Text/StyledText'; +import theme from '../../styles/theme'; +import { UserProfileContainer, UserImg, UserDetails, BioStyledText } from './style'; + +interface UserProfileProps { + userImg?: string; // string | undefined + bio?: string; + nickname: string; +} + +const UserProfile: React.FC = React.memo(({ userImg, bio = '', nickname }) => { + const truncatedBio = bio ? (bio.length > 50 ? bio.substring(0, 50) + '...' : bio) : ''; + return ( + + + + {nickname} + + {truncatedBio} + + + + ); +}); + +export default UserProfile; diff --git a/src/components/UserProfile/style.tsx b/src/components/UserProfile/style.tsx new file mode 100644 index 00000000..709717e0 --- /dev/null +++ b/src/components/UserProfile/style.tsx @@ -0,0 +1,32 @@ +import styled from 'styled-components'; +import { StyledText } from '../Text/StyledText'; + +export const UserProfileContainer = styled.section` + display: flex; + flex-direction: row; +`; +export const UserImg = styled.img` + width: 4.5rem; + height: 4.5rem; + object-fit: cover; + border-radius: 50%; +`; + +export const UserDetails = styled.section` + display: flex; + flex-direction: column; + justify-content: center; + text-align: left; + gap: 0.5rem; + margin-left: 1rem; +`; + +export const BioStyledText = styled(StyledText)` + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + max-width: 15.5rem; + width: 100%; + text-overflow: ellipsis; +`; diff --git a/src/config/constant.ts b/src/config/constant.ts index f98278eb..bc777657 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,2 +1,2 @@ -// JWT를 로컬 스토리지에 저장할 때 사용하는 키. 추후 수정 export const JWT_KEY = 'jwt_token'; +export const NEW_JWT_KEY = 'new_jwt_token'; diff --git a/src/config/firebaseConfig.ts b/src/config/firebaseConfig.ts new file mode 100644 index 00000000..ae318c5d --- /dev/null +++ b/src/config/firebaseConfig.ts @@ -0,0 +1,18 @@ +import { initializeApp } from 'firebase/app'; +import { getStorage } from 'firebase/storage'; + +const firebaseConfig = { + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, + storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, + appId: import.meta.env.VITE_FIREBASE_APP_ID, + measurementId: import.meta.env.VITE_FIREBASE_MEASURENMENT_ID, +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +const storage = getStorage(app); + +export { storage }; diff --git a/src/context/SocketProvider.tsx b/src/context/SocketProvider.tsx index 3ac55773..eb6e9665 100644 --- a/src/context/SocketProvider.tsx +++ b/src/context/SocketProvider.tsx @@ -7,7 +7,9 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr const [socket, setSocket] = useState(null); useEffect(() => { - const newSocket = io(import.meta.env.VITE_API_URL); + const newSocket = io(`${import.meta.env.VITE_NEW_API_URL}/socket/chatting`, { + transports: ['websocket'], + }); setSocket(newSocket); newSocket.on('connect', () => { diff --git a/src/pages/AccountCancel/index.tsx b/src/pages/AccountCancel/index.tsx index f0973d17..ca9fc4d8 100644 --- a/src/pages/AccountCancel/index.tsx +++ b/src/pages/AccountCancel/index.tsx @@ -6,10 +6,10 @@ import { OODDFrame } from '../../components/Frame/Frame'; import { useNavigate } from 'react-router-dom'; import TopBar from '../../components/TopBar'; -import back from '../../assets/back.svg'; +import back from '../../assets/arrow/left.svg'; import BottomButton from '../../components/BottomButton'; -import request, { BaseResponse } from '../../apis/core'; +import { patchUserWithdrawApi } from '../../apis/user'; // 새로운 API import const AccountCancel: React.FC = () => { const [isChecked, setIsChecked] = useState(false); @@ -26,7 +26,7 @@ const AccountCancel: React.FC = () => { return; } - const storedUserId = localStorage.getItem('id'); + const storedUserId = localStorage.getItem('my_id'); const token = localStorage.getItem('jwt_token'); if (!storedUserId || !token) { @@ -36,32 +36,23 @@ const AccountCancel: React.FC = () => { } // API 요청 - const response = await request.patch>( - `/users/${storedUserId}/sign-out`, - {}, // 요청 본문 - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); + const response = await patchUserWithdrawApi(storedUserId); // 새로운 API 호출 // 요청이 성공했는지 확인 if (response.isSuccess) { // 성공 메시지 출력 - const successMessage = response.result?.message || '계정이 성공적으로 삭제되었습니다.'; - alert(successMessage); + alert('계정이 성공적으로 삭제되었습니다.'); // 계정 삭제 시 localStorage에서 사용자 정보 제거 - localStorage.removeItem('id'); + localStorage.removeItem('my_id'); localStorage.removeItem('jwt_token'); // 로그인 페이지로 리다이렉트 navigate('/login'); } else { // 요청 실패 시 오류 메시지 출력 - console.error('API Error:', response.message || '알 수 없는 오류가 발생했습니다.'); - alert(response.message || 'Failed to delete account'); + console.error('API Error:', response.code || '알 수 없는 오류가 발생했습니다.'); + alert(response.code || 'Failed to delete account'); } } catch (error) { console.error('계정 삭제하는데 오류남:', error); diff --git a/src/pages/AccountCancel/styles.tsx b/src/pages/AccountCancel/styles.tsx index 72383caf..4931d25d 100644 --- a/src/pages/AccountCancel/styles.tsx +++ b/src/pages/AccountCancel/styles.tsx @@ -33,8 +33,9 @@ export const Text = styled.p` export const InfoBox = styled.div` background: #f5f5f5; - padding: 20px; /* 20px */ + padding: 70px; /* 20px */ margin-top: 10px; + border-radius: 10px; margin: 10px 20px 1.25rem 20px; /* 10px 위 여백, 20px 좌우 여백, 20px 아래 여백 */ `; @@ -47,16 +48,19 @@ export const InfoItem = styled.p` align-items: center; text-align: center; height: 100%; /* 부모 컨테이너의 높이에 맞추기 */ + `; export const CheckboxWrapper = styled.div` display: flex; align-items: center; - margin-bottom: 1.25rem; /* 20px */ - padding: 0rem 15px; /* 20px */ + margin-bottom: 1.25rem; + padding: 0rem 15px; input[type='checkbox'] { margin-right: 0.625rem; /* 10px */ + border-radius: 50%; + } `; diff --git a/src/pages/AccountEdit/assets/facebookIcon.png b/src/pages/AccountEdit/assets/facebookIcon.png deleted file mode 100644 index 4f1d667d..00000000 Binary files a/src/pages/AccountEdit/assets/facebookIcon.png and /dev/null differ diff --git a/src/pages/AccountEdit/assets/googleIcon.png b/src/pages/AccountEdit/assets/googleIcon.png deleted file mode 100644 index ceade69b..00000000 Binary files a/src/pages/AccountEdit/assets/googleIcon.png and /dev/null differ diff --git a/src/pages/AccountEdit/assets/kakaoIcon.png b/src/pages/AccountEdit/assets/kakaoIcon.png deleted file mode 100644 index 5132fbbd..00000000 Binary files a/src/pages/AccountEdit/assets/kakaoIcon.png and /dev/null differ diff --git a/src/pages/AccountEdit/assets/naverIcon.png b/src/pages/AccountEdit/assets/naverIcon.png deleted file mode 100644 index 04b15368..00000000 Binary files a/src/pages/AccountEdit/assets/naverIcon.png and /dev/null differ diff --git a/src/pages/AccountEdit/index.tsx b/src/pages/AccountEdit/index.tsx index 18f20b30..598bf59d 100644 --- a/src/pages/AccountEdit/index.tsx +++ b/src/pages/AccountEdit/index.tsx @@ -21,13 +21,10 @@ import { useNavigate } from 'react-router-dom'; import { StyledText } from '../../components/Text/StyledText'; import theme from '../../styles/theme'; -import naverIcon from './assets/naverIcon.png'; -import googleIcon from './assets/googleIcon.png'; -import kakaoIcon from './assets/kakaoIcon.png'; -import facebookIcon from './assets/facebookIcon.png'; - +import naver from '../../assets/default/snsIcon/naver.svg'; +import kakao from '../../assets/default/snsIcon/kakao.svg'; import TopBar from '../../components/TopBar'; -import back from '../../assets/back.svg'; +import back from '../../assets/arrow/left.svg'; const AccountEdit: React.FC = () => { const navigate = useNavigate(); // useNavigate 훅 사용 @@ -60,10 +57,8 @@ const AccountEdit: React.FC = () => { - - - - + + diff --git a/src/pages/AccountSetting/assets/cancleaccount.svg b/src/pages/AccountSetting/assets/cancleaccount.svg deleted file mode 100644 index 6e77344a..00000000 --- a/src/pages/AccountSetting/assets/cancleaccount.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/pages/AccountSetting/assets/exit.svg b/src/pages/AccountSetting/assets/exit.svg deleted file mode 100644 index 4a32205b..00000000 --- a/src/pages/AccountSetting/assets/exit.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/pages/AccountSetting/assets/next.svg b/src/pages/AccountSetting/assets/next.svg deleted file mode 100644 index 19807d84..00000000 --- a/src/pages/AccountSetting/assets/next.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/AccountSetting/assets/useredit.svg b/src/pages/AccountSetting/assets/useredit.svg deleted file mode 100644 index 2cb6cb2b..00000000 --- a/src/pages/AccountSetting/assets/useredit.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/pages/AccountSetting/dto.tsx b/src/pages/AccountSetting/dto.tsx deleted file mode 100644 index 6211eaeb..00000000 --- a/src/pages/AccountSetting/dto.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// UserResponse.ts -export interface UserResponse { - id: number; - name: string; - email: string; - profilePictureUrl: string | null; -} diff --git a/src/pages/AccountSetting/index.tsx b/src/pages/AccountSetting/index.tsx index 61efd7bb..b4ec09bc 100644 --- a/src/pages/AccountSetting/index.tsx +++ b/src/pages/AccountSetting/index.tsx @@ -4,18 +4,16 @@ import { useNavigate } from 'react-router-dom'; import { ProfileEditContainer, ProfilePic, ProfilePicWrapper, Label, Row, List, ListItem } from './styles'; import { OODDFrame } from '../../components/Frame/Frame'; import ConfirmationModal from '../../components/ConfirmationModal'; -import useredit from './assets/useredit.svg'; -import exit from './assets/exit.svg'; -import cancleaccount from './assets/cancleaccount.svg'; -import next from './assets/next.svg'; import { StyledText } from '../../components/Text/StyledText'; import theme from '../../styles/theme'; import TopBar from '../../components/TopBar'; -import back from '../../assets/back.svg'; +import back from '../../assets/arrow/left.svg'; import request, { BaseResponse } from '../../apis/core'; import { UserProfileResponse } from '../ProfileEdit/dto'; -import imageBasic from '../../assets/imageBasic.svg'; +import imageBasic from '../../assets/default/defaultProfile.svg'; import Loading from '../../components/Loading'; +import Profile_s from './../../assets/default/my-page.svg'; +import leave from '../../assets/default/leave.svg'; const AccountSetting: React.FC = () => { const navigate = useNavigate(); @@ -51,10 +49,6 @@ const AccountSetting: React.FC = () => { navigate('/login'); }; - const handleEditProfileClick = () => { - navigate('/account-edit'); - }; - const handleLogoutClick = () => { setIsLogoutModalOpen(true); }; @@ -69,7 +63,7 @@ const AccountSetting: React.FC = () => { }; if (!userProfile) { - return ; // 로딩 상태 + return ; // 로딩 상태 } return ( @@ -84,39 +78,31 @@ const AccountSetting: React.FC = () => { + - - 회원 정보 수정 아이콘 - - 회원 정보 수정 - - 다음 아이콘 - - 로그아웃 아이콘 + 로그아웃 아이콘 Logout - 다음 아이콘 - 회원 탈퇴 아이콘 + 회원 탈퇴 아이콘 회원탈퇴 - 다음 아이콘 {isLogoutModalOpen && ( diff --git a/src/pages/AccountSetting/styles.tsx b/src/pages/AccountSetting/styles.tsx index 50c1b29d..cbf82ed4 100644 --- a/src/pages/AccountSetting/styles.tsx +++ b/src/pages/AccountSetting/styles.tsx @@ -15,7 +15,7 @@ export const ProfilePicWrapper = styled.div` flex-direction: column; align-items: center; margin-bottom: 1.25rem; /* 20px */ - margin-top: 50px; + margin-top: 24px; `; export const ProfilePic = styled.div` @@ -35,7 +35,6 @@ export const ProfilePic = styled.div` `; export const Label = styled.div` - margin-top: 600px; /* 60px */ text-align: center; `; @@ -44,7 +43,7 @@ export const Row = styled.div` justify-content: center; align-items: center; width: 100%; - margin-bottom: 1.875rem; /* 14px */ + margin-bottom: 10px; ${Label} { width: auto; @@ -52,6 +51,7 @@ export const Row = styled.div` } `; + export const FileInput = styled.input` display: none; `; @@ -59,16 +59,18 @@ export const FileInput = styled.input` export const List = styled.ul` width: 100%; padding: 0; - margin-top: 300px; /* 200px */ + margin: 0; list-style: none; - border-top: 1px solid #eee; + border-top: 0px solid #eee; + position: absolute; + bottom: 20px; `; export const ListItem = styled.li` display: flex; align-items: center; padding: 15px 1.25rem; /* 15px 20px */ - border-bottom: 1px solid #eee; + border-bottom: 0px solid #eee; cursor: pointer; & img:first-child { diff --git a/src/pages/Chats/ChatRoom/ChatBox/index.tsx b/src/pages/Chats/ChatRoom/ChatBox/index.tsx index 0b0636ee..b5ed47f0 100644 --- a/src/pages/Chats/ChatRoom/ChatBox/index.tsx +++ b/src/pages/Chats/ChatRoom/ChatBox/index.tsx @@ -1,23 +1,21 @@ -import { ChatBoxContainer, Textarea, SendIcon } from './styles'; +import { ChatBoxContainer, Textarea, SendButton } from './styles'; import { useEffect, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import Send from '../../../../assets/Chats/Send.svg'; import { useParams } from 'react-router-dom'; -import { OpponentInfoAtom } from '../../../../recoil/OpponentInfo'; +import { OpponentInfoAtom } from '../../../../recoil/util/OpponentInfo'; import { useSocket } from '../../../../context/SocketProvider'; const ChatBox: React.FC = () => { const opponentInfo = useRecoilValue(OpponentInfoAtom); - const storageValue = localStorage.getItem('id'); + const storageValue = localStorage.getItem('my_id'); const userId = storageValue ? Number(storageValue) : -1; - const { roomId } = useParams(); - const roomIdNumber = Number(roomId); + const { chatRoomId } = useParams(); const textareaRef = useRef(null); const [newMessage, setNewMessage] = useState(''); const socket = useSocket(); - const isOpponentValid = !!(opponentInfo && opponentInfo.id && opponentInfo.name); + const isOpponentValid = !!(opponentInfo && opponentInfo.id); // textarea 내용에 따라 높이 조정 useEffect(() => { @@ -45,7 +43,15 @@ const ChatBox: React.FC = () => { // 메시지 전송 if (socket) { - socket.emit('message', roomIdNumber, userId, opponentInfo?.id, newMessage); + const sendMessageRequest = { + chatRoomId: Number(chatRoomId), + toUserId: opponentInfo?.id, + message: newMessage, + fromUserId: userId, + createdAt: new Date().toISOString(), + }; + + socket.emit('sendMessage', sendMessageRequest); setNewMessage(''); } }; @@ -61,13 +67,14 @@ const ChatBox: React.FC = () => { - - - ); -}); - -export default ReportTextarea; diff --git a/src/pages/Home/Tooltip/TooltipBubble/index.tsx b/src/pages/Home/Tooltip/TooltipBubble/index.tsx deleted file mode 100644 index 64c4aa84..00000000 --- a/src/pages/Home/Tooltip/TooltipBubble/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { StyledText } from '../../../../components/Text/StyledText'; -import theme from '../../../../styles/theme'; -import { TooltipLayout, TooltipContentBox, TooltipArrow } from './styles'; - -const TooltipBubble: React.FC<{ content: string; arrow: string; top?: number }> = ({ content, arrow, top }) => { - return ( - - - - {content} - - - - - ); -}; - -export default TooltipBubble; diff --git a/src/pages/Home/Tooltip/TooltipBubble/styles.tsx b/src/pages/Home/Tooltip/TooltipBubble/styles.tsx deleted file mode 100644 index 53689cc5..00000000 --- a/src/pages/Home/Tooltip/TooltipBubble/styles.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import styled from 'styled-components'; - -export const TooltipWrapper = styled.div` - background-color: rgb(0, 0, 0, 0.3); - position: fixed; - display: inline-block; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 100; -`; - -export const TooltipLayout = styled.div<{ $top?: number }>` - background-color: ${({ theme }) => theme.colors.white}; - position: fixed; - display: flex; - left: 50%; - ${({ $top }) => ($top ? `top: ${$top}px` : 'bottom: 13.5rem')}; - transform: translate(-50%, -50%); - border-radius: 0.625rem; - width: 13rem; - height: 4rem; - z-index: 200; -`; - -export const TooltipArrow = styled.div<{ $arrow: string }>` - position: absolute; - bottom: -11px; - left: ${({ $arrow }) => `${$arrow}`}; - transform: translateX(-50%); - border-width: 15px 8px 0 8px; - border-style: solid; - border-color: ${({ theme }) => theme.colors.white} transparent transparent transparent; -`; - -export const TooltipContentBox = styled.div` - display: flex; - flex: 1; - justify-content: center; - align-items: center; - text-align: center; -`; diff --git a/src/pages/Home/Tooltip/index.tsx b/src/pages/Home/Tooltip/index.tsx deleted file mode 100644 index 89bd092e..00000000 --- a/src/pages/Home/Tooltip/index.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { useEffect, useState } from 'react'; -import TooltipBubble from './TooltipBubble'; - -import Cookies from 'js-cookie'; -import { TooltipWrapper } from './styles'; - -interface TooltipDto { - cardRef: React.MutableRefObject; - ootdTooltipRef: React.MutableRefObject; - activeIndex: number; -} - -const Tooltip: React.FC = ({ cardRef, ootdTooltipRef, activeIndex }) => { - const [isOpenMatchingTooltip, setIsOpenMatchingTooltip] = useState(false); - const [isOpenOotdTooltip, setIsOpenOotdTooltip] = useState(false); - const [matchingTooltipIndex, setMatchingTooltipIndex] = useState(0); - const [ootdTooltipIndex, setOotdTooltipIndex] = useState(0); - const [matchingTooltipBottom, setMatchingTooltipBottom] = useState(0); - - const onClickMatchingTooltip = () => { - if (matchingTooltipIndex < 1) { - setMatchingTooltipIndex((prev) => prev + 1); - } else { - setIsOpenMatchingTooltip(false); - Cookies.set('hasSeenMatchingTooltip', 'true'); - } - }; - - const onClickOotdTooltip = () => { - if (ootdTooltipIndex < 2) { - setOotdTooltipIndex((prev) => prev + 1); - } else { - setIsOpenOotdTooltip(false); - Cookies.set('hasSeenOotdTooltip', 'true'); - } - }; - - useEffect(() => { - const seenMatching = Cookies.get('hasSeenMatchingTooltip'); - const seenOotd = Cookies.get('hasSeenOotdTooltip'); - - // 매칭 탭에서 툴팁이 표시된 적이 없으면 - if (!seenMatching && activeIndex === 0) { - const element = cardRef.current; - if (element) { - setIsOpenMatchingTooltip(true); - - setTimeout(() => { - // 선택된 요소의 위치 계산 - const rect = element.getBoundingClientRect(); - const scrollTop = document.documentElement.scrollTop; - const viewportHeight = window.innerHeight; - - const desiredPosition = viewportHeight - 73; - const scrollToPosition = rect.bottom + scrollTop - desiredPosition; - - window.scrollTo({ - top: scrollToPosition, - behavior: 'smooth', - }); - }, 300); // 스와이퍼가 완료된 후 스크롤 - - // 스크롤 된 뷰포트를 기준으로 다시 위치 계산 - setTimeout(() => { - const newRect = element.getBoundingClientRect(); - const tooltipBottom = newRect.bottom - 225; - - // 툴팁 위치 설정 - setMatchingTooltipBottom(tooltipBottom); - }, 500); // 스크롤이 완료된 후 위치 계산 - } - } else { - setIsOpenMatchingTooltip(false); - } - - // ootd 탭에서 툴팁이 표시된 적이 없으면 - if (!seenOotd && activeIndex === 1) { - if (ootdTooltipRef) { - setIsOpenOotdTooltip(true); - - const element = ootdTooltipRef.current[1]; - - if (element) { - setTimeout(() => { - // 선택된 요소의 위치 계산 - const rect = element.getBoundingClientRect(); - const scrollTop = document.documentElement.scrollTop; - const viewportHeight = window.innerHeight; - - const desiredPosition = viewportHeight - 73; - const scrollToPosition = rect.bottom + scrollTop - desiredPosition; - - window.scrollTo({ - top: scrollToPosition, - behavior: 'smooth', - }); - }, 100); // 페이지가 다 렌더링 된 후 스크롤 - } - setIsOpenOotdTooltip(true); - } - } else { - setIsOpenOotdTooltip(false); - } - }, [activeIndex, ootdTooltipRef]); - - return ( - <> - {isOpenMatchingTooltip && ( - - {matchingTooltipIndex === 0 ? ( - - ) : null} - {matchingTooltipIndex === 1 ? ( - - ) : null} - - )} - {isOpenOotdTooltip && ( - - {ootdTooltipIndex === 0 ? ( - - ) : null} - {ootdTooltipIndex === 1 ? ( - - ) : null} - {ootdTooltipIndex === 2 ? ( - - ) : null} - - )} - - ); -}; - -export default Tooltip; diff --git a/src/pages/Home/Tooltip/styles.tsx b/src/pages/Home/Tooltip/styles.tsx deleted file mode 100644 index e6597236..00000000 --- a/src/pages/Home/Tooltip/styles.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import styled from 'styled-components'; - -export const TooltipWrapper = styled.div` - background-color: rgb(0, 0, 0, 0.3); - position: fixed; - display: inline-block; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 100; -`; diff --git a/src/pages/Home/dto.ts b/src/pages/Home/dto.ts index 6704e2e3..1151716d 100644 --- a/src/pages/Home/dto.ts +++ b/src/pages/Home/dto.ts @@ -4,3 +4,9 @@ export interface ApiDto { message: string; result: any[]; } + +export interface MatchingInfoDto { + requesterId: number; + targetId: number; + targetName: string; +} diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index f003d9ae..3756b895 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,232 +1,16 @@ -import React, { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; import { OODDFrame } from '../../components/Frame/Frame'; -import HomeTabBar from './HomeTabBar'; -import HomeTopBar from './HomeTopBar'; import NavBar from '../../components/NavBar'; +import HomeTopBar from './HomeTopBar'; +import OOTD from './OOTD/index.tsx'; import { HomeContainer } from './styles'; -import request, { BaseResponse } from '../../apis/core'; - -import Modal from '../../components/Modal/index.tsx'; -import HeartBottomSheet from './BottomSheets/HeartBottomSheet.tsx'; -import { - IsOpenRequestFailModalAtom, - IsOpenRequestSuccessModalAtom, - PostRequestAtom, -} from '../../recoil/HeartBottomSheetAtom.ts'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { ModalProps } from '../../components/Modal/dto.ts'; -import { - IsOpenBlockConfirmationModalAtom, - IsOpenBlockFailModalAtom, - IsOpenBlockSuccessModalAtom, - PostBlockAtom, -} from '../../recoil/BlockBottomSheetAtom.ts'; -import BlockConfirmationModal from './BottomSheets/BlockBottomSheet.tsx'; -import MeatballBottomSheet from './BottomSheets/MeatballBottomSheet.tsx'; -import ReportBottomSheet from './BottomSheets/ReportBottomSheet.tsx'; -import { - IsOpenMeatballBottomSheetAtom, - IsOpenReportBottomSheetAtom, - IsOpenReportFailModalAtom, - IsOpenReportSuccessModalAtom, - PostReportAtom, -} from '../../recoil/MeatballBottomSheetAtom.ts'; -import PostCommentBottomSheet from './BottomSheets/PostCommentBottomSheet.tsx'; -import { - IsOpenPostCommentBottomSheetAtom, - IsOpenPostCommentFailModalAtom, - IsOpenPostCommentSuccessModalAtom, -} from '../../recoil/PostCommentBottomSheetAtom.ts'; - -interface UserResponseType { - id: number; - name: string; - email: string; - nickname: string | null; - phoneNumber: string | null; - profilePictureUrl: string; - bio: string | null; - joinedAt: string; -} - -interface UserResponse extends BaseResponse {} // Home 페이지입니다. const Home: React.FC = () => { - const navigate = useNavigate(); - - // 모달과 바텀시트 상태 및 로직 - const [isOpenRequestSuccessModal, setIsOpenRequestSuccessModal] = useRecoilState(IsOpenRequestSuccessModalAtom); - const [isOpenRequestFailModal, setIsOpenRequestFailModal] = useRecoilState(IsOpenRequestFailModalAtom); - const postRequest = useRecoilValue(PostRequestAtom); - const [isOpenBlockSuccessModal, setIsOpenBlockSuccessModal] = useRecoilState(IsOpenBlockSuccessModalAtom); - const [isOpenBlockFailModal, setIsOpenBlockFailModal] = useRecoilState(IsOpenBlockFailModalAtom); - const isOpenBlockConfirmationModal = useRecoilValue(IsOpenBlockConfirmationModalAtom); - const postBlock = useRecoilValue(PostBlockAtom); - const [, setIsOpenMeatballBottomSheet] = useRecoilState(IsOpenMeatballBottomSheetAtom); - const [, setIsOpenReportBottomSheet] = useRecoilState(IsOpenReportBottomSheetAtom); - const [isOpenReportSuccessModal, setIsOpenReportSuccessModal] = useRecoilState(IsOpenReportSuccessModalAtom); - const [isOpenReportFailModal, setIsOpenReportFailModal] = useRecoilState(IsOpenReportFailModalAtom); - const postReport = useRecoilValue(PostReportAtom); - const [, setIsOpenPostCommentBottomSheet] = useRecoilState(IsOpenPostCommentBottomSheetAtom); - const [isOpenPostCommentSuccessModal, setIsOpenPostCommentSuccessModal] = useRecoilState( - IsOpenPostCommentSuccessModalAtom, - ); - const [isOpenPostCommentFailModal, setIsOpenPostCommentFailModal] = useRecoilState(IsOpenPostCommentFailModalAtom); - - // 로그인 여부에 따라 navigate - useEffect(() => { - const checkAuth = async () => { - const userId = localStorage.getItem('id'); - const token = localStorage.getItem('jwt_token'); - - if (!userId || !token) { - navigate('/login'); - return; - } - - try { - const response = await request.get(`/users/${userId}`); - if (!response || !response.result.id) { - console.log(response); - navigate('/login'); - } - } catch (error) { - console.error('Failed to authenticate user:', error); - navigate('/login'); - } - }; - - checkAuth(); - }, [navigate]); - - // feed - // x 버튼 클릭 시 - const blockSuccessModalProps: ModalProps = { - onClose: () => { - setIsOpenBlockSuccessModal(false); - }, - content: `${postBlock?.friendName} 님을 차단했어요`, - }; - - const blockFailModalProps: ModalProps = { - onClose: () => { - setIsOpenBlockFailModal(false); - }, - content: `차단에 실패했어요\n잠시 뒤 다시 시도해 보세요`, - }; - - // 하트 버튼 클릭 시 - const requestSuccessModalProps: ModalProps = { - onClose: () => { - setIsOpenRequestSuccessModal(false); - }, - content: `${postRequest?.targetName} 님에게 대표 OOTD와\n한줄 메시지를 보냈어요!`, - }; - - const requestFailModalProps: ModalProps = { - onClose: () => { - setIsOpenRequestFailModal(false); - }, - content: `요청에 실패했어요\n잠시 뒤 다시 시도해 보세요`, - }; - - // 코멘트 남기기 버튼 - const postCommentSuccessModalProps: ModalProps = { - onClose: () => { - setIsOpenPostCommentSuccessModal(false); - }, - content: '코멘트가 전달되었어요', - }; - - const postCommentFailModalProps: ModalProps = { - onClose: () => { - setIsOpenPostCommentFailModal(false); - }, - content: '일시적인 오류입니다다', - }; - - // 신고하기 메뉴 - const reportSuccessModalProps: ModalProps = { - onClose: () => { - setIsOpenReportSuccessModal(false); - }, - content: `${postReport?.userName} 님의\nOOTD를 신고했어요`, - }; - - const reportFailModalProps: ModalProps = { - onClose: () => { - setIsOpenReportFailModal(false); - }, - content: `신고에 실패했어요\n잠시 뒤 다시 시도해 보세요`, - }; - - // 코멘트 남기기 버튼 클릭 시 - // const commentProps: CommentProps = { - // content: `${userName}님의 게시물에 대한 코멘트를 남겨주세요.\n코멘트는 ${userName}님에게만 전달됩니다.`, - // sendComment: (message: string) => { - // const postNewRequest = async () => { - // if (postRequest) { - // const response = await request.post('/user-relationships', { - // requesterId: postRequest.requesterId, - // targetId: postRequest.targetId, - // message: message, - // }); - - // if (response.isSuccess) { - // setIsOpenHeartBottomSheet(false); - // setTimeout(() => { - // setIsOpenRequestSuccessModal(true); - // }, 100); - // } else { - // setIsOpenRequestFailModal(true); - // } - // } else { - // alert('잘못된 요청입니다.'); - // } - // }; - - // postNewRequest(); - // }, - // }; - - // const commentSheetProps: BottomSheetProps = { - // isOpenBottomSheet: isCommentModalOpen, - // isHandlerVisible: true, - // Component: Comment, - // componentProps: commentProps, - // onCloseBottomSheet: () => { - // setIsCommentModalOpen(false); - // }, - // }; - return ( - {isOpenBlockConfirmationModal && } - {isOpenBlockSuccessModal && } - {isOpenBlockFailModal && } - - - {isOpenRequestSuccessModal && } - {isOpenRequestFailModal && } - - - {isOpenPostCommentSuccessModal && } - {isOpenPostCommentFailModal && } - - - - {isOpenReportSuccessModal && } - {isOpenReportFailModal && } - - setIsOpenMeatballBottomSheet(true)} - onOpenReportSheet={() => setIsOpenReportBottomSheet(true)} - onOpenCommentModal={() => setIsOpenPostCommentBottomSheet(true)} - /> + diff --git a/src/pages/Home/styles.tsx b/src/pages/Home/styles.tsx index 88819d07..9f7bb5e2 100644 --- a/src/pages/Home/styles.tsx +++ b/src/pages/Home/styles.tsx @@ -10,74 +10,32 @@ export const HomeContainer = styled.div` // HomeTopBar -export const HomeTopBarContainer = styled.div` +export const HomeTopBarContainer = styled.header` width: 100%; - max-width: 32rem; - height: 2.75rem; + padding: 0.5rem 1.25rem; display: flex; justify-content: space-between; background-color: white; - z-index: 10; + z-index: 20; align-items: center; position: fixed; + ${({ theme }) => theme.visibleOnMobileTablet}; `; export const HomeLogo = styled.img` - width: 6.6875rem; - height: 1.6875rem; - margin-left: 1.25rem; - cursor: pointer; - overflow: hidden; - - img { - width: 100%; - height: 100%; - } + padding: 0.0938rem 0; `; -// HomeTabBar - -export const TabLayout = styled.div` +export const ButtonContainer = styled.div` display: flex; - flex-direction: column; - width: 100%; - height: auto; -`; - -export const HomeTabBarLayout = styled.div` - position: fixed; - width: 100%; - max-width: 32rem; - background-color: white; - z-index: 10; - top: 2.75rem; - height: 2.5rem; - border-bottom: 0.063rem solid ${({ theme }) => theme.colors.gray2}; + gap: 1rem; `; -export const HomeTabBarList = styled.ul` - height: 2.5rem; +export const Button = styled.button` + width: 1.125rem; + height: 1.125rem; display: flex; - gap: 1.25rem; - justify-content: space-between; - margin: 0 1.25rem; -`; - -export const HomeTabBarWrapper = styled.li<{ $isSelected: boolean; $isPointer: boolean }>` - margin-top: 1rem; - border-bottom: 0.125rem solid ${({ $isSelected }) => ($isSelected ? 'black' : 'transparent')}; - text-align: center; - flex-grow: 1; - flex-basis: 0; - cursor: ${({ $isPointer }) => ($isPointer ? 'pointer' : '')}; -`; - -export const Tabs = styled.div` - margin-top: 5.25rem; - z-index: 0; - flex-grow: 1; - height: 100%; - .swiper-container { - height: 100%; - } + justify-content: center; + align-items: center; + border-radius: 0.03rem; `; diff --git a/src/pages/Login/components/Google/GoogleCallback.tsx b/src/pages/Login/components/Google/GoogleCallback.tsx deleted file mode 100644 index 48962977..00000000 --- a/src/pages/Login/components/Google/GoogleCallback.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; -import Loading from '../../../../components/Loading'; - -const GoogleCallback: React.FC = () => { - const navigate = useNavigate(); - useEffect(() => { - const query = new URLSearchParams(window.location.search); - const code = query.get('code'); - console.log(code); - - if (code) { - // 인증 코드를 쿼리스트링으로 백엔드 서버에 전송 - axios - .get(`https://api-dev.oodd.today/auth/login/google?code=${code}`) - .then((response) => { - const statusCode = response.status; - console.log(JSON.stringify(response.data)); - const token = response.data.accessToken; // 응답 중, 성공 여부와 user 정보 추출 - localStorage.setItem('id', '22'); // 로그인 성공을 하면... isSuccess랑 토큰이 오니까... 내 정보 조회를 먼저 해서 id를 가져 와서 로컬에 저장하기~ - localStorage.removeItem('jwt_token'); - localStorage.setItem('jwt_token', token); - console.log(token); - console.log(localStorage.getItem('jwt_token')); - if (statusCode === 200) { - // Postman에서 api 호출해 보고 응답을 보고 적어야 함 - // user.id를 서버로 보내 해당 유저의 nickname 유무에 따른 리디렉션 - navigate('/'); - } else { - console.error('로그인 실패:', response.data); - alert('구글 계정의 정보를 불러오지 못했습니다.'); - navigate('/login'); - // 로그인 실패 시 처리 (예: 오류 페이지로 리디렉션) - } - }) - .catch((error) => { - console.error('서버 요청 실패:', error); - }); - } else { - // 인증 코드가 없는 경우 처리 - console.error('인증 코드가 없습니다.'); - } - }, [navigate]); - return ; -}; - -export default GoogleCallback; diff --git a/src/pages/Login/components/Google/index.tsx b/src/pages/Login/components/Google/index.tsx deleted file mode 100644 index 6a272fbc..00000000 --- a/src/pages/Login/components/Google/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { StyledText } from '../../../../components/Text/StyledText'; -import theme from '../../../../styles/theme'; -import google from '../../../../assets/Login/google.png'; -import { SocialLogin, LogoImgWrapper, LogoImage, TextWrapper } from '../style'; - -const Google: React.FC = () => { - const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID; - const redirectUri = encodeURIComponent( - import.meta.env.VITE_DEV_DOMAIN - ? import.meta.env.VITE_DEV_DOMAIN + '/auth/google/callback' - : 'http://localhost:3000/auth/google/callback', - ); - - const handleLogin = () => { - window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=profile email`; - }; - - return ( - - - - - - - 구글로 시작하기 - - - - ); -}; - -export default Google; diff --git a/src/pages/Login/components/Kakao/KakaoCallback.tsx b/src/pages/Login/components/Kakao/KakaoCallback.tsx index ef51ff2b..8c3df685 100644 --- a/src/pages/Login/components/Kakao/KakaoCallback.tsx +++ b/src/pages/Login/components/Kakao/KakaoCallback.tsx @@ -1,68 +1,58 @@ -//카카오 인증 완료 후 인증 코드는 승인된 리디렉트 URL로 - -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; -import { UserInfoDto } from '../../../ProfileViewer/ResponseDto/UserInfoDto'; -import request from '../../../../apis/core'; + import Loading from '../../../../components/Loading'; +import Modal from '../../../../components/Modal'; + +import { handleError } from '../../../../apis/util/handleError'; const KakaoCallback: React.FC = () => { const navigate = useNavigate(); + const apiBaseUrl = import.meta.env.VITE_NEW_API_URL; + + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMessage, setModalMessage] = useState(''); useEffect(() => { - const code = new URL(window.location.href).searchParams.get('code'); // URL에서 인증 코드 추출 - console.log(code); // 인증 코드 출력 + const handleKakaoLogin = async () => { + try { + // URL에서 인증 코드 추출 + const code = new URL(window.location.href).searchParams.get('code'); + console.log('인증 코드:', code); + + if (!code) { + throw new Error('인증 코드가 없습니다.'); + } + + // 리다이렉트 URL 설정 및 서버 URL 생성 + const redirectUrl = encodeURIComponent('http://localhost:3000/login/complete'); + const serverUrl = `${apiBaseUrl}/auth/login/kakao?redirectUrl=${redirectUrl}`; + + // 서버로 리다이렉션 + window.location.href = serverUrl; + } catch (error) { + // 에러 처리 + console.error('카카오 로그인 중 오류 발생:', error); + const errorMessage = handleError(error); + setModalMessage(`카카오 ${errorMessage}`); + setIsModalOpen(true); + } + }; - if (code) { - // 인증 코드를 쿼리스트링으로 백엔드 서버에 전송 - axios - .get(`https://api-dev.oodd.today/auth/login/kakao?code=${code}`) - .then((response) => { - const statusCode = response.status; // 200 OK - console.log(JSON.stringify(response.data)); - if (statusCode === 200) { - // 추후 Postman에서 api 호출해 보고 응답을 보고 적어야 함 - // userid를 서버로 보내 해당 유저의 nickname 유무에 따른 리디렉션 - const token = response.data.accessToken; + handleKakaoLogin(); + }, [navigate, apiBaseUrl]); - localStorage.setItem('id', response.data.id); // 응답으로 id가 오지 않기 때문에 여기서 설정해야 함 수정 필요? - localStorage.removeItem('jwt_token'); - localStorage.setItem('jwt_token', token); - const userid = localStorage.getItem('id'); + const handleModalClose = () => { + setIsModalOpen(false); + navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동 + }; - request - .get(`/users/${userid}`) - .then((response) => { - console.log(response); - if (response.result.nickname) { - navigate('/'); - } else { - navigate(`/signup`); - } - }) - .catch((error) => { - // API 요청 실패 시 처리 - console.error('API 요청 실패:', error); - alert('사용자 정보를 불러오지 못했습니다.'); - navigate('/login'); // 실패 시 로그인 페이지로 리디렉션 - }); - } else { - console.error('로그인 실패:', response.data); - alert('카카오 계정의 정보를 불러오지 못했습니다.'); - navigate('/login'); - // 로그인 실패 시 처리 (예: 오류 페이지로 리디렉션) - } - }) - .catch((error) => { - console.error('서버 요청 실패:', error); - }); - } else { - // 인증 코드가 없는 경우 처리 - console.error('인증 코드가 없습니다.'); - } - }, [navigate]); - return ; + return ( + <> + + {isModalOpen && } + + ); }; export default KakaoCallback; diff --git a/src/pages/Login/components/Kakao/KakaoLoginDto.ts b/src/pages/Login/components/Kakao/KakaoLoginDto.ts deleted file mode 100644 index 74f6a8d5..00000000 --- a/src/pages/Login/components/Kakao/KakaoLoginDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface KakaoLoginDto { - status: number; - data: { - message: string; - accessToken: string; - }; -} diff --git a/src/pages/Login/components/Kakao/index.tsx b/src/pages/Login/components/Kakao/index.tsx index cf254d16..5a769018 100644 --- a/src/pages/Login/components/Kakao/index.tsx +++ b/src/pages/Login/components/Kakao/index.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { StyledText } from '../../../../components/Text/StyledText'; + import theme from '../../../../styles/theme'; -import kakao from '../../../../assets/Login/kakao.png'; import { SocialLogin, LogoImgWrapper, LogoImage, TextWrapper } from '../style'; +import { StyledText } from '../../../../components/Text/StyledText'; + +import kakao from '../../../../assets/default/snsIcon/kakao.svg'; const Kakao: React.FC = () => { - // 환경 변수에서 값을 읽어옴 const REST_API_KEY = import.meta.env.VITE_KAKAO_REST_API_KEY; const REDIRECT_URI = encodeURIComponent( import.meta.env.VITE_DEV_DOMAIN @@ -18,13 +19,13 @@ const Kakao: React.FC = () => { }; return ( - - + + - - 카카오로 시작하기 + + Kakao로 계속하기 diff --git a/src/pages/Login/components/LoginComplete.tsx b/src/pages/Login/components/LoginComplete.tsx new file mode 100644 index 00000000..b7856395 --- /dev/null +++ b/src/pages/Login/components/LoginComplete.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import Loading from '../../../components/Loading'; +import Modal from '../../../components/Modal'; + +import { getUserInfoByJwtApi } from '../../../apis/auth'; +import { handleError } from '../../../apis/util/handleError'; +import { postTermsAgreementApi } from '../../../apis/user'; + +const LoginComplete: React.FC = () => { + const location = useLocation(); + const navigate = useNavigate(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMessage, setModalMessage] = useState(''); + + useEffect(() => { + // URLSearchParams를 사용해 쿼리 문자열에서 token 추출 + const queryParams = new URLSearchParams(location.search); + const token = queryParams.get('token'); + + if (token) { + localStorage.setItem('new_jwt_token', token); + console.log('Extracted Token:', token); + + // JWT로 사용자 정보 조회하는 함수 + const getUserInfoByJwt = async () => { + try { + const response = await getUserInfoByJwtApi(); + console.log(response); + + const { nickname, name, userId } = response.data; + localStorage.setItem('my_id', `${userId}`); + + if (nickname && name) { + if (nickname && name) { + const isAgreed = await checkTermsAgreement(userId); + navigate(isAgreed ? '/' : '/terms-agreement'); + } + } else { + navigate('/signup'); + } + } catch (error) { + console.error('사용자 정보 조회 실패:', error); + const errorMessage = handleError(error, 'user'); + setModalMessage(errorMessage); + setIsModalOpen(true); + } + }; + getUserInfoByJwt(); + } + }, [location]); + + const checkTermsAgreement = async (userId: string): Promise => { + try { + await postTermsAgreementApi(userId); + return true; // 동의 완료 + } catch { + return false; // 동의 필요 + } + }; + + const handleModalClose = () => { + setIsModalOpen(false); + navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동 + }; + + return ( + <> + + {isModalOpen && } + + ); +}; + +export default LoginComplete; diff --git a/src/pages/Login/components/Naver/NaverCallback.tsx b/src/pages/Login/components/Naver/NaverCallback.tsx index 8f4fb520..d2156d5d 100644 --- a/src/pages/Login/components/Naver/NaverCallback.tsx +++ b/src/pages/Login/components/Naver/NaverCallback.tsx @@ -1,69 +1,54 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; -import request from '../../../../apis/core'; -import { UserInfoDto } from '../../../ProfileViewer/ResponseDto/UserInfoDto'; + import Loading from '../../../../components/Loading'; +import Modal from '../../../../components/Modal'; + +import { handleError } from '../../../../apis/util/handleError'; const NaverCallback: React.FC = () => { const navigate = useNavigate(); + const apiBaseUrl = import.meta.env.VITE_NEW_API_URL; - useEffect(() => { - const query = new URLSearchParams(window.location.search); - const code = query.get('code'); - console.log(code); - - if (code) { - // 인증 코드를 쿼리스트링으로 백엔드 서버에 전송 - axios - .get(`https://api-dev.oodd.today/auth/login/naver?code=${code}&state=STATE_TOKEN`) - .then((response) => { - const statusCode = response.status; // 200 OK - console.log(JSON.stringify(response.data)); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMessage, setModalMessage] = useState(''); - if (statusCode === 200) { - // 추후 Postman에서 api 호출해 보고 응답을 보고 적어야 함 - // userid를 서버로 보내 해당 유저의 nickname 유무에 따른 리디렉션 - const token = response.data.accessToken; - - localStorage.setItem('id', response.data.id); - localStorage.removeItem('jwt_token'); - localStorage.setItem('jwt_token', token); - const userid = localStorage.getItem('id'); - - request - .get(`/users/${userid}`) - .then((response) => { - console.log(response); - if (response.result.nickname) { - navigate('/'); - } else { - navigate(`/signup`); - } - }) - .catch((error) => { - // API 요청 실패 시 처리 - console.error('API 요청 실패:', error); - alert('사용자 정보를 불러오지 못했습니다.'); - navigate('/login'); // 실패 시 로그인 페이지로 리디렉션 - }); - } else { - console.error('로그인 실패:', response.data); - alert('네이버 계정의 정보를 불러오지 못했습니다.'); - navigate('/login'); - // 로그인 실패 시 처리 (예: 오류 페이지로 리디렉션) - } - }) - .catch((error) => { - console.error('서버 요청 실패:', error); - }); - } else { - // 인증 코드가 없는 경우 처리 - console.error('인증 코드가 없습니다.'); - } - }, [navigate]); - - return ; + useEffect(() => { + const handleNaverLogin = async () => { + try { + // URL에서 인증 코드 추출 + const code = new URL(window.location.href).searchParams.get('code'); + console.log('인증 코드:', code); + + if (!code) { + throw new Error('인증 코드가 없습니다.'); + } + + // 리다이렉트 URL 설정 및 서버 URL 생성 해 서버로 리다이렉션 + const redirectUrl = encodeURIComponent('http://localhost:3000/login/complete'); + const serverUrl = `${apiBaseUrl}/auth/login/naver?redirectUrl=${redirectUrl}`; + window.location.href = serverUrl; + } catch (error) { + console.error('네이버 로그인 중 오류 발생:', error); + const errorMessage = handleError(error); + setModalMessage(`네이버 ${errorMessage}`); + setIsModalOpen(true); + } + }; + + handleNaverLogin(); + }, [navigate, apiBaseUrl]); + + const handleModalClose = () => { + setIsModalOpen(false); + navigate('/login'); // 모달 닫힌 후 로그인 페이지로 이동 + }; + return ( + <> + + {isModalOpen && } + + ); }; export default NaverCallback; diff --git a/src/pages/Login/components/Naver/index.tsx b/src/pages/Login/components/Naver/index.tsx index 2e3417fa..7928c545 100644 --- a/src/pages/Login/components/Naver/index.tsx +++ b/src/pages/Login/components/Naver/index.tsx @@ -1,8 +1,10 @@ import React from 'react'; + import { StyledText } from '../../../../components/Text/StyledText'; import theme from '../../../../styles/theme'; import { SocialLogin, TextWrapper, LogoImgWrapper, LogoImage } from '../style'; -import naver from '../../../../assets/Login/naver.png'; + +import naver from '../../../../assets/default/snsIcon/naver.svg'; const Naver: React.FC = () => { const clientId = import.meta.env.VITE_NAVER_CLIENT_ID; // 네이버 개발자 센터에서 받은 클라이언트 ID @@ -11,20 +13,19 @@ const Naver: React.FC = () => { ? import.meta.env.VITE_DEV_DOMAIN + '/auth/naver/callback' : 'http://localhost:3000/auth/naver/callback', ); - //const state = 'random_state_string'; // CSRF 공격 방지를 위한 랜덤 문자열 const handleLogin = () => { window.location.href = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&state=STATE_TOKEN`; }; return ( - - + + - - 네이버로 시작하기 + + 네이버로 계속하기 diff --git a/src/pages/Login/components/style.tsx b/src/pages/Login/components/style.tsx index a21e2b3d..85611457 100644 --- a/src/pages/Login/components/style.tsx +++ b/src/pages/Login/components/style.tsx @@ -3,22 +3,22 @@ import styled from 'styled-components'; export const SocialLogin = styled.button<{ $bgColor: string; $border?: boolean }>` display: flex; align-items: center; - width: 100%; - max-width: 21.375rem; /* 342px / 16 */ - height: 3.5rem; /* 56px / 16 */ + width: calc(100% - 3.5rem); + max-width: 40rem; + height: 3.5rem; background-color: ${({ $bgColor }) => $bgColor}; - border-radius: 0.1875rem; /* 3px / 16 */ + border-radius: 0.5rem; border: ${({ $border }) => ($border ? '1px solid #000' : 'none')}; cursor: pointer; - margin-bottom: 0.75rem; + margin-bottom: 0.5rem; box-sizing: border-box; `; -export const LogoImgWrapper = styled.div<{ $logowidth: string; $logoheight: string }>` +export const LogoImgWrapper = styled.figure` display: flex; align-items: center; - width: ${({ $logowidth }) => $logowidth}; - height: ${({ $logoheight }) => $logoheight}; + width: 2.25rem; + height: 2.25rem; margin-left: 1rem; `; @@ -27,7 +27,7 @@ export const LogoImage = styled.img` max-height: 100%; `; -export const TextWrapper = styled.div<{ $left?: string }>` +export const TextWrapper = styled.section<{ $left?: string }>` display: flex; width: 12.5rem; padding-left: ${({ $left }) => $left || '1.2rem'}; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 1af714f0..d30c211c 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,36 +1,21 @@ import React from 'react'; -import { StyledText } from '../../components/Text/StyledText'; -import theme from '../../styles/theme'; + import { OODDFrame } from '../../components/Frame/Frame'; -import { LogoWrapper, LoginContainer, WelcomeWrapper, Service, LogoImg } from './styles'; import Naver from './components/Naver'; import Kakao from './components/Kakao'; -import Google from './components/Google'; -import OODDlogo from '../../../src/assets/OODDlogo.svg'; + +import { LoginContainer, StyledWelcomeWrapper } from './styles'; +import theme from '../../styles/theme'; const Login: React.FC = () => { return ( - - - - - - 반가워요! - - - 계정을 선택해주세요 - - - + + {'반가워요! \n계정을 선택해주세요.'} + - - - - 서비스 약관 확인{' >'} - - + ); diff --git a/src/pages/Login/styles.tsx b/src/pages/Login/styles.tsx index 0a6d76b5..544dc3ac 100644 --- a/src/pages/Login/styles.tsx +++ b/src/pages/Login/styles.tsx @@ -1,42 +1,21 @@ import styled from 'styled-components'; +import { StyledText } from '../../components/Text/StyledText'; -export const LoginContainer = styled.div` +export const LoginContainer = styled.main` display: flex; flex-direction: column; + justify-content: center; align-items: center; width: 100%; - max-width: 32rem; /* 최대 너비 512px */ - height: auto; + height: 100%; margin: 0 auto; /* 중앙 정렬 */ - //box-shadow: 0 0 0.625rem rgba(0, 0, 0, 0.1); /* 경계 구분용*/ `; -export const LogoWrapper = styled.div` - display: flex; - justify-content: center; /* 수평 중앙 정렬 */ - width: 100%; - max-width: 7.25rem; /* 116px / 16 */ - margin-top: 10.8rem; /* 195px */ -`; - -export const LogoImg = styled.img` - display: flex; -`; - -export const WelcomeWrapper = styled.div` +export const StyledWelcomeWrapper = styled(StyledText)` display: flex; flex-direction: column; - max-width: 11.75rem; width: 100%; - height: 4rem; /* 64px / 16 */ + height: 5rem; text-align: center; - margin: 1.5637rem 0 2.25rem 0; /* 36px / 16 */ -`; - -export const Service = styled.button` - display: flex; - border: none; - width: fit-content; /* 버튼 너비가 내용에 맞도록 설정 */ - padding: 0 1rem; /* 16px / 16 */ - margin: 0.75rem 0 16rem 0; + margin-bottom: 2rem; `; diff --git a/src/pages/Mypage/ButtonSecondary/index.tsx b/src/pages/MyPage/ButtonSecondary/index.tsx similarity index 93% rename from src/pages/Mypage/ButtonSecondary/index.tsx rename to src/pages/MyPage/ButtonSecondary/index.tsx index 7abbf4e2..8682b4b3 100644 --- a/src/pages/Mypage/ButtonSecondary/index.tsx +++ b/src/pages/MyPage/ButtonSecondary/index.tsx @@ -13,7 +13,7 @@ const ButtonSecondary: React.FC = () => { return ( diff --git a/src/pages/Mypage/ButtonSecondary/styles.tsx b/src/pages/MyPage/ButtonSecondary/styles.tsx similarity index 60% rename from src/pages/Mypage/ButtonSecondary/styles.tsx rename to src/pages/MyPage/ButtonSecondary/styles.tsx index 68334d5f..d4401860 100644 --- a/src/pages/Mypage/ButtonSecondary/styles.tsx +++ b/src/pages/MyPage/ButtonSecondary/styles.tsx @@ -1,14 +1,16 @@ import styled from 'styled-components'; export const Button = styled.button` - width: calc(100% - 30px); /* 양옆에 30px씩 공간을 확보 */ + width: 100%; padding: 6px; margin: 1.25rem auto; - - border: 1px solid #000; - border-radius: 10px; height: 3.1rem; /* 44px */ text-align: center; + color: #ff2389; cursor: pointer; box-sizing: border-box; + border: 1px solid; + border-radius: 10px; + border-color: #ff2389; + padding: 10px; /* 텍스트가 보더와 겹치지 않게 패딩 설정 */ `; diff --git a/src/pages/Mypage/dto.tsx b/src/pages/MyPage/dto.tsx similarity index 96% rename from src/pages/Mypage/dto.tsx rename to src/pages/MyPage/dto.tsx index 23a1c3c8..30e058b1 100644 --- a/src/pages/Mypage/dto.tsx +++ b/src/pages/MyPage/dto.tsx @@ -5,7 +5,7 @@ export interface PostData { comments: number; } -// src/pages/Mypage/dto.ts +// src/pages/MyPage/dto.ts export interface UserResponse { id: number; diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx new file mode 100644 index 00000000..b296fd79 --- /dev/null +++ b/src/pages/MyPage/index.tsx @@ -0,0 +1,166 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + ProfileContainer, + Header, + StatsContainer, + Stat, + StatNumber, + StatLabel, + PostsContainer, + AddButton, +} from './styles'; +import { OODDFrame } from '../../components/Frame/Frame'; +import NavbarProfile from '../../components/NavbarProfile'; +import NavBar from '../../components/NavBar'; +import ButtonSecondary from './ButtonSecondary'; +import PostItem from '../../components/PostItem'; +import imageBasic from '../../assets/default/defaultProfile.svg'; +import Loading from '../../components/Loading'; +import BottomSheet from '../../components/BottomSheet'; +import { BottomSheetProps } from '../../components/BottomSheet/dto'; +import BottomSheetMenu from '../../components/BottomSheetMenu'; +import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto'; +import button_plus from '../../assets/default/plus.svg'; +import insta from '../../assets/default/insta.svg'; +import photo from '../../assets/default/photo.svg'; +import UserProfile from '../../components/UserProfile'; + +import { getUserPostListApi } from '../../apis/post'; +import { UserPostSummary } from '../../apis/post/dto'; + +const MyPage: React.FC = () => { + const [isLoading, setIsLoading] = useState(true); + const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false); + const [posts, setPosts] = useState([]); + const [totalStats, setTotalStats] = useState<{ + totalPostsCount: number; + totalPostCommentsCount: number; + totalPostLikesCount: number; + }>(); + const navigate = useNavigate(); + + const bottomSheetMenuProps: BottomSheetMenuProps = { + items: [ + { + text: '인스타 피드 가져오기', + action: () => { + setIsBottomSheetOpen(false); + navigate('/insta-connect'); + }, + icon: insta, + }, + { + text: '사진 올리기', + action: () => { + setIsBottomSheetOpen(false); + navigate('/image-select'); + }, + icon: photo, + }, + ], + marginBottom: '50px', + }; + + const bottomSheetProps: BottomSheetProps = { + isOpenBottomSheet: isBottomSheetOpen, + isHandlerVisible: true, + Component: BottomSheetMenu, + componentProps: bottomSheetMenuProps, + onCloseBottomSheet: () => { + setIsBottomSheetOpen(false); + }, + }; + + const handleOpenSheet = () => { + setIsBottomSheetOpen(true); + }; + + //게시물 리스트 조회 api - 콘솔 삭제 예정! + const fetchPostList = async () => { + try { + const storedUserId = localStorage.getItem('my_id'); // my_id로 변경되었음 + if (!storedUserId) { + console.error('User ID not found in localStorage'); + return; + } + + console.log('Fetching posts for user ID:', storedUserId); // 디버깅: User ID 확인 + + // API 호출 + const response = await getUserPostListApi(1, 10, Number(storedUserId)); + console.log('API Response:', response); // 디버깅: API 응답 확인 + + const { post, totalPostsCount, totalPostCommentsCount, totalPostLikesCount, meta } = response.data; + + console.log('Post List:', post); // 디버깅: 게시물 리스트 확인 + console.log('Pagination Meta:', meta); // 디버깅: 페이지네이션 정보 확인 + + // 상태 업데이트 + setPosts(post); + setTotalStats({ totalPostsCount, totalPostCommentsCount: totalPostCommentsCount ?? 0, totalPostLikesCount }); + + if (totalPostsCount === 0) { + console.log('No posts available for the user.'); + } + } catch (error) { + console.error('Error fetching post list:', error); // 디버깅: 에러 확인 + } finally { + setIsLoading(false); + console.log('Loading completed.'); // 디버깅: 로딩 완료 확인 + } + }; + useEffect(() => { + fetchPostList(); + }, []); + + if (isLoading) { + return ; + } + + return ( + + + + Add + + + +
+ +
+ + + + OOTD + {totalStats?.totalPostsCount} + + + 코멘트 + {totalStats?.totalPostCommentsCount} + + + 좋아요 + {totalStats?.totalPostLikesCount} + + + + {posts.length > 0 ? ( + posts + .sort((a, b) => { + if (b.isRepresentative && !a.isRepresentative) return 1; + if (a.isRepresentative && !b.isRepresentative) return -1; + return 0; + }) + .map((post) => ) + ) : ( +

게시물이 없습니다.

+ )} +
+ +
+
+ ); +}; + +export default MyPage; diff --git a/src/pages/MyPage/styles.tsx b/src/pages/MyPage/styles.tsx new file mode 100644 index 00000000..2c4a8eb6 --- /dev/null +++ b/src/pages/MyPage/styles.tsx @@ -0,0 +1,76 @@ +import styled from 'styled-components'; + +export const ProfileContainer = styled.div` + width: 100%; + flex-grow: 1; + margin: 0 auto; /* 중앙 정렬 */ + display: flex; + flex-direction: column; + align-self: center; + box-sizing: border-box; /* 패딩을 포함한 전체 크기를 설정 */ + overflow-y: auto; /* 내용이 넘칠 경우 스크롤 */ + padding-top: 0rem; +`; + +export const Header = styled.div` + margin-top: 0; + display: flex; + align-items: center; + padding: 0rem; + margin-left: 20px; +`; + +export const StatsContainer = styled.div` + display: flex; + justify-content: space-around; + padding: 0.625rem 0; /* 10px 0 */ + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; +`; + +export const Stat = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +export const StatNumber = styled.div` + color: var(--Color-gray4, #8e8e8e); + text-align: center; + + font-family: 'Pretendard'; + font-size: 1rem; /* 16px */ + font-style: normal; + font-weight: 400; + line-height: normal; +`; + +export const StatLabel = styled.div` + color: var(--Color-gray4, #8e8e8e); + text-align: center; + font-family: 'Pretendard'; + font-size: 0.75rem; /* 12px */ + font-style: normal; + font-weight: 300; +`; + +export const PostsContainer = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: space-between; /* 두 개씩 나란히 배치 */ + gap: 15px; + cursor: pointer; + margin-bottom: 100px; + padding: 20px; +`; + +export const AddButton = styled.button` + position: absolute; + bottom: 6.75rem; + right: 1.25rem; + width: 5rem; + height: 5rem; + border: none; + border-radius: 50%; + z-index: 2; +`; diff --git a/src/pages/MyPost/assets/DeleteIcon.png b/src/pages/MyPost/assets/DeleteIcon.png deleted file mode 100644 index 298d5d66..00000000 Binary files a/src/pages/MyPost/assets/DeleteIcon.png and /dev/null differ diff --git a/src/pages/MyPost/assets/EditIcon.svg b/src/pages/MyPost/assets/EditIcon.svg deleted file mode 100644 index 90ae81b5..00000000 --- a/src/pages/MyPost/assets/EditIcon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/pages/MyPost/assets/PinIcon.svg b/src/pages/MyPost/assets/PinIcon.svg deleted file mode 100644 index b6939703..00000000 --- a/src/pages/MyPost/assets/PinIcon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/MyPost/assets/commentIcon.svg b/src/pages/MyPost/assets/commentIcon.svg deleted file mode 100644 index 91cea06d..00000000 --- a/src/pages/MyPost/assets/commentIcon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/MyPost/assets/heartIcon.svg b/src/pages/MyPost/assets/heartIcon.svg deleted file mode 100644 index cf0fc3a9..00000000 --- a/src/pages/MyPost/assets/heartIcon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/MyPost/assets/mockImage.png b/src/pages/MyPost/assets/mockImage.png deleted file mode 100644 index 063d18c1..00000000 Binary files a/src/pages/MyPost/assets/mockImage.png and /dev/null differ diff --git a/src/pages/MyPost/assets/nextIcon.svg b/src/pages/MyPost/assets/nextIcon.svg deleted file mode 100644 index 05c1cff9..00000000 --- a/src/pages/MyPost/assets/nextIcon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/MyPost/dto.tsx b/src/pages/MyPost/dto.tsx deleted file mode 100644 index dcaa4532..00000000 --- a/src/pages/MyPost/dto.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// src/types.ts - -export interface BaseResponse { - isSuccess: boolean; - code: number; - message: string; - result?: T; -} - -export interface PostDetailResponse { - isSuccess: boolean; - code: number; - message: string; - result: { - postId: number; - userId: number; - likes: number | null; - comments: { commentId: number; userId: number; text: string; timestamp: string }[] | null; - photoUrls: string[]; - content: string; - styletags: string[]; - clothingInfo: { imageUrl: string; brand: string; model: string; modelNumber: string; url: string }[] | null; - }; -} - -export interface LikesResponse { - isSuccess: boolean; - code: number; - message: string; - result: { - totalLikes: number; - likes: Array<{ - id: number; - userId: number; - postId: number; - status: string; - createdAt: string; - updatedAt: string; - user: { - id: number; - nickname: string; - profilePictureUrl: string; - }; - }>; - }; -} - -export interface CommentsResponse { - isSuccess: boolean; - code: number; - message: string; - result: { - comments: Array<{ - id: number; - postId: number; - content: string; - status: string; - createdAt: string; - updatedAt: string; - deletedAt: string | null; - user: { - id: number; - nickname: string; - profilePictureUrl: string; - }; - }>; - }; -} -export interface User { - id: number; - name: string; - profilePictureUrl: string; - nickname: string; -} - -export interface UserResponse { - id: number; - name: string; - email: string; - nickname: string | null; - phoneNumber: string | null; - profilePictureUrl: string; - bio: string | null; - joinedAt: string; - isSuccess: boolean; - result: User; - message: string; -} diff --git a/src/pages/MyPost/index.tsx b/src/pages/MyPost/index.tsx index a90928c8..ef765396 100644 --- a/src/pages/MyPost/index.tsx +++ b/src/pages/MyPost/index.tsx @@ -1,390 +1,149 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { - PostDetailContainer, - UserID, - Pic_exam, - UserInfoContainer, - UserRow, - Text, - Menu, - ImageWrapper, - Image, - IconRow, - IconWrapper, - ClothingInfoContainer, - Tab, - ContentContainer, - UserItem, - CircleIcon, - ModalContainer, - TabContainer, - Arrow, - Indicator, -} from './styles'; -import TopBar from '../../components/TopBar'; -import { OODDFrame } from '../../components/Frame/Frame'; -import ConfirmationModal from '../../components/ConfirmationModal'; +import { useRecoilState } from 'recoil'; +import { isPostRepresentativeAtom } from '../../recoil/Post/PostAtom'; + +import PostBase from '../../components/PostBase'; +import Modal from '../../components/Modal'; +import { ModalProps } from '../../components/Modal/dto'; import BottomSheet from '../../components/BottomSheet'; import { BottomSheetProps } from '../../components/BottomSheet/dto'; import BottomSheetMenu from '../../components/BottomSheetMenu'; import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto'; -import ClothingInfoCard from '../Post/ClothingInfoCard'; -import imageBasic from '../../assets/imageBasic.svg'; -import back from '../../assets/back.svg'; -import nextIcon from '../../assets/Upload/next.svg'; -import DeleteIcon from './assets/DeleteIcon.png'; -import EditIcon from './assets/EditIcon.svg'; -import PinIcon from './assets/PinIcon.svg'; -import mockImage from './assets/mockImage.png'; -import heartIcon from './assets/heartIcon.svg'; -import commentIcon from './assets/commentIcon.svg'; +import Edit from '../../assets/default/edit.svg'; +import Pin from '../../assets/default/pin.svg'; +import Delete from '../../assets/default/delete.svg'; -import request from '../../apis/core'; -import { UserResponse } from './dto'; -import { BaseResponse, PostDetailResponse, LikesResponse, CommentsResponse } from './dto'; -import Loading from '../../components/Loading'; +import { modifyPostRepresentativeStatusApi, deletePostApi } from '../../apis/post'; const MyPost: React.FC = () => { const { postId } = useParams<{ postId: string }>(); - const [postDetail, setPostDetail] = useState(null); - const [currentImageIndex, setCurrentImageIndex] = useState(0); - const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false); - const [activeTab, setActiveTab] = useState<'likes' | 'comments' | 'menu'>('menu'); - const [likes, setLikes] = useState([]); - const [comments, setComments] = useState([]); - const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); + const [isPostRepresentative, setIsPostRepresentative] = useRecoilState(isPostRepresentativeAtom); + const [postPinStatus, setPostPinStatus] = useState<'지정' | '해제'>('지정'); + const [isMenuBottomSheetOpen, setIsMenuBottomSheetOpen] = useState(false); + const [isDeleteConfirmationModalOpen, setIsDeleteConfirmationModalOpen] = useState(false); + const [isApiResponseModalOpen, setIsApiResponseModalOpen] = useState(false); + const [pinPostResultlModalContent, setPinPostResultlModalContent] = useState(''); const navigate = useNavigate(); - const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); // 로딩 상태 추가 - - // 좋아요 리스트 불러오기 - const fetchLikes = async () => { - try { - const response = await request.get(`/posts/${postId}/like`); - if (response.isSuccess) { - setLikes(response.result.likes); - } else { - console.error('Failed to fetch likes:', response.message); - } - } catch (error) { - console.error('Error fetching likes:', error); - } - }; - - // 코멘트 리스트 불러오기 - const fetchComments = async () => { - try { - const response = await request.get(`/posts/${postId}/comments`); - if (response.isSuccess) { - setComments(response.result.comments); - } else { - console.error('Failed to fetch comments:', response.message); - } - } catch (error) { - console.error('Error fetching comments:', error); - } - }; - - // 유저 정보 가져오기 - const fetchUserData = async () => { - try { - const storedUserId = localStorage.getItem('id'); // Ensure correct key is used - if (!storedUserId) { - console.error('User is not logged in'); - return; - } - - const response = await request.get>(`/users/${storedUserId}`); - setUser(response.result as UserResponse); - } catch (error) { - console.error('Error fetching user data:', error); - } finally { - setIsLoading(false); // 로딩 완료 후 로딩 상태 false로 설정 - } - }; useEffect(() => { - fetchPostDetail(); - fetchUserData(); // Fetch user data - }, [postId]); + if (isPostRepresentative) { + setPostPinStatus('해제'); + } else { + setPostPinStatus('지정'); + } + }, [isPostRepresentative]); const bottomSheetMenuProps: BottomSheetMenuProps = { items: [ { - text: '대표 OOTD로 지정하기', + text: `대표 OOTD ${postPinStatus}하기`, action: () => { - setIsBottomSheetOpen(false); - handlePinPost(); + setIsMenuBottomSheetOpen(false); + modifyPostRepresentativeStatus(); }, - icon: PinIcon, + icon: Pin, }, { text: 'OODD 수정하기', action: () => { - setIsBottomSheetOpen(false); - handleEditPost(); + setIsMenuBottomSheetOpen(false); + handlePostEdit(); }, - icon: EditIcon, + icon: Edit, }, { text: 'OOTD 삭제하기', action: () => { - setIsBottomSheetOpen(false); - handleDeletePost(); + setIsMenuBottomSheetOpen(false); + setIsDeleteConfirmationModalOpen(true); }, - icon: DeleteIcon, + icon: Delete, }, ], marginBottom: '50px', }; - const bottomSheetProps: BottomSheetProps = { - isOpenBottomSheet: isBottomSheetOpen, - isHandlerVisible: true, - // TODO: 컴포넌트 분리에 따라 BottomSheetProps 제너릭 타입 추후 수정 - Component: () => { - if (activeTab === 'menu') { - return ; - } else { - return ( - - - setActiveTab('likes')}> - 좋아요 {likes.length} - - setActiveTab('comments')}> - 코멘트 {comments.length} - - - - {activeTab === 'likes' && ( - <> - {likes.map((like) => ( - - - user avatar - - {like.user.nickname} - - ))} - - )} - {activeTab === 'comments' && ( - <> - {comments.map((comment) => ( - - - user avatar - -
- {comment.user.nickname} -
{comment.content}
-
-
- ))} - - )} -
-
- ); - } - }, - onCloseBottomSheet: () => { - setIsBottomSheetOpen(false); - }, - }; - - const handleOpenSheet = async (tab: 'likes' | 'comments' | 'menu') => { - setActiveTab(tab); - if (tab === 'likes') await fetchLikes(); - if (tab === 'comments') await fetchComments(); - setIsBottomSheetOpen(true); - }; - - const fetchPostDetail = async () => { - try { - const response = await request.get(`/posts/${postId}`); - if (response.isSuccess) { - setPostDetail(response.result); - } else { - console.error('Unexpected response:', response.message); - } - } catch (error) { - console.error('Error fetching post details:', error); - } finally { - setIsConfirmationModalOpen(false); // 확인 모달을 닫음 - } - }; - - const handleNextImage = () => { - if (postDetail && currentImageIndex < postDetail.photoUrls.length - 1) { - setCurrentImageIndex(currentImageIndex + 1); - } + const menuBottomSheetProps: BottomSheetProps = { + isOpenBottomSheet: isMenuBottomSheetOpen, + Component: BottomSheetMenu, + componentProps: bottomSheetMenuProps, + onCloseBottomSheet: () => setIsMenuBottomSheetOpen(false), }; - const handlePrevImage = () => { - if (currentImageIndex > 0) { - setCurrentImageIndex(currentImageIndex - 1); - } + const handleMenuOpen = () => { + setIsMenuBottomSheetOpen(true); }; - const handleEditPost = () => { + const handlePostEdit = () => { navigate('/upload', { state: { mode: 'edit', postId: postId } }); }; - const handlePinPost = async () => { - // localStorage에서 storedUserId를 가져옴 - const storedUserId = localStorage.getItem('id'); - - if (!storedUserId) { - console.error('User ID not found'); - return; - } - + const modifyPostRepresentativeStatus = async () => { try { - const response = await request.patch(`/posts/${postId}/isRepresentative/${storedUserId}`, { - isRepresentative: true, - }); + const response = await modifyPostRepresentativeStatusApi(Number(postId)); if (response.isSuccess) { - console.log('Post pinned successfully:', response.result); - // PostDetail 재로드 - fetchPostDetail(); - navigate('/mypage'); + setPinPostResultlModalContent(`대표 OOTD ${postPinStatus}에 성공했어요`); + setIsPostRepresentative((prev) => !prev); } else { - console.error('Failed to pin post:', response.message); + setPinPostResultlModalContent(`대표 OOTD ${postPinStatus}에 실패했어요\n잠시 뒤 다시 시도해 보세요`); } } catch (error) { console.error('Error pinning post:', error); } finally { - setIsConfirmationModalOpen(false); // 확인 모달을 닫음 + setIsApiResponseModalOpen(true); } }; - const handleDeletePost = () => { - setIsConfirmationModalOpen(true); - }; - - const handleConfirmDelete = async () => { + const deletePost = async () => { try { - const response = await request.delete(`/posts/${postId}`); - if (response.message === 'Post deleted successfully') { - console.log(response.message); - navigate('/mypage'); // 성공적으로 삭제 후 다른 페이지로 이동 + const response = await deletePostApi(Number(postId)); + + if (response.isSuccess) { + setPinPostResultlModalContent('OOTD 삭제에 성공했어요'); + // 1초 뒤에 mypage로 이동 + setTimeout(() => { + navigate('/mypage'); + }, 1000); } else { - console.error('Unexpected response:', response.message); + setPinPostResultlModalContent(`OOTD 삭제에 실패했어요\n잠시 뒤 다시 시도해 보세요`); } } catch (error) { console.error('Error deleting post:', error); } finally { - setIsConfirmationModalOpen(false); // 확인 모달을 닫음 + setIsApiResponseModalOpen(true); + setIsDeleteConfirmationModalOpen(false); // 확인 모달을 닫음 } }; - const handleCancelDelete = () => { - setIsConfirmationModalOpen(false); + const deleteConfirmationModalProps: ModalProps = { + isCloseButtonVisible: true, + onClose: () => setIsDeleteConfirmationModalOpen(false), + content: '해당 OOTD를 삭제하시겠습니까?', + button: { + content: '삭제하기', + onClick: deletePost, + }, }; - useEffect(() => { - fetchPostDetail(); - }, [postId]); - if (isLoading) { - return ; // 로딩 중일 때 Loading 컴포넌트 표시 - } - return ( - - - navigate(-1)} /> + const apiResponseModalProps: ModalProps = { + onClose: () => setIsApiResponseModalOpen(false), + content: pinPostResultlModalContent, + }; - - - - User Profile - - {user?.nickname || 'Unknown User'} - - {postDetail?.content || 'Loading...'} - - handleOpenSheet('menu')}> - - - - + return ( + <> + - - {postDetail?.photoUrls && postDetail.photoUrls.length > 1 && ( - <> - - Previous - - - Next - - - {currentImageIndex + 1} / {postDetail.photoUrls.length} - - - )} - Post - + - {isBottomSheetOpen && } - {isConfirmationModalOpen && ( - - )} - - handleOpenSheet('likes')}> - Heart Icon - {postDetail?.likes || 0} {/* 좋아요 수 */} - - handleOpenSheet('comments')}> - Comment Icon - {postDetail?.comments?.length || 0} {/* 댓글 수 */} - - - - {postDetail?.clothingInfo?.map((clothingInfo, index: number) => ( - - ))} - - - + {isDeleteConfirmationModalOpen && } + {isApiResponseModalOpen && } + ); }; diff --git a/src/pages/MyPost/styles.tsx b/src/pages/MyPost/styles.tsx index 8b141576..e130d565 100644 --- a/src/pages/MyPost/styles.tsx +++ b/src/pages/MyPost/styles.tsx @@ -1,187 +1,4 @@ import styled from 'styled-components'; -import theme from '../../styles/theme'; - -export const PostDetailContainer = styled.div` - max-width: 512px; /* 32rem */ - display: flex; - flex-direction: column; - position: relative; -`; - -export const UserInfoContainer = styled.div` - display: flex; - flex-direction: column; - align-items: flex-start; /* 왼쪽 정렬 */ - margin-left: 5px; - object-fit: cover; - padding: 10px 20px; /* 1.25rem */ -`; - -export const UserRow = styled.div` - display: flex; - align-items: center; /* Pic_exam과 UserID를 수평으로 정렬 */ -`; - -export const UserID = styled.div` - /* Body1/Medium */ - font-family: 'Gmarket Sans'; - font-size: 16px; /* 1rem */ - color: #000; - font-style: normal; - font-weight: 400; - line-height: normal; - margin-left: 8px; /* 0.5rem */ -`; - -export const Pic_exam = styled.div` - width: 36px; /* 2.25rem */ - height: 36px; /* 2.25rem */ - flex-shrink: 0; - display: flex; - padding: 0; - margin-left: 0; - - img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 50%; - } -`; - -export const Text = styled.div` - color: var(--Color-black50, rgba(0, 0, 0, 0.5)); - font-family: 'Pretendard Variable'; - font-size: 12px; /* 0.75rem */ - font-style: normal; - font-weight: 300; - line-height: 1.2; /* 120% */ - margin-top: 4px; /* 0.25rem */ -`; - -export const Menu = styled.div` - position: absolute; - top: 70px; /* 4.375rem */ - right: 20px; /* 1.25rem */ - cursor: pointer; -`; - -export const ImageWrapper = styled.div` - position: relative; - margin-top: 0px; /* 1.25rem */ - width: 100%; - display: flex; - justify-content: center; -`; - -export const Image = styled.img` - width: 100%; - aspect-ratio: 3 / 4; - height: auto; - object-fit: cover; -`; - -export const IconRow = styled.div` - display: flex; - align-items: center; - padding: 10px 20px; /* 1.25rem */ -`; - -export const IconWrapper = styled.div` - display: flex; - align-items: center; - cursor: pointer; - - img { - width: 24px; /* 아이콘 크기 */ - height: 24px; - margin-right: 5px; - margin-top: 5px; - } - - span { - font-size: 14px; - color: #000; /* 텍스트 색상 */ - margin-right: 14px; - } -`; - -export const ClothingInfoContainer = styled.div` - display: flex; - overflow-x: auto; /* 가로 스크롤 가능하도록 설정 */ - white-space: nowrap; /* 줄바꿈 없이 한 줄로 나열 */ - padding: 0.625rem 0; - //margin-top: 16px; /* 상단과의 간격 */ - padding: 0px 20px; /* 1.25rem */ - - &::-webkit-scrollbar { - height: 0rem; - } -`; - -/* -export const BrandBox = styled.div` - display: inline-flex; // inline-flex를 사용하여 가로 배치 유지 - align-items: center; - padding: 0.625rem; - border: 0.0625rem solid #7b7b7b; - margin-right: 0.625rem; - width: 15.3243rem; // 지정된 너비 - height: 4.5rem; // 지정된 높이 - flex-shrink: 0; // 크기 고정 - - img { - width: 3.125rem; - height: 3.125rem; - margin-right: 0.625rem; - } - - div { - display: flex; - flex-direction: column; - justify-content: center; - flex-grow: 1; /./ 텍스트 영역 확장 - } - - &:last-child { - margin-right: 0; - } - - .next-icon { - width: 1.875rem; // 아이콘 크기 - height: 19px; - margin-left: auto; // 자동으로 오른쪽 끝으로 배치 - } -`; - -export const BrandLink = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; -`; -*/ - -export const TabContainer = styled.div` - display: flex; - justify-content: space-around; - border-bottom: 1px solid ${theme.colors.gray3}; -`; - -export const TabButton = styled.button<{ active: boolean }>` - flex: 1; - padding: 10px; - background-color: ${({ active }) => (active ? theme.colors.white : theme.colors.gray1)}; - color: ${({ active }) => (active ? theme.colors.black : theme.colors.gray4)}; - border: none; - cursor: pointer; - font-weight: ${({ active }) => (active ? 'bold' : 'normal')}; -`; - -export const TabContent = styled.div` - padding: 20px; - background-color: ${theme.colors.white}; -`; export const ModalContainer = styled.div` background: white; @@ -191,64 +8,3 @@ export const ModalContainer = styled.div` height: 377px; flex-shrink: 0; `; - -export const Tab = styled.div<{ active: boolean }>` - flex: 1; - text-align: center; - padding: 16px 0; - cursor: pointer; - font-weight: ${(props) => (props.active ? 'bold' : 'normal')}; - border-bottom: ${(props) => (props.active ? '2px solid black' : 'none')}; - color: var(--Color-black, #000); - text-align: center; - font-family: 'Pretendard Variable'; - font-size: 16px; - font-style: normal; - font-weight: 500; - line-height: 150%; /* 24px */ -`; - -export const ContentContainer = styled.div` - padding: 16px; -`; - -export const UserItem = styled.div` - display: flex; - align-items: center; - padding: 8px 0; - border-bottom: 1px solid #eee; -`; - -export const CircleIcon = styled.div` - width: 24px; - height: 24px; - border-radius: 50%; - background-color: red; /* 동그라미 색상 */ - display: flex; - align-items: center; - justify-content: center; - color: white; - font-weight: bold; - margin-right: 8px; -`; - -export const Arrow = styled.div<{ direction: string; disabled: boolean }>` - position: absolute; - top: 50%; - ${({ direction }) => (direction === 'left' ? 'left: 10px;' : 'right: 10px;')} - transform: translateY(-50%); - cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; - opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; -`; - -export const Indicator = styled.div` - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - background-color: rgba(0, 0, 0, 0.5); - color: #fff; - padding: 5px 10px; - border-radius: 15px; - font-size: 14px; -`; diff --git a/src/pages/Mypage/Post/Heart.svg b/src/pages/Mypage/Post/Heart.svg deleted file mode 100644 index c9a9a0a7..00000000 --- a/src/pages/Mypage/Post/Heart.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/Mypage/Post/comment.svg b/src/pages/Mypage/Post/comment.svg deleted file mode 100644 index b321a5a9..00000000 --- a/src/pages/Mypage/Post/comment.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/Mypage/Post/index.tsx b/src/pages/Mypage/Post/index.tsx deleted file mode 100644 index 5257190d..00000000 --- a/src/pages/Mypage/Post/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { PostContainer, ImageWrapper, Image, IconContainer, Icon, PinIcon } from './styles'; -import Heart from './Heart.svg'; -import Comment from './comment.svg'; -import Pin from './pin.svg'; - -interface PostProps { - imgUrl: string; - likes: number; - comments: number; - onClick: () => void; - isFirst?: boolean; // 첫 번째 포스트인지 여부를 나타내는 새로운 prop -} - -const Post: React.FC = ({ imgUrl, likes, comments, onClick, isFirst }) => { - const navigate = useNavigate(); - - const handleIconClick = (event: React.MouseEvent, type: 'likes' | 'comments') => { - event.stopPropagation(); - navigate(`/post/1/${type}`); - }; - - return ( - - - {isFirst && } - Post - - handleIconClick(event, 'likes')}> - Like - {likes} - - handleIconClick(event, 'comments')}> - Comment - {comments} - - - - - ); -}; - -export default Post; diff --git a/src/pages/Mypage/Post/pin.svg b/src/pages/Mypage/Post/pin.svg deleted file mode 100644 index 61e93c64..00000000 --- a/src/pages/Mypage/Post/pin.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/Mypage/Post/styles.tsx b/src/pages/Mypage/Post/styles.tsx deleted file mode 100644 index 6e94ffd3..00000000 --- a/src/pages/Mypage/Post/styles.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import styled from 'styled-components'; - -export const PostContainer = styled.div` - width: 50%; /* 포스트 크기를 약간 키움 */ - max-width: 32rem; /* 512px */ - height: auto; /* 비율 유지를 위해 auto로 설정 */ - flex-shrink: 0; /* 포스트가 줄어들지 않도록 설정 */ - margin: 0; /* 간격을 없앰 */ - display: flex; - flex-direction: column; - align-items: center; - position: relative; -`; - -export const ImageWrapper = styled.div` - width: 100%; - padding-top: 150%; /* 3:2 비율을 유지 */ - position: relative; -`; - -export const Image = styled.img` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; -`; - -export const IconContainer = styled.div` - position: absolute; - bottom: 0.625rem; /* 10px */ - right: 0.625rem; /* 10px */ - display: flex; - align-items: center; -`; - -export const Icon = styled.div` - display: flex; - align-items: center; - margin-left: 1.25rem; /* 20px */ - - svg { - margin-right: 0.3125rem; /* 5px */ - } - - span { - color: white; - font-size: 0.875rem; /* 14px */ - } -`; - -export const PinIcon = styled.img` - position: absolute; - top: 0.625rem; /* 10px */ - left: 0.625rem; /* 10px */ - width: 1.25rem; /* 20px */ - height: 1.25rem; /* 20px */ - z-index: 1; /* 이미지 위에 표시되도록 z-index 추가 */ -`; diff --git a/src/pages/Mypage/assets/PinIcon.svg b/src/pages/Mypage/assets/PinIcon.svg deleted file mode 100644 index b6939703..00000000 --- a/src/pages/Mypage/assets/PinIcon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/pages/Mypage/index.tsx b/src/pages/Mypage/index.tsx deleted file mode 100644 index a79f3116..00000000 --- a/src/pages/Mypage/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - ProfileContainer, - Header, - AvatarWrapper, - Avatar, - UserInfo, - Username, - Bio, - StatsContainer, - Stat, - StatNumber, - StatLabel, - PostsContainer, - AddButton, -} from './styles'; -import { OODDFrame } from '../../components/Frame/Frame'; -import NavbarProfile from '../../components/NavbarProfile'; -import NavBar from '../../components/NavBar'; -import ButtonSecondary from './ButtonSecondary'; -import Post from './Post'; -import request, { BaseResponse } from '../../apis/core'; -import { PostItem, PostsResponse, UserResponse } from './dto'; -import imageBasic from '../../assets/imageBasic.svg'; -import Loading from '../../components/Loading'; -import BottomSheet from '../../components/BottomSheet'; -import { BottomSheetProps } from '../../components/BottomSheet/dto'; -import BottomSheetMenu from '../../components/BottomSheetMenu'; -import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto'; -import button_plus from '../../assets/Profile/button_plus.svg'; -import Insta from '../../assets/BottomSheetMenu/Insta.svg'; -import Picture from '../../assets/BottomSheetMenu/Picture.svg'; - -const MyPage: React.FC = () => { - const [user, setUser] = useState(null); - const [posts, setPosts] = useState([]); - const [totalPosts, setTotalPosts] = useState(0); - const [totalLikes, setTotalLikes] = useState(0); - const [totalComments, setTotalComments] = useState(0); // Comments count - const [isLoading, setIsLoading] = useState(true); // 로딩 상태 추가 - const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false); - const navigate = useNavigate(); - - const bottomSheetMenuProps: BottomSheetMenuProps = { - items: [ - { - text: '인스타 피드 가져오기', - action: () => { - setIsBottomSheetOpen(false); - handleInstagramSelect(); - }, - icon: Insta, - }, - { - text: '사진 올리기', - action: () => { - setIsBottomSheetOpen(false); - handlePhotoUploadSelect(); - }, - icon: Picture, - }, - ], - marginBottom: '50px', - }; - - const bottomSheetProps: BottomSheetProps = { - isOpenBottomSheet: isBottomSheetOpen, - isHandlerVisible: true, - Component: BottomSheetMenu, - componentProps: bottomSheetMenuProps, - onCloseBottomSheet: () => { - setIsBottomSheetOpen(false); - }, - }; - - const handleOpenSheet = () => { - setIsBottomSheetOpen(true); - }; - - const handleInstagramSelect = () => { - navigate('/upload', { state: { mode: 'instagram' } }); - }; - - const handlePhotoUploadSelect = () => { - navigate('/upload', { state: { mode: 'image' } }); - }; - - const handlePostClick = (postId: string) => { - navigate(`/my-post/${postId}`); - }; - - // 사용자 정보 가져오기 함수 - const fetchUserData = async () => { - try { - const storedUserId = localStorage.getItem('id'); - - if (!storedUserId) { - console.error('User is not logged in'); - return; - } - - const response = await request.get>(`/users/${storedUserId}`); - setUser(response.result); - } catch (error) { - console.error('Error fetching user data:', error); - } - }; - - // API에서 포스트 리스트를 가져오는 함수 - const handlePostList = async () => { - try { - const storedUserId = localStorage.getItem('id'); - if (!storedUserId) { - console.error('User is not logged in'); - return; - } - - const response = await request.get(`/posts?userId=${storedUserId}`); - if (response.isSuccess) { - const { totalPosts, totalLikes, posts } = response.result; - setTotalPosts(totalPosts); - setTotalLikes(totalLikes); - setPosts(posts); - const totalComments = posts.reduce((sum, post) => sum + (post.commentsCount || 0), 0); - setTotalComments(totalComments); - } else { - console.error('Unexpected response:', response.message); - } - } catch (error) { - console.error('Error fetching posts:', error); - } finally { - setIsLoading(false); // 로딩 완료 후 로딩 상태 false로 설정 - } - }; - - useEffect(() => { - fetchUserData(); - handlePostList(); - }, []); - - if (isLoading) { - return ; // 로딩 중일 때 Loading 컴포넌트 표시 - } - - return ( - - - - - - - -
- - - - - {user?.nickname || '김아무개...'} - {user?.bio || '소개글이 없습니다.'} - -
- - - - OOTD - {totalPosts} - - - 코멘트 - {totalComments} - - - 좋아요 - {totalLikes} - - - - {posts - .sort((a, b) => { - if (b.isRepresentative && !a.isRepresentative) return 1; - if (a.isRepresentative && !b.isRepresentative) return -1; - return 0; - }) - .map((post) => ( - handlePostClick(post.postId.toString())} - isFirst={post.isRepresentative} - /> - ))} - - -
-
- ); -}; - -export default MyPage; diff --git a/src/pages/Mypage/styles.tsx b/src/pages/Mypage/styles.tsx deleted file mode 100644 index e1dd08a8..00000000 --- a/src/pages/Mypage/styles.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import styled from 'styled-components'; - -export const ProfileContainer = styled.div` - width: 100%; - max-width: 32rem; - flex-grow: 1; - margin: 0 auto; /* 중앙 정렬 */ - display: flex; - flex-direction: column; - align-self: center; - box-sizing: border-box; /* 패딩을 포함한 전체 크기를 설정 */ - overflow-y: auto; /* 내용이 넘칠 경우 스크롤 */ - padding-top: 0rem; -`; - -export const Header = styled.div` - margin-top: 0; - display: flex; - align-items: center; - padding: 0rem; - margin-left: -0.9375rem; -`; - -export const AvatarWrapper = styled.div` - width: 4.5rem; /* 72px */ - height: 4.5rem; /* 72px */ - border-radius: 50%; - overflow: hidden; - margin-right: 1.25rem; /* 20px */ - margin-left: 30px; -`; - -export const Avatar = styled.img` - width: 100%; - height: 100%; - object-fit: cover; -`; - -export const UserInfo = styled.div` - display: flex; - flex-direction: column; - justify-content: center; -`; - -export const Username = styled.div` - color: var(--Color-black, #000); - - /* Body1/Medium */ - font-family: 'Gmarket Sans'; - font-size: 1rem; /* 16px */ - font-style: normal; - font-weight: 400; - line-height: normal; -`; - -export const Bio = styled.div` - color: var(--Color-gray4, #434343); - - /* Body4/Light */ - font-family: 'Pretendard Variable'; - font-size: 0.8125rem; /* 13px */ - font-style: normal; - font-weight: 300; - line-height: normal; - - margin-top: 10px; -`; - -export const StatsContainer = styled.div` - display: flex; - justify-content: space-around; - padding: 0.625rem 0; /* 10px 0 */ - border-top: 1px solid #eee; - border-bottom: 1px solid #eee; -`; - -export const Stat = styled.div` - display: flex; - flex-direction: column; - align-items: center; -`; - -export const StatNumber = styled.div` - color: var(--Color-gray4, #434343); - text-align: center; - - /* Body1/Medium */ - font-family: 'Gmarket Sans'; - font-size: 1rem; /* 16px */ - font-style: normal; - font-weight: 400; - line-height: normal; -`; - -export const StatLabel = styled.div` - color: var(--Color-gray4, #434343); - text-align: center; - - /* Body6/Light */ - font-family: 'Pretendard Variable'; - font-size: 0.75rem; /* 12px */ - font-style: normal; - font-weight: 300; - line-height: normal; -`; - -export const PostsContainer = styled.div` - display: flex; - flex-wrap: wrap; - justify-content: space-between; /* 두 개씩 나란히 배치 */ - gap: 0; /* 간격을 없앰 */ - cursor: pointer; -`; - -export const AddButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - position: fixed; /* absolute에서 fixed로 변경 */ - bottom: 6.75rem; - right: 1.25rem; - width: 5rem; - height: 5rem; - border: none; - border-radius: 50%; - background-color: ${({ theme }) => theme.colors.white}; - color: ${({ theme }) => theme.colors.black}; - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1); - font-size: 1rem; - cursor: pointer; - z-index: 2; - - &:hover { - background-color: ${({ theme }) => theme.colors.gray3}; - } -`; diff --git a/src/pages/NotFound/index.tsx b/src/pages/NotFound/index.tsx new file mode 100644 index 00000000..4c879a1f --- /dev/null +++ b/src/pages/NotFound/index.tsx @@ -0,0 +1,44 @@ +import { useNavigate } from 'react-router-dom'; +import { OODDFrame } from '../../components/Frame/Frame'; +import { NotFoundContainer, TextContainer, ButtonContainer, StyledButton } from './styles'; +import { StyledText } from '../../components/Text/StyledText'; +import theme from '../../styles/theme'; + +const NotFound = () => { + const navigate = useNavigate(); + + return ( + + + + + 404 ERROR + + 죄송합니다. 페이지를 찾을 수 없습니다. +
+ 페이지의 주소가 잘못 입력되었거나, + + 요청하신 페이지의 주소가 변경, 삭제되어 찾을 수 없습니다. + +
+
+ + + 메인으로 + + navigate(-1)} + className="prev" + $textTheme={{ style: 'body2-regular' }} + color={theme.colors.white} + > + 이전으로 + + +
+
+ ); +}; + +export default NotFound; diff --git a/src/pages/NotFound/styles.tsx b/src/pages/NotFound/styles.tsx new file mode 100644 index 00000000..ec172fb8 --- /dev/null +++ b/src/pages/NotFound/styles.tsx @@ -0,0 +1,49 @@ +import styled from 'styled-components'; +import { StyledText } from '../../components/Text/StyledText'; + +export const NotFoundContainer = styled.div` + display: flex; + height: 80%; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export const TextContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + + div { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } +`; + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 16px; + margin: 20px; +`; + +export const StyledButton = styled(StyledText)` + display: inline-block; + text-align: center; + padding: 6px 16px; + border: 1px solid ${({ theme }) => theme.colors.pink3}; + border-radius: 8px; + cursor: pointer; + text-decoration: none; + + &.prev { + background-color: ${({ theme }) => theme.colors.pink3}; + color: ${({ theme }) => theme.colors.white}; + } +`; diff --git a/src/pages/Post/ClothingInfoCard.tsx b/src/pages/Post/ClothingInfoCard.tsx deleted file mode 100644 index c15aefad..00000000 --- a/src/pages/Post/ClothingInfoCard.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { StyledText } from '../../components/Text/StyledText'; -import theme from '../../styles/theme'; -import { - NextBtn, - ClothingInfoCardContainer, - ClothingInfoImg, - ClothingInfoDetail, - StyledTextClipped, - ClothingInfoLeft, -} from './styles'; -import nextBtn from './../../assets/Post/next.svg'; -import { ClothingInfoCardProps } from './dto'; - -const ClothingInfoCard: React.FC = ({ imageUrl, brand, model, url }) => { - const handleClick = () => { - if (url) { - window.location.href = url; - } - }; - - return ( - - - - - - {brand} - - - {model} - - - - - nextBtn - - - ); -}; - -export default ClothingInfoCard; diff --git a/src/pages/Post/PostTopBar.tsx b/src/pages/Post/PostTopBar.tsx deleted file mode 100644 index 4d7eb0e6..00000000 --- a/src/pages/Post/PostTopBar.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { BackIcon, MidWrapper, PostTopBarContainer, RightSpace } from './styles'; -import backIcon from './../../assets/ProfileViewer/backIcon.svg'; -import { StyledText } from '../../components/Text/StyledText'; -import theme from '../../styles/theme'; -import { useNavigate } from 'react-router-dom'; -import { PostTopBarProps } from './dto'; - -// Post 페이지의 상단 바입니다. -const PostTopBar: React.FC = ({ userName }) => { - const nav = useNavigate(); - - return ( - - nav(-1)} /> - - - {userName} - - - OOTD - - - - - ); -}; - -export default PostTopBar; diff --git a/src/pages/Post/dto.ts b/src/pages/Post/dto.ts index 3b27eb24..e69de29b 100644 --- a/src/pages/Post/dto.ts +++ b/src/pages/Post/dto.ts @@ -1,61 +0,0 @@ -export interface PostTopBarProps { - userName: string; -} - -export interface PostResponse { - isSuccess: boolean; - code: number; - message: string; - result: PostData; -} - -interface Comments { - commentId: number | null; - userId: number | null; - text: string | null; - timestamp: string | null; -} - -export interface PostData { - postId: number; - userId: number; - likes: number | null; - comments: Comments[] | null; - photoUrls: string[]; - content: string; - styletags: string[]; - clothingInfo: ClothingInfo[] | null; -} - -export interface ClothingInfo { - brand: string; - model: string; - modelNumber: number; - url: string; - imageUrl: string; -} - -export interface UserResponse { - isSuccess: boolean; - code: number; - message: string; - result: UserData; -} - -export interface UserData { - id: number; - name: string; - email: string; - nickname: string | null; - phoneNumber: string | null; - profilePictureUrl: string; - bio: string | null; - joinedAt: string; -} - -export interface ClothingInfoCardProps { - imageUrl: string; - brand: string; - model: string; - url: string; -} diff --git a/src/pages/Post/index.tsx b/src/pages/Post/index.tsx index cb595c3e..d2c2cb17 100644 --- a/src/pages/Post/index.tsx +++ b/src/pages/Post/index.tsx @@ -1,302 +1,41 @@ -import React, { useEffect, useState } from 'react'; -import { OODDFrame } from '../../components/Frame/Frame'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import { Pagination, Navigation } from 'swiper/modules'; -import { BottomSheetMenuProps } from '../../components/BottomSheetMenu/dto.ts'; -import { StyledText } from '../../components/Text/StyledText'; -import { - MoreBtn, - PostImg, - PostInfo, - PostText, - PostWrapper, - ClothingInfos, - UserInfo, - UserName, - UserProfile, -} from './styles'; -import Loading from '../../components/Loading/index.tsx'; -import PostTopBar from './PostTopBar'; -import ClothingInfoCard from './ClothingInfoCard'; -import BottomSheet from '../../components/BottomSheet'; -import BottomSheetMenu from '../../components/BottomSheetMenu'; -import Modal from '../../components/Modal'; -import Comment from '../../components/Comment'; -import 'swiper/css'; -import 'swiper/css/pagination'; -import theme from '../../styles/theme'; -import profileImg from './../../assets/Post/profileImg.svg'; -import more from './../../assets/Post/more.svg'; -import declaration from '../../assets/Post/declaration.svg'; -import block from '../../assets/Post/block.svg'; -import ConfirmationModal from '../../components/ConfirmationModal/index.tsx'; -import { CommentProps } from '../../components/Comment/dto.ts'; -import { BottomSheetProps } from '../../components/BottomSheet/dto.ts'; -import ReportTextarea from '../Home/ReportTextarea.tsx'; -import { PostResponse, UserResponse, ClothingInfo } from './dto'; -import request from '../../apis/core'; +import React, { useState } from 'react'; -interface CommentResponse { - isSuccess: boolean; - message: string; - result?: any; // 성공 시 반환되는 데이터가 있다면 여기에 정의할 수 있습니다. -} +import { useRecoilValue } from 'recoil'; +import { postIdAtom, userAtom } from '../../recoil/Post/PostAtom.ts'; -const Post: React.FC = () => { - const { postId } = useParams<{ postId: string }>(); - const [postData, setPostData] = useState(); - const [user, setUser] = useState(); - const [userName, setUserName] = useState(''); - const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false); - const [isOpenReportSheet, setIsOpenReportSheet] = useState(false); - const [showInput, setShowInput] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); - const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); - const [isCommentModalOpen, setIsCommentModalOpen] = useState(false); - const nav = useNavigate(); - const location = useLocation(); - - useEffect(() => { - if (location.state && location.state.isCommentModalOpen) { - setIsCommentModalOpen(true); - } - }, [location.state]); - - useEffect(() => { - const fetchPostData = async () => { - try { - const response = await request.get(`/posts/${postId}`); - if (response.isSuccess) { - setPostData(response.result); - fetchUser(response.result.userId); - } else { - console.error('Failed to fetch post data'); - } - } catch (error) { - console.error('Error fetching post data:', error); - } - }; - - const fetchUser = async (userId: number) => { - try { - const response = await request.get(`/users/${userId}`); - if (response.isSuccess) { - setUser(response.result); - setUserName(response.result.nickname || response.result.name); - } else { - console.error('Failed to fetch user data'); - } - } catch (error) { - console.error('Error fetching user data:', error); - } - }; - - fetchPostData(); - }, [postId]); - - // 코멘트를 보내는 함수 - const sendComment = async (comment: string) => { - try { - setIsCommentModalOpen(false); - const response = await request.post(`/posts/${postId}/comment`, { - content: comment, - }); - - if (response.isSuccess) { - console.log('Comment sent successfully'); - } else { - console.error('Failed to send comment:', response.message); - } - } catch (error) { - console.error('Error sending comment:', error); - } - }; - - const bottomSheetMenuProps: BottomSheetMenuProps = { - items: [ - { - text: '신고하기', - action: () => { - setIsOpenBottomSheet(false); - setIsOpenReportSheet(true); - }, - icon: declaration, - }, - { - text: '차단하기', - action: () => { - setIsOpenBottomSheet(false); - setIsConfirmationModalOpen(true); - }, - icon: block, - }, - ], - marginBottom: '3.125rem', - }; +import PostBase from '../../components/PostBase/index.tsx'; +import OptionsBottomSheet from '../../components/BottomSheet/OptionsBottomSheet/index.tsx'; +import { OptionsBottomSheetProps } from '../../components/BottomSheet/OptionsBottomSheet/dto.ts'; - const reportSheetMenuProps: BottomSheetMenuProps = { - items: [ - { - text: '스팸', - action: () => { - setIsOpenReportSheet(false); - setIsModalOpen(true); - }, - }, - { - text: '부적절한 콘텐츠', - action: () => { - setIsOpenReportSheet(false); - setIsModalOpen(true); - }, - }, - { - text: '선정적', - action: () => { - setIsOpenReportSheet(false); - setIsModalOpen(true); - }, - }, - { - text: '직접 입력', - action: () => { - setShowInput((prev) => !prev); - }, - }, - ], - marginBottom: '3.125rem', - }; - - const bottomSheetProps: BottomSheetProps = { - isOpenBottomSheet: isOpenBottomSheet, - isHandlerVisible: true, - Component: BottomSheetMenu, - componentProps: bottomSheetMenuProps, - onCloseBottomSheet: () => { - setIsOpenBottomSheet(false); - setShowInput(false); - }, - }; - - const commentProps: CommentProps = { - content: `${userName}님의 게시물에 대한 코멘트를 남겨주세요.\n코멘트는 ${userName}님에게만 전달됩니다.`, - sendComment: sendComment, // API 함수 전달 - }; +const Post: React.FC = () => { + const [isOptionsBottomSheetOpen, setIsOptionsBottomSheetOpen] = useState(false); + const postId = useRecoilValue(postIdAtom); + const user = useRecoilValue(userAtom); - const commentSheetProps: BottomSheetProps = { - isOpenBottomSheet: isCommentModalOpen, - isHandlerVisible: true, - Component: Comment, - componentProps: commentProps, - onCloseBottomSheet: () => { - setIsCommentModalOpen(false); - }, + const handleMenuOpen = () => { + setIsOptionsBottomSheetOpen(true); }; - const confirmationModalProps = { - content: `${userName}님을 정말로 차단하시겠습니까?`, - isCancelButtonVisible: true, - confirm: { - text: '차단하기', - action: () => { - setIsConfirmationModalOpen(false); - setIsBlockedModalOpen(true); // 차단 완료 모달 열기 - }, + // 게시글 옵션(더보기) 바텀시트 + const optionsBottomSheetProps: OptionsBottomSheetProps = { + domain: 'post', + targetId: { + userId: user.userId || -1, + postId: postId || -1, }, - onCloseModal: () => { - setIsConfirmationModalOpen(false); + targetNickname: user.nickname || '알수없음', + isBottomSheetOpen: isOptionsBottomSheetOpen, + onClose: () => { + setIsOptionsBottomSheetOpen(false); }, }; - if (!postData) { - return ; // 로딩 중 표시 - } - return ( - - - ( -
- - {showInput && ( - setIsOpenReportSheet(false)} - onOpenModal={() => setIsModalOpen(true)} - /> - )} -
- )} - onCloseBottomSheet={() => { - setIsOpenReportSheet(false); - setShowInput(false); - }} - /> - - {isModalOpen && setIsModalOpen(false)} />} - {isConfirmationModalOpen && } - {isBlockedModalOpen && ( - setIsBlockedModalOpen(false)} /> - )} + <> + - - - - nav(`/users/${postData.userId}`)}> - - profileImg - - - - {userName} - - - - setIsOpenBottomSheet(true)}> - more - - - - - {postData.content} - - - - - {postData.photoUrls.map((image: string, index: number) => ( - - {`postImg-${index}`} - - ))} - - - - {postData.clothingInfo?.map((clothingInfo: ClothingInfo, index: number) => ( - - ))} - - -
+ + ); }; diff --git a/src/pages/Post/styles.tsx b/src/pages/Post/styles.tsx index fe5a3d50..e7119c4a 100644 --- a/src/pages/Post/styles.tsx +++ b/src/pages/Post/styles.tsx @@ -1,168 +1,4 @@ import styled from 'styled-components'; -import { StyledText } from '../../components/Text/StyledText'; - -// PostTopBar - -export const PostTopBarContainer = styled.div` - width: 100%; - max-width: 32rem; - height: 2.75rem; - display: flex; - justify-content: space-between; - background-color: #ffffff; - z-index: 10; - align-items: center; - position: fixed; -`; - -export const BackIcon = styled.img` - width: 0.5625rem; - height: 1.125rem; - margin-left: 1.3125rem; - cursor: pointer; - overflow: hidden; - - img { - width: 100%; - height: 100%; - } -`; - -export const MidWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 0.125rem; - margin-top: 0.25rem; -`; - -export const RightSpace = styled.div` - width: 0.5625rem; - height: 1.125rem; - margin-right: 1.3125rem; -`; - -// Post - -export const PostWrapper = styled.div` - width: 100%; - height: auto; -`; - -export const PostInfo = styled.div` - display: flex; - margin-top: 3.375rem; - justify-content: space-between; - align-items: center; -`; - -export const UserInfo = styled.div` - cursor: pointer; - display: flex; - align-items: center; -`; - -export const UserProfile = styled.div` - width: 2.25rem; - height: 2.25rem; - border-radius: 50%; - overflow: hidden; - margin: 0 0.75rem 0 1.25rem; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } -`; - -export const UserName = styled.div``; - -export const MoreBtn = styled.div` - width: 1.5rem; - height: 1.5rem; - margin-right: 1.25rem; - cursor: pointer; -`; - -export const PostText = styled.span` - margin: 0 1.25rem 0.5rem; - margin-top: 0.75rem; - margin-bottom: 0.4875rem; - display: flex; - align-items: center; - word-wrap: break-word; - word-break: break-all; -`; - -export const PostImg = styled.div` - width: 100%; - position: relative; - - .postSwiper { - position: relative; - width: 100%; - padding-bottom: 133.33%; - } - - .postSwiper .swiper-pagination { - position: absolute; - top: 0.75rem; - left: 50%; - transform: translateX(-50%); - z-index: 10; - pointer-events: none; - } - - .postSwiper .swiper-pagination-bullet { - width: 0.375rem; - height: 0.375rem; - border: 0.0625rem solid ${({ theme }) => theme.colors.white}; - background: rgba(255, 255, 255, 0.5); - opacity: 1; - pointer-events: auto; - } - - .postSwiper .swiper-pagination-bullet-active { - width: 0.375rem; - height: 0.375rem; - background-color: ${({ theme }) => theme.colors.white}; - opacity: 1; - } - - .postSwiper .swiper-button-prev:after, - .postSwiper .swiper-button-next:after { - text-shadow: 0rem 0rem 0.25rem rgba(0, 0, 0, 0.25); - color: ${({ theme }) => theme.colors.white} !important; - font-size: 1.5rem !important; - } - - img { - position: absolute; - top: 0; - left: 0; - width: 100%; - aspect-ratio: 3 / 4; - height: auto; - object-fit: cover; - } -`; - -export const ClothingInfos = styled.div` - margin-top: 0.6687rem; - margin-left: 1.25rem; - margin-bottom: 4rem; - display: flex; - overflow-x: auto; - white-space: nowrap; - scrollbar-width: none; /* Firefox에서 스크롤바 숨기기 */ - -ms-overflow-style: none; /* Internet Explorer에서 스크롤바 숨기기 */ - - &::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera에서 스크롤바 숨기기 */ - } -`; export const InputLayout = styled.div` display: flex; @@ -190,83 +26,3 @@ export const InputLayout = styled.div` resize: none; } `; - -// export const InputWrapper = styled.textarea` -// display: block; -// width: calc(100% - 3rem); -// height: 5.75rem; -// border-radius: 0.125rem; -// border: 0.0625rem solid ${({ theme }) => theme.colors.gray3}; -// margin-bottom: 5.875rem; -// z-index: 2; -// margin-top: -3.75rem; -// outline: none; -// padding: 0.8125rem 0.9375rem; -// font-family: 'Pretendard Variable'; -// font-size: 1rem; -// font-style: normal; -// font-weight: 300; -// line-height: 150%; -// color: ${({ theme }) => theme.colors.black}; -// resize: none; - -// &::placeholder { -// color: ${({ theme }) => theme.colors.gray3}; -// } -// `; - -// ClothingInfoCard - -export const ClothingInfoCardContainer = styled.div` - position: relative; - width: 15.3125rem; - height: 4.5rem; - border-radius: 0.1875rem; - box-shadow: 0 0 0 0.0625rem ${({ theme }) => theme.colors.gray3} inset; - display: flex; - align-items: center; - flex-shrink: 0; - justify-content: space-between; - margin-right: 0.75rem; - cursor: pointer; -`; - -export const ClothingInfoLeft = styled.div` - display: flex; - align-items: center; -`; - -export const ClothingInfoImg = styled.img` - width: 3.5rem; - height: 3.5rem; - margin: 0.5rem; -`; - -export const ClothingInfoDetail = styled.div` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - width: 8rem; - display: flex; - flex-direction: column; -`; - -export const StyledTextClipped = styled(StyledText)` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - width: 100%; - display: inline-block; /* 텍스트 클리핑을 적용하기 위해 inline-block으로 설정 */ -`; - -export const NextBtn = styled.div` - position: absolute; - right: 0; - width: 1.5rem; - height: 1.5rem; - margin-right: 0.375rem; - img { - width: 100%; - height: 100%; - } -`; diff --git a/src/pages/Upload/ImageReviewModal/ImageSwiper/index.tsx b/src/pages/PostImageSelect/ImageSwiper/index.tsx similarity index 58% rename from src/pages/Upload/ImageReviewModal/ImageSwiper/index.tsx rename to src/pages/PostImageSelect/ImageSwiper/index.tsx index 79ae4f64..0a2b09a2 100644 --- a/src/pages/Upload/ImageReviewModal/ImageSwiper/index.tsx +++ b/src/pages/PostImageSelect/ImageSwiper/index.tsx @@ -1,39 +1,22 @@ import React, { useRef, useState } from 'react'; import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation, Pagination } from 'swiper/modules'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; import { SwiperContainer, ImageWrapper, RemoveButton, StyledNavigation, AddButton, HiddenFileInput } from './styles'; -import remove from '../../../../assets/Upload/remove.svg'; -import plus from '../../../../assets/Upload/plus.svg'; -import { Navigation, Pagination } from 'swiper/modules'; + +import Reject from '../../../assets/default/reject.svg'; +import Plus from '../../../assets/default/plus.svg'; + import { ImageSwiperProps } from '../dto'; -const ImageSwiper: React.FC = ({ images, onRemove, onAddImages }) => { +const ImageSwiper: React.FC = ({ images, onProcessFile, onRemoveImage }) => { const fileInputRef = useRef(null); const [currentSlide, setCurrentSlide] = useState(0); - const handleFileUpload = (event: React.ChangeEvent) => { - if (event.target.files) { - const filesArray = Array.from(event.target.files); - const newImages: string[] = []; - filesArray.forEach((file) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (reader.result) { - newImages.push(reader.result.toString()); - if (newImages.length === filesArray.length) { - onAddImages(newImages); - } - } - }; - reader.readAsDataURL(file); - }); - } - }; - - const handleAddImageClick = () => { + const handleSelectImage = () => { if (fileInputRef.current) { fileInputRef.current.click(); } @@ -57,20 +40,30 @@ const ImageSwiper: React.FC = ({ images, onRemove, onAddImages {images.map((image, index) => ( - {`Selected + {`Selected {images.length > 1 && ( - onRemove(image)}> - Remove + onRemoveImage(image.imageUrl)}> + remove )} ))} - - + + - + { + if (event.target.files) { + onProcessFile(event.target.files); + } + }} + ref={fileInputRef} + multiple + accept="image/*,.heic" + /> diff --git a/src/pages/Upload/ImageReviewModal/ImageSwiper/styles.tsx b/src/pages/PostImageSelect/ImageSwiper/styles.tsx similarity index 80% rename from src/pages/Upload/ImageReviewModal/ImageSwiper/styles.tsx rename to src/pages/PostImageSelect/ImageSwiper/styles.tsx index 631eef9c..f7e511e5 100644 --- a/src/pages/Upload/ImageReviewModal/ImageSwiper/styles.tsx +++ b/src/pages/PostImageSelect/ImageSwiper/styles.tsx @@ -16,7 +16,7 @@ export const SwiperContainer = styled.div` .review-swiper .swiper-slide { width: 21.875rem; max-width: calc(100% - 2.5rem); - aspect-ratio: 3 / 4; + aspect-ratio: 4 / 5; height: auto; object-fit: cover; transition: transform 0.3s; @@ -48,8 +48,9 @@ export const ImageWrapper = styled.div` height: 100%; width: auto; width: 100%; - aspect-ratio: 3 / 4; + aspect-ratio: 4 / 5; object-fit: cover; + border-radius: 8px; } `; @@ -61,21 +62,24 @@ export const StyledNavigation = styled.button` top: 50%; transform: translateY(-50%); color: white; - width: 4.375rem; - height: 4.375rem; - padding: 1.25rem; + background-color: rgba(192, 192, 192, 0.5); + border-radius: 50%; + width: 40px; + height: 40px; &::after { - font-size: 1.25rem; + font-size: 1rem; color: white; } &.swiper-button-prev { - margin: 0 0 0 max(calc((100% - 21.875rem) / 2 - 1.25rem), 0rem); + margin: 0 0 0 max(calc((100% - 19.875rem) / 2 - 1.25rem), 0rem); + padding-right: 3px; } &.swiper-button-next { - margin: 0 max(calc((100% - 21.875rem) / 2) - 1.25rem, 0rem) 0 0; + margin: 0 max(calc((100% - 19.875rem) / 2) - 1.25rem, 0rem) 0 0; + padding-left: 3px; } `; @@ -94,16 +98,9 @@ export const AddButton = styled.button` align-items: center; justify-content: center; background: none; - color: #999; - font-size: 1.875rem; - width: 6.25rem; - height: 6.25rem; + width: 80px; + height: 80px; margin: auto; - - &:hover { - border-color: #666; - color: #666; - } `; export const HiddenFileInput = styled.input` diff --git a/src/pages/PostImageSelect/dto.ts b/src/pages/PostImageSelect/dto.ts new file mode 100644 index 00000000..15dd65bf --- /dev/null +++ b/src/pages/PostImageSelect/dto.ts @@ -0,0 +1,8 @@ +export interface ImageSelectModalProps {} +import { PostImage } from '../../apis/post/dto'; + +export interface ImageSwiperProps { + images: PostImage[]; + onProcessFile: (files: FileList) => void; + onRemoveImage: (image: string) => void; +} diff --git a/src/pages/PostImageSelect/index.tsx b/src/pages/PostImageSelect/index.tsx new file mode 100644 index 00000000..41495da4 --- /dev/null +++ b/src/pages/PostImageSelect/index.tsx @@ -0,0 +1,175 @@ +import React, { useState, useRef } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; + +import { useRecoilState } from 'recoil'; +import { + postImagesAtom, + postContentAtom, + postClothingInfosAtom, + postStyletagAtom, + postIsRepresentativeAtom, +} from '../../recoil/PostUpload/PostUploadAtom'; + +import { UploadContainer, ImageDragDropContainer, Content } from './styles'; + +import { OODDFrame } from '../../components/Frame/Frame'; +import { StyledText } from '../../components/Text/StyledText'; +import TopBar from '../../components/TopBar'; +import BottomButton from '../../components/BottomButton'; +import ImageSwiper from './ImageSwiper'; + +import X from '../../assets/default/x.svg'; +import Left from '../../assets/arrow/left.svg'; +import PhotoBig from '../../assets/default/photo-big.svg'; + +import { ImageSelectModalProps } from './dto'; +import heic2any from 'heic2any'; + +const PostImageSelect: React.FC = () => { + const [images, setImages] = useRecoilState(postImagesAtom); + const [, setContent] = useRecoilState(postContentAtom); + const [, setClothingInfos] = useRecoilState(postClothingInfosAtom); + const [, setStyletag] = useRecoilState(postStyletagAtom); + const [, setIsRepresentative] = useRecoilState(postIsRepresentativeAtom); + const [isActive, setActive] = useState(false); + const fileInputRef = useRef(null); + const location = useLocation(); + const navigate = useNavigate(); + + const handleClose = () => { + navigate('/mypage'); + }; + + const handlePrev = () => { + setImages([]); + setContent(''); + setClothingInfos([]); + setStyletag([]); + setIsRepresentative(false); + }; + + const handleNext = () => { + const state = location.state as { mode?: string; postId?: number }; + navigate('/upload', { state: { mode: state?.mode, postId: state?.postId } }); + }; + + // 파일 선택기에서 사진 업로드 + const handleSelectImage = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + // 드래그 앤 드롭으로 사진 업로드 + const handleDragEnter = () => setActive(true); + const handleDragLeave = () => setActive(false); + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); // 파일을 드래그했을 때, 브라우저 기본 동작에 의해 새 창이 뜨는 것 방지 + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); // 파일을 드롭했을 때, 브라우저 기본 동작에 의해 새 창이 뜨는 것 방지 + + setActive(false); + + if (event.dataTransfer.files) { + handleProcessFile(event.dataTransfer.files); + } + }; + + const handleFileInputChange = (event: React.ChangeEvent) => { + event.preventDefault(); + if (event.target.files) { + handleProcessFile(event.target.files); + // 파일 선택 후 input 값 초기화 + if (fileInputRef.current) { + fileInputRef.current.value = ''; // input 값을 초기화하여 동일한 파일을 다시 추가할 수 있도록 함 + } + } + }; + + const handleProcessFile = async (files: FileList) => { + const filesArray = Array.from(files); + for (const file of filesArray) { + try { + let fileBlob = file; + + // HEIC 파일인 경우 변환 + if (/\.(heic)$/i.test(fileBlob.name)) { + const convertedBlob = await heic2any({ blob: fileBlob, toType: 'image/jpeg' }); + + // Blob을 File로 변환 + const newFile = new File([convertedBlob as Blob], fileBlob.name.replace(/\.heic$/i, '.jpeg'), { + type: 'image/jpeg', + lastModified: new Date().getTime(), + }); + fileBlob = newFile; // 변환된 파일을 다시 fileBlob으로 할당 + } + + const reader = new FileReader(); + reader.readAsDataURL(fileBlob); + reader.onload = () => { + if (reader.result) { + handleAddImage(reader.result.toString()); + } + }; + } catch (error) { + alert('이미지 처리 중 오류가 발생했습니다.'); + console.error(error); + } + } + }; + + const handleAddImage = (newImage: string) => { + setImages((prevImages) => { + const maxOrderNum = prevImages.reduce((max, img) => (img.orderNum > max ? img.orderNum : max), -1); + return [...prevImages, { imageUrl: newImage, orderNum: maxOrderNum + 1 }]; + }); + }; + + const handleRemoveImage = (image: string) => { + // 이미지가 1개일 때는 삭제 할 수 없음 + if (images.length > 1) { + const newImages = images.filter((img) => img.imageUrl !== image); + setImages(newImages.map((img, idx) => ({ ...img, orderNum: idx }))); + } + }; + + return ( + + + + + {images.length === 0 ? ( + + + 사진을 여기에 끌어다 놓으세요 + + + + + ) : ( + + )} + + + + + + ); +}; + +export default PostImageSelect; diff --git a/src/pages/PostImageSelect/styles.tsx b/src/pages/PostImageSelect/styles.tsx new file mode 100644 index 00000000..2fbe61c2 --- /dev/null +++ b/src/pages/PostImageSelect/styles.tsx @@ -0,0 +1,59 @@ +import styled from 'styled-components'; + +export const UploadContainer = styled.div` + flex-grow: 1; + height: 100vh; + width: 100%; + position: relative; +`; + +export const ImageDragDropContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; + height: 100%; + position: absolute; + top: 2.75rem; + left: 0; + + div { + margin-bottom: 4.5rem; + } + + svg { + z-index: 2; + } + + :nth-child(2) { + margin-bottom: 9rem; + } + + &.active svg { + color: ${({ theme }) => theme.colors.black}; + } + + input { + display: none; + } +`; + +export const Content = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: absolute; + top: 2.75rem; + left: 0; + width: 100%; + //max-width: 512px; + height: calc(100% - 10rem); + flex: 1; +`; + +export const HiddenFileInput = styled.input` + display: none; +`; diff --git a/src/pages/PostInstaConnect/dto.ts b/src/pages/PostInstaConnect/dto.ts new file mode 100644 index 00000000..2a10b46e --- /dev/null +++ b/src/pages/PostInstaConnect/dto.ts @@ -0,0 +1,6 @@ +export interface InstaConnectModalProps {} + +export interface Post { + imgs: string[]; + caption: string; +} diff --git a/src/pages/PostInstaConnect/index.tsx b/src/pages/PostInstaConnect/index.tsx new file mode 100644 index 00000000..27c09fc4 --- /dev/null +++ b/src/pages/PostInstaConnect/index.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; + +import theme from '../../styles/theme'; +import { Content, StyledInput } from './styles'; + +import { OODDFrame } from '../../components/Frame/Frame'; +import { StyledText } from '../../components/Text/StyledText'; +import TopBar from '../../components/TopBar'; +import BottomButton from '../../components/BottomButton'; +import Modal from '../../components/Modal'; +import { ModalProps } from '../../components/Modal/dto'; + +import X from '../../assets/default/x.svg'; + +import { InstaConnectModalProps } from './dto'; + +const PostInstaConnect: React.FC = () => { + const [instagramID, setInstagramID] = useState(''); + const [isConnectFailModalOpen, setIsConnectFailModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const handleConnect = async () => { + try { + setIsLoading(true); + window.location.href = 'https://localhost:3001/auth'; //인스타그램 인증 처리 + } catch (error) { + console.error('Failed to fetch Instagram media:', error); + setIsLoading(false); + setIsConnectFailModalOpen(true); + } + }; + + const connectFailModalProps: ModalProps = { + isCloseButtonVisible: false, + onClose: () => setIsConnectFailModalOpen(false), + content: `${instagramID} 계정 연동에 실패했어요`, + button: { + content: '다시 시도하기', + onClick: () => { + setIsConnectFailModalOpen(false); + handleConnect(); + }, + }, + }; + + /* + useEffect(() => { + if (accessToken) { + fetchInstagramData(accessToken); + } + }, [accessToken]); + */ + + return ( + + + + + {isLoading ? ( + + {instagramID} 계정에 연동하고 있어요 + + ) : ( + <> + 인스타 계정 연동을 위해 + + 인스타그램 ID를 작성해주세요 + + setInstagramID(e.target.value)} + placeholder="인스타그램 ID" + /> + + {!instagramID ? '탭해서 ID를 작성하세요' : ' .'} + + + )} + + + + + {isConnectFailModalOpen && } + + ); +}; + +export default PostInstaConnect; diff --git a/src/pages/Upload/InstaConnectModal/styles.tsx b/src/pages/PostInstaConnect/styles.tsx similarity index 67% rename from src/pages/Upload/InstaConnectModal/styles.tsx rename to src/pages/PostInstaConnect/styles.tsx index 2359b2d3..5d2cc0d4 100644 --- a/src/pages/Upload/InstaConnectModal/styles.tsx +++ b/src/pages/PostInstaConnect/styles.tsx @@ -1,5 +1,12 @@ import styled from 'styled-components'; +export const UploadContainer = styled.div` + flex-grow: 1; + height: 100vh; + width: 100%; + position: relative; +`; + export const Content = styled.div` display: flex; flex-direction: column; @@ -22,51 +29,40 @@ export const StyledInput = styled.input` padding: 0; margin: 6.25rem 0 0.9375rem 0; border: none; - font-family: 'Gmarket Sans'; - font-weight: 400; - font-size: 2rem; text-align: center; + ${({ theme }) => theme.fontStyles['title1-regular']} + &:focus { outline: none; } ::placeholder { color: ${({ theme }) => theme.colors.gray3}; - font-family: 'Gmarket Sans'; - font-weight: 400; - font-size: 2rem; + ${({ theme }) => theme.fontStyles['title1-regular']} } /* Firefox */ &:-moz-placeholder { color: ${({ theme }) => theme.colors.gray3}; - font-family: 'Gmarket Sans'; - font-weight: 400; - font-size: 2rem; + ${({ theme }) => theme.fontStyles['title1-regular']} } /* Internet Explorer 10-11 */ &:-ms-input-placeholder { color: ${({ theme }) => theme.colors.gray3}; - font-family: 'Gmarket Sans'; - font-weight: 400; - font-size: 2rem; + ${({ theme }) => theme.fontStyles['title1-regular']} } /* Edge */ &::-ms-input-placeholder { color: ${({ theme }) => theme.colors.gray3}; - font-family: 'Gmarket Sans'; - font-weight: 400; - font-size: 2rem; + ${({ theme }) => theme.fontStyles['title1-regular']} } /* Safari */ &::placeholder { color: ${({ theme }) => theme.colors.gray3}; - font-family: 'Gmarket Sans'; - font-weight: 400; - font-size: 2rem; + ${({ theme }) => theme.fontStyles['title1-regular']} } `; diff --git a/src/pages/PostInstaFeedSelect/dto.ts b/src/pages/PostInstaFeedSelect/dto.ts new file mode 100644 index 00000000..82dee836 --- /dev/null +++ b/src/pages/PostInstaFeedSelect/dto.ts @@ -0,0 +1,5 @@ +export interface InstaFeedSelectModalProps {} + +export interface Post { + imgs: string[]; +} diff --git a/src/pages/PostInstaFeedSelect/index.tsx b/src/pages/PostInstaFeedSelect/index.tsx new file mode 100644 index 00000000..fa422c52 --- /dev/null +++ b/src/pages/PostInstaFeedSelect/index.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import { useRecoilState } from 'recoil'; +import { postImagesAtom } from '../../recoil/PostUpload/PostUploadAtom'; + +import { Content, PostContainer, ImageWrapper } from './styles'; + +import { OODDFrame } from '../../components/Frame/Frame'; +import TopBar from '../../components/TopBar'; +import Modal from '../../components/Modal'; +import { ModalProps } from '../../components/Modal/dto'; + +import X from '../../assets/default/x.svg'; + +import { InstaFeedSelectModalProps, Post } from './dto'; + +const PostInstaFeedSelect: React.FC = () => { + const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(true); + const [, setIsLoading] = useState(false); + const [isFailModalOpen, setIsFailModalOpen] = useState(false); + const [posts, setPosts] = useState([]); // Post 타입으로 지정 + const [, setImages] = useRecoilState(postImagesAtom); + const navigate = useNavigate(); + + // 인스타그램 데이터 가져오는 함수 + const fetchInstagramData = async (accessToken: string) => { + try { + setIsLoading(true); + const response = await axios.get('https://localhost:3001/instagram-import', { + params: { access_token: accessToken }, + }); + setPosts(response.data as Post[]); // Post 타입으로 받아옴 + } catch (error) { + console.error('Failed to fetch Instagram media:', error); + setIsFailModalOpen(true); // 실패 모달 열기 + } finally { + setIsLoading(false); + } + }; + + // 연동 실패 모달 속성 + const connectFailModalProps: ModalProps = { + isCloseButtonVisible: false, + content: `계정 연동에 실패했어요`, + onClose: () => setIsFailModalOpen(false), + button: { + content: '다시 시도하기', + onClick: () => { + setIsFailModalOpen(false); + fetchInstagramData('accessToken'); // 함수 호출 시 실행되도록 수정 + }, + }, + }; + + // 연동 성공 모달 속성 + const connectSuccessModalProps: ModalProps = { + content: `계정 연동에 성공했어요!\n가져올 OOTD를 선택해 보세요`, + onClose: () => { + setIsSuccessModalOpen(false); + }, + }; + + // 이미지 선택 시 실행 + const handlePostSelect = (post: Post) => { + const newImages = post.imgs.map((imageUrl, index) => ({ imageUrl, orderNum: index })); + setImages(newImages); // 선택한 이미지 Recoil 상태로 설정 + navigate('/upload'); // 다음 페이지로 이동 + }; + + // 페이지 종료 함수 + const handleClose = () => { + navigate('/mypage'); // 마이페이지로 이동 + }; + + return ( + + {isSuccessModalOpen && } + {isFailModalOpen && } + {' '} + + {posts.map((post, index) => ( + handlePostSelect(post)}> + + {`Instagram {/* alt 추가 */} + + + ))} + + + ); +}; + +export default PostInstaFeedSelect; diff --git a/src/pages/Upload/InstaFeedSelectModal/styles.tsx b/src/pages/PostInstaFeedSelect/styles.tsx similarity index 89% rename from src/pages/Upload/InstaFeedSelectModal/styles.tsx rename to src/pages/PostInstaFeedSelect/styles.tsx index 25733a1f..295bd824 100644 --- a/src/pages/Upload/InstaFeedSelectModal/styles.tsx +++ b/src/pages/PostInstaFeedSelect/styles.tsx @@ -1,5 +1,12 @@ import styled from 'styled-components'; +export const UploadContainer = styled.div` + flex-grow: 1; + height: 100vh; + width: 100%; + position: relative; +`; + export const Content = styled.div` display: grid; grid-template-columns: repeat(auto-fit, minmax(6.25rem, 1fr)); diff --git a/src/pages/Upload/PostUploadModal/ImageSwiper/index.tsx b/src/pages/PostUpload/ImageSwiper/index.tsx similarity index 91% rename from src/pages/Upload/PostUploadModal/ImageSwiper/index.tsx rename to src/pages/PostUpload/ImageSwiper/index.tsx index 3ed8fe2d..aaa0ef0a 100644 --- a/src/pages/Upload/PostUploadModal/ImageSwiper/index.tsx +++ b/src/pages/PostUpload/ImageSwiper/index.tsx @@ -1,11 +1,14 @@ import React, { useRef } from 'react'; import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react'; +import { Navigation, Pagination } from 'swiper/modules'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; + import { SwiperContainer, ImageWrapper, StyledNavigation, StyledPagination } from './styles'; -import picture2 from '../../../../assets/Upload/picture2.svg'; -import { Navigation, Pagination } from 'swiper/modules'; + +import PhotoWhite from '../../../assets/default/photo-white.svg'; + import { ImageSwiperProps } from '../dto'; const ImageSwiper: React.FC = ({ images }) => { @@ -29,7 +32,7 @@ const ImageSwiper: React.FC = ({ images }) => { renderCustom: (_, current, total) => { return `
- Pagination Icon + Pagination Icon ${current}/${total}
`; }, @@ -51,7 +54,7 @@ const ImageSwiper: React.FC = ({ images }) => { {images.map((image, index) => ( - {`Selected + {`Selected ))} diff --git a/src/pages/Upload/PostUploadModal/ImageSwiper/styles.tsx b/src/pages/PostUpload/ImageSwiper/styles.tsx similarity index 91% rename from src/pages/Upload/PostUploadModal/ImageSwiper/styles.tsx rename to src/pages/PostUpload/ImageSwiper/styles.tsx index 279d174e..be380107 100644 --- a/src/pages/Upload/PostUploadModal/ImageSwiper/styles.tsx +++ b/src/pages/PostUpload/ImageSwiper/styles.tsx @@ -18,7 +18,7 @@ export const SwiperContainer = styled.div` } .upload-swiper .swiper-slide { - width: 15.4375rem; + width: 16.45rem; height: 20.5625rem; object-fit: cover; transition: transform 0.3s; @@ -39,8 +39,9 @@ export const ImageWrapper = styled.div` height: 100%; img { + border-radius: 8px; height: 100%; - aspect-ratio: 3 / 4; + aspect-ratio: 4 / 5; object-fit: cover; } `; @@ -79,11 +80,11 @@ export const StyledPagination = styled.div` display: flex; justify-content: center; align-items: center; - width: 3.75rem; - height: 1.5rem; + width: 65px; + height: 34px; color: white; - background: ${({ theme }) => theme.colors.black}; - border-radius: 0.75rem; + background: ${({ theme }) => theme.colors.gradient}; + border-radius: 17px; .swiper-pagination-custom { display: flex; diff --git a/src/pages/Upload/PostUploadModal/SearchBottomSheetContent/index.tsx b/src/pages/PostUpload/SearchBottomSheetContent/index.tsx similarity index 93% rename from src/pages/Upload/PostUploadModal/SearchBottomSheetContent/index.tsx rename to src/pages/PostUpload/SearchBottomSheetContent/index.tsx index 5715f81c..b3c1b0bc 100644 --- a/src/pages/Upload/PostUploadModal/SearchBottomSheetContent/index.tsx +++ b/src/pages/PostUpload/SearchBottomSheetContent/index.tsx @@ -1,8 +1,11 @@ import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; + import { Content, Input, SearchResultList, SearchResultItem, Loader } from './styles'; -import { StyledText } from '../../../../components/Text/StyledText'; -import theme from '../../../../styles/theme'; +import theme from '../../../styles/theme'; + +import { StyledText } from '../../../components/Text/StyledText'; + import { SearchBottomSheetProps } from '../dto'; const SearchBottomSheetContent: React.FC = ({ onClose, onSelectClothingInfo }) => { @@ -115,9 +118,9 @@ const SearchBottomSheetContent: React.FC = ({ onClose, o const handleAddClothingInfo = (item: any) => { onSelectClothingInfo({ imageUrl: item.image, - brand: item.brand, - model: removeBrandFromTitle(item.title, item.brand), //검색 결과에서 태그 제거하고 텍스트만 표시 - modelNumber: 1, + brandName: item.brand, + modelName: removeBrandFromTitle(item.title, item.brand), //검색 결과에서 태그 제거하고 텍스트만 표시 + modelNumber: '1', url: item.link, }); onClose(); @@ -151,7 +154,7 @@ const SearchBottomSheetContent: React.FC = ({ onClose, o value={searchQuery} onChange={(e) => handleInputChange(e.target.value)} /> - + 취소
@@ -161,12 +164,12 @@ const SearchBottomSheetContent: React.FC = ({ onClose, o handleAddClothingInfo(searchResultItem)}> {searchResultItem.title.replace(/<[^]+>/g, '')} />
- + {searchResultItem.brand} {removeBrandFromTitle(searchResultItem.title, searchResultItem.brand)} diff --git a/src/pages/Upload/PostUploadModal/SearchBottomSheetContent/styles.tsx b/src/pages/PostUpload/SearchBottomSheetContent/styles.tsx similarity index 91% rename from src/pages/Upload/PostUploadModal/SearchBottomSheetContent/styles.tsx rename to src/pages/PostUpload/SearchBottomSheetContent/styles.tsx index ec36621a..c2639a89 100644 --- a/src/pages/Upload/PostUploadModal/SearchBottomSheetContent/styles.tsx +++ b/src/pages/PostUpload/SearchBottomSheetContent/styles.tsx @@ -2,14 +2,14 @@ import styled from 'styled-components'; export const Content = styled.div` - height: calc(100vh - 4rem); + height: calc(100vh - 44px); .input_container { display: flex; flex-direction: row; align-items: center; width: 100%; - padding: 0 1.25rem 0.825rem 1.25rem; + padding: 0.825rem 0; border-bottom: solid 0.0625rem ${({ theme }) => theme.colors.gray1}; @@ -27,7 +27,7 @@ export const Input = styled.input` padding: 0 0.9375rem; text-align: left; font-size: 1rem; - background-color: ${({ theme }) => theme.colors.gray1}; + background-color: #f8f8f8; border: 0.0625rem solid ${({ theme }) => theme.colors.gray2}; border-radius: 0.3125rem; @@ -80,8 +80,14 @@ export const SearchResultList = styled.div` flex-direction: column; width: 100%; height: calc(100% - 3.75rem); - padding: 0.9375rem; overflow-y: auto; + padding-bottom: 20px; + + scrollbar-width: none; // Firefox + -ms-overflow-style: none; // IE 10+ + &::-webkit-scrollbar { + display: none; // Safari & Chrome + } .total { color: ${({ theme }) => theme.colors.gray3}; diff --git a/src/pages/Upload/PostUploadModal/ToggleSwitch/index.tsx b/src/pages/PostUpload/ToggleSwitch/index.tsx similarity index 100% rename from src/pages/Upload/PostUploadModal/ToggleSwitch/index.tsx rename to src/pages/PostUpload/ToggleSwitch/index.tsx diff --git a/src/pages/PostUpload/ToggleSwitch/styles.tsx b/src/pages/PostUpload/ToggleSwitch/styles.tsx new file mode 100644 index 00000000..294066bd --- /dev/null +++ b/src/pages/PostUpload/ToggleSwitch/styles.tsx @@ -0,0 +1,35 @@ +import styled from 'styled-components'; + +export const HiddenCheckbox = styled.input.attrs({ type: 'checkbox' })` + appearance: none; + position: relative; + border: 1.5px solid ${({ theme }) => theme.colors.pink3}; + border-radius: 28px; + width: 52px; + height: 28px; + margin: 0; + cursor: pointer; + + &::before { + content: ''; + position: absolute; + top: -1.5px; + left: -1.5px; + width: 25px; + height: 25px; + border-radius: 50%; + background-color: ${({ theme }) => theme.colors.white}; + border: 1.5px solid ${({ theme }) => theme.colors.pink3}; + transition: left 250ms linear; + } + + &:checked { + background: ${({ theme }) => theme.colors.gradient}; + border: 1.5px solid ${({ theme }) => theme.colors.gradient}; + } + + &:checked::before { + background-color: white; + left: 25px; + } +`; diff --git a/src/pages/PostUpload/dto.ts b/src/pages/PostUpload/dto.ts new file mode 100644 index 00000000..743ad97f --- /dev/null +++ b/src/pages/PostUpload/dto.ts @@ -0,0 +1,21 @@ +import { ClothingInfo } from '../../components/ClothingInfoItem/dto'; +import { PostImage } from '../../apis/post/dto'; + +export interface PostUploadModalProps { + postId?: number | null; +} + +export interface ImageSwiperProps { + images: PostImage[]; +} + +export interface SearchBottomSheetProps { + onClose: () => void; + onSelectClothingInfo: (clothingInfo: ClothingInfo) => void; +} + +export interface ToggleSwitchProps { + checked: boolean; + onChange: () => void; + disabled?: boolean; +} diff --git a/src/pages/PostUpload/index.tsx b/src/pages/PostUpload/index.tsx new file mode 100644 index 00000000..931b6d4a --- /dev/null +++ b/src/pages/PostUpload/index.tsx @@ -0,0 +1,400 @@ +//PostUploadModal/index.tsx +import React, { useState, useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { useRecoilState } from 'recoil'; +import { + postImagesAtom, + postContentAtom, + postClothingInfosAtom, + postStyletagAtom, + postIsRepresentativeAtom, + modeAtom, +} from '../../recoil/PostUpload/PostUploadAtom'; + +import { + UploadContainer, + Content, + StyledInput, + TagContainer, + ClothingInfoList, + StyletagList, + 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); + const [content, setContent] = useRecoilState(postContentAtom); + const [clothingInfos, setClothingInfos] = useRecoilState(postClothingInfosAtom); + const [selectedStyletag, setSelectedStyletag] = useRecoilState(postStyletagAtom); + const [isRepresentative, setIsRepresentative] = useRecoilState(postIsRepresentativeAtom); + const [mode, setMode] = useRecoilState(modeAtom); + const [isSearchBottomSheetOpen, setIsSearchBottomSheetOpen] = useState(false); + const [isStyletagListOpen, setIsStyletagListOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); + const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.'); + const location = useLocation(); + const navigate = useNavigate(); + + const styletags = [ + 'classic', + 'street', + 'hip', + 'casual', + 'sporty', + 'feminine', + 'minimal', + 'formal', + 'outdoor', + 'luxury', + ]; + + // 게시물 업로드인지 수정인지 모드 확인 + useEffect(() => { + const handleMode = async () => { + const state = location.state as { mode?: string; postId?: number }; + if (state?.mode) { + setMode(state.mode); // 모드 상태를 설정 + } + if (state.mode === 'edit' && state?.postId && selectedImages.length === 0) { + await getPost(state.postId); + } + }; + + handleMode(); + }, []); + + const handlePrev = () => { + const state = location.state as { mode?: string; postId?: number }; + if (mode === 'edit') { + setMode('edit2'); + } + navigate('/image-select', { state: { mode: mode, postId: state.postId } }); + }; + + const getPost = async (postId: number) => { + setIsLoading(true); + + try { + const response = await getPostDetailApi(postId); + + const { postImages, content, postStyletags, postClothings, isRepresentative } = response.data; + + setSelectedImages(postImages); + setContent(content); + setClothingInfos(postClothings ?? []); + setSelectedStyletag(postStyletags); + setIsRepresentative(isRepresentative); + } catch (error) { + const errorMessage = handleError(error, 'post'); + setModalContent(errorMessage); + setIsStatusModalOpen(true); + } finally { + setIsLoading(false); + } + }; + + const handleToggleSearchSheet = () => { + setIsSearchBottomSheetOpen((open) => !open); + }; + + const handleToggleStyleTagList = () => { + setIsStyletagListOpen((open) => !open); + }; + + const handleAddClothingInfo = (newClothingInfo: ClothingInfo) => { + setClothingInfos((clothingInfos) => { + // 중복 확인 (새로운 의류 정보가 이미 존재하지 않을 경우 추가) + const isDuplicate = clothingInfos.some( + (info) => info.modelName === newClothingInfo.modelName && info.brandName === newClothingInfo.brandName, + ); + if (!isDuplicate) { + return [...clothingInfos, newClothingInfo]; + } else { + return clothingInfos; // 중복이면 기존 리스트 그대로 반환 + } + }); + }; + + const handleDeleteClothingInfo = (deleteClothingInfo: ClothingInfo) => { + const deletedClothingInfo = clothingInfos.filter((clothing) => clothing !== deleteClothingInfo); + setClothingInfos(deletedClothingInfo); + }; + + const bottomSheetProps: BottomSheetProps = { + isOpenBottomSheet: isSearchBottomSheetOpen, + isHandlerVisible: false, + Component: SearchBottomSheetContent, + onCloseBottomSheet: () => { + setIsSearchBottomSheetOpen(false); + }, + componentProps: { + onClose: () => setIsSearchBottomSheetOpen(false), + onSelectClothingInfo: handleAddClothingInfo, + }, + }; + + const handleSelectStyletag = (tag: string) => { + setSelectedStyletag((prev) => { + // 선택된 태그가 이미 존재하면 제거 + if (prev.includes(tag)) { + return prev.filter((t) => t !== tag); + } + // 선택된 태그 추가 + return [...prev, tag]; + }); + setIsStyletagListOpen(false); + }; + + const handleToggleIsRepresentative = () => { + setIsRepresentative(!isRepresentative); + }; + + const cropImage = (imageUrl: string): Promise => { + return new Promise((resolve) => { + const img = new Image(); + img.src = imageUrl; + img.onload = () => { + const aspectRatio = 4 / 5; + let width = img.width; + let height = img.height; + let startX = 0; + let startY = 0; + + // 이미지의 비율이 원하는 비율과 다른 경우, 자르기 + if (width / height > aspectRatio) { + // 이미지가 더 넓은 경우, 좌우를 잘라냄 + width = height * aspectRatio; + startX = (img.width - width) / 2; // 좌우 균등하게 자름 + } else { + // 이미지가 더 높은 경우, 상하를 잘라냄 + height = width / aspectRatio; + startY = (img.height - height) / 2; // 상하 균등하게 자름 + } + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(img, startX, startY, width, height, 0, 0, width, height); + } + + // Blob으로 변환 + canvas.toBlob((blob) => { + if (blob) resolve(blob); + }, 'image/jpeg'); + }; + }); + }; + + const uploadImageToFirebase = async (imageUrl: string) => { + //Firebase URL 형식인지 확인 + if (imageUrl.startsWith('https://firebasestorage.googleapis.com/')) { + return imageUrl; // 이미 업로드된 경우, URL을 그대로 반환 + } + + // 새로 업로드해야 하는 경우 + const croppedBlob = await cropImage(imageUrl); + + const storageRef = ref(storage, `ootd/images/${Date.now()}`); + await uploadBytes(storageRef, croppedBlob) + .then(() => { + console.log('success'); + }) + .catch((error) => { + console.log(JSON.stringify(error)); + }); + + return getDownloadURL(storageRef); + }; + + const handleSubmit = async () => { + if (selectedStyletag.length === 0) { + setModalContent('*스타일 태그를 지정해주세요*'); + setIsStatusModalOpen(true); + return; + } + + setIsLoading(true); + + try { + // 업로드된 이미지 URL과 함께 orderNum을 추가 + const uploadedImages = await Promise.all( + selectedImages.map(async (image, index) => { + const imageUrl = await uploadImageToFirebase(image.imageUrl); + return { imageUrl: imageUrl, orderNum: index + 1 }; // orderNum 추가 + }), + ); + + const postData: PostBase = { + postImages: uploadedImages, + content, + postStyletags: selectedStyletag || [], + postClothings: clothingInfos, + isRepresentative: isRepresentative, + }; + + let response; + if (mode === 'edit' || mode === 'edit2') { + // 게시물 수정 (PATCH) + const state = location.state as { mode: string; postId: number }; + response = await modifyPostApi(state.postId, postData); + } else { + // 새 게시물 업로드 (POST) + response = await createPostApi(postData); + } + console.log(response); + + //초기화 + setSelectedImages([]); + setClothingInfos([]); + setContent(''); + setIsRepresentative(false); + setSelectedStyletag([]); + setMode(''); + + navigate('/mypage'); + } catch (error) { + const errorMessage = handleError(error, 'post'); + setModalContent(errorMessage); + setIsStatusModalOpen(true); + } finally { + setIsLoading(false); + } + }; + + // api 처리 상태 모달 (성공/실패) + const statusModalProps: ModalProps = { + content: modalContent, + onClose: () => { + setIsStatusModalOpen(false); + }, + }; + + return ( + + + + + + setContent(e.target.value)} + placeholder="문구를 작성하세요..." + /> + +
+ + + 옷 정보 태그 + + {clothingInfos.length > 0 && ( + + {clothingInfos.length} + + )} + +
+ {clothingInfos.length > 0 && ( + + {clothingInfos.map((clothingObj, index) => ( + + ))} + + )} +
+ +
+ + + 스타일 태그 + + {isStyletagListOpen ? ( + + ) : selectedStyletag.length === 0 ? ( + <> + + 미지정 + + + + ) : ( + + + #{selectedStyletag[0]} + + + )} +
+ {isStyletagListOpen && ( + + {styletags.map((tag) => ( + handleSelectStyletag(tag)} + selected={selectedStyletag[0] === tag} + > + + #{tag} + + + ))} + + )} +
+ + + 대표 OOTD 지정 +
+ +
+
+
+ + + + +
+ {isStatusModalOpen && } +
+ ); +}; + +export default PostUpload; diff --git a/src/pages/Upload/PostUploadModal/styles.tsx b/src/pages/PostUpload/styles.tsx similarity index 85% rename from src/pages/Upload/PostUploadModal/styles.tsx rename to src/pages/PostUpload/styles.tsx index d47d5646..c1c9b679 100644 --- a/src/pages/Upload/PostUploadModal/styles.tsx +++ b/src/pages/PostUpload/styles.tsx @@ -1,5 +1,12 @@ import styled from 'styled-components'; +export const UploadContainer = styled.div` + flex-grow: 1; + height: 100vh; + width: 100%; + position: relative; +`; + export const Content = styled.div` display: flex; flex-direction: column; @@ -7,8 +14,10 @@ export const Content = styled.div` top: 2.75rem; left: 0; width: 100%; - height: calc(100% - 10rem); - overflow-y: auto; + //max-width: 512px; + height: calc(100% - 44px); + padding-bottom: 100px; + overflow-y: scroll; scrollbar-width: none; // Firefox -ms-overflow-style: none; // IE 10+ @@ -19,9 +28,9 @@ export const Content = styled.div` export const StyledInput = styled.textarea` width: calc(100% - 2.5rem); - min-height: 3.125rem; + min-height: 100px; max-height: 5rem; - margin: 1.25rem; + margin: 0 1.25rem; border: none; resize: none; overflow-y: scroll; @@ -152,12 +161,14 @@ export const StyletagItem = styled.span<{ selected: boolean }>` padding: 0 0.65rem; height: 2rem; //min-width: 4.375rem; - background-color: ${({ color }) => color}; - color: black; - border: ${({ selected }) => (selected ? `solid 0.0625rem black` : 'none')}; - border-radius: 0.3125rem; - //box-shadow: ${({ selected }) => (selected ? `0rem 0rem 0.5rem rgba(0, 0, 0, 0.2)` : 'none')}; + background: ${({ selected, theme }) => (selected ? theme.colors.gradient : 'none')}; + border: 1px solid ${({ theme }) => theme.colors.pink2}; + border-radius: 8px; cursor: pointer; + + .tag { + color: ${({ selected, theme }) => (selected ? theme.colors.white : theme.colors.pink3)}; + } `; export const PinnedPostToggleContainer = styled.label` diff --git a/src/pages/ProfileEdit/dto.tsx b/src/pages/ProfileEdit/dto.tsx index 53401bc6..103a8e2c 100644 --- a/src/pages/ProfileEdit/dto.tsx +++ b/src/pages/ProfileEdit/dto.tsx @@ -15,3 +15,25 @@ export interface ApiResponse { message: string; result: T; } + +export interface UserResponse { + id: number; + name: string; + email: string; + nickname: string | null; + phoneNumber: string | null; + profilePictureUrl: string; + bio: string | null; + joinedAt: string; +} + +export interface UserResponse { + id: number; + name: string; + email: string; + nickname: string | null; + phoneNumber: string | null; + profilePictureUrl: string; + bio: string | null; + joinedAt: string; +} diff --git a/src/pages/ProfileEdit/index.tsx b/src/pages/ProfileEdit/index.tsx index 89f9c5b9..595f725e 100644 --- a/src/pages/ProfileEdit/index.tsx +++ b/src/pages/ProfileEdit/index.tsx @@ -1,26 +1,42 @@ import React, { useRef, useEffect, useState } from 'react'; -import { ProfileEditContainer, ProfilePic, ProfilePicWrapper, Input, Button, Row, FileInput } from './styles'; +import { + ProfileEditContainer, + ProfilePic, + ProfilePicWrapper, + Input, + Button, + Row, + FileInput, + CameraIcon, + UserInfo, + Username, +} from './styles'; import { StyledText } from '../../components/Text/StyledText'; import theme from '../../styles/theme'; import { OODDFrame } from '../../components/Frame/Frame'; -import request from '../../apis/core'; import { useNavigate } from 'react-router-dom'; import TopBar from '../../components/TopBar'; -import back from '../../assets/back.svg'; -import { BaseResponse } from '../MyPost/dto'; +import back from '../../assets/arrow/left.svg'; import BottomButton from '../../components/BottomButton'; import { UserProfileResponse } from './dto'; -import imageBasic from '../../assets/imageBasic.svg'; +import imageBasic from '../../assets/default/defaultProfile.svg'; import Loading from '../../components/Loading'; +import camera from '../../assets/default/camera.svg'; +import request, { BaseResponse } from '../../apis/core'; +import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; +import { storage } from '../../config/firebaseConfig'; +import { UserResponse } from './dto'; const ProfileEdit: React.FC = () => { + const [user, setUser] = useState(null); const fileInputRef = useRef(null); const [userProfile, setUserProfile] = useState(null); const [nickname, setNickname] = useState(''); const [bio, setBio] = useState(''); const [profilePictureUrl, setProfilePictureUrl] = useState(null); const navigate = useNavigate(); + const [uploading, setUploading] = useState(false); // 업로드 상태 관리 useEffect(() => { const fetchUserProfile = async () => { @@ -49,12 +65,42 @@ const ProfileEdit: React.FC = () => { fileInputRef.current?.click(); }; - const handleFileChange = (event: React.ChangeEvent) => { + // 사용자 정보 가져오기 함수 + useEffect(() => { + const fetchUserData = async () => { + try { + const storedUserId = localStorage.getItem('id'); + + if (!storedUserId) { + console.error('User is not logged in'); + return; + } + + const response = await request.get>(`/users/${storedUserId}`); + setUser(response.result); // user 상태에 사용자 정보 설정 (닉네임 포함) + } catch (error) { + console.error('Error fetching user data:', error); + } + }; + + fetchUserData(); // user 데이터를 가져오는 함수 호출 + }, []); + + const handleFileChange = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { - const imageUrl = URL.createObjectURL(file); - setProfilePictureUrl(imageUrl); - console.log('Selected file:', file); + setUploading(true); + try { + const storageRef = ref(storage, `profilePictures/${file.name}`); + await uploadBytes(storageRef, file); // Firebase에 파일 업로드 + const imageUrl = await getDownloadURL(storageRef); // 업로드된 파일의 다운로드 URL 가져오기 + setProfilePictureUrl(imageUrl); + console.log('File uploaded and URL retrieved:', imageUrl); + } catch (error) { + console.error('Error uploading file:', error); + } finally { + setUploading(false); + } } }; @@ -72,7 +118,7 @@ const ProfileEdit: React.FC = () => { bio, }); if (response.isSuccess) { - navigate(`/mypage`); // 마이페이지로 이동 + navigate(`/mypage`); } else { alert('프로필 수정에 실패했습니다.'); } @@ -80,28 +126,42 @@ const ProfileEdit: React.FC = () => { console.error('Error updating profile:', error); alert('프로필 수정 중 오류가 발생했습니다.'); } + { + uploading ? ( + + ) : ( + + ); + } }; if (!userProfile) { - return ; // 또는 로딩 스피너 등을 사용할 수 있습니다. + return ; // 또는 로딩 스피너 등을 사용할 수 있습니다. } return ( - navigate(-1)} /> + navigate(-1)} /> 프로필 사진 + + + {user?.nickname || '김아무개...'} + + 닉네임 @@ -115,7 +175,7 @@ const ProfileEdit: React.FC = () => { setBio(e.target.value)} /> diff --git a/src/pages/ProfileEdit/styles.tsx b/src/pages/ProfileEdit/styles.tsx index 7cef0026..4ca71d57 100644 --- a/src/pages/ProfileEdit/styles.tsx +++ b/src/pages/ProfileEdit/styles.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const ProfileEditContainer = styled.div` flex-grow: 1; /* flexbox에서 공간을 채우도록 설정 */ width: 100%; - max-width: 600px; + //max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; @@ -14,7 +14,39 @@ export const ProfilePicWrapper = styled.div` display: flex; flex-direction: column; align-items: center; - margin-bottom: 50px; /* 20px */ + margin-bottom: 10px; /* 20px */ + position: relative; +`; + +export const Label = styled.label` + font-size: 0.875rem; /* 14px */ + color: #333; +`; + +export const Input = styled.input` + width: 100%; /* Row의 padding에 맞춰 꽉 채우기 */ + padding: 25px; /* 10px padding */ + margin: 10px 0; /* 위아래 간격 조정 */ + border: 0px; + box-sizing: border-box; + border-radius: 10px; + background-color: #f0f0f0; /* 박스 내부 회색 배경 */ + text-align: left; +`; + +export const Button = styled.button` + position: absolute; + bottom: 10px; + right: 10px; + z-index: 1; + width: 1.7rem; + 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; /* 아이콘 테두리 */ + cursor: pointer; `; export const ProfilePic = styled.div` @@ -33,35 +65,15 @@ export const ProfilePic = styled.div` } `; -export const Label = styled.label` - font-size: 0.875rem; /* 14px */ - color: #333; -`; - -export const Input = styled.input` - width: calc(100% - 6.25rem - 10px); /* 라벨과 간격을 제외한 너비 */ - padding: 0.5rem; /* 8px */ - margin-left: 10px; - border: 1px solid #ccc; - border: 0px; - box-sizing: border-box; - padding: 10px; -`; - -export const Button = styled.button` - text-decoration: underline; - margin-top: 0.8rem; - background: none; - border: none; - cursor: pointer; -`; +export const CameraIcon = styled.img``; export const Row = styled.div` display: flex; - align-items: center; + flex-direction: column; /* 세로 배치 */ + align-items: stretch; width: 100%; - margin-top: 10px; /* Row 간격 10px */ - padding: 0px 20px; + margin-top: 0px; /* Row 간격 10px */ + padding: 0px 20px; /* 좌우 여백 20px */ ${Label} { width: 6.25rem; /* 100px */ @@ -71,3 +83,19 @@ export const Row = styled.div` export const FileInput = styled.input` display: none; `; + +export const BottomButton = styled.button` + color: #ff2389; +`; + +export const UserInfo = styled.button``; + +export const Username = styled.button` + color: #000; + font-family: Pretendard; + font-size: 22px; + font-style: normal; + font-weight: 700; + line-height: 136.4%; /* 30.008px */ + letter-spacing: -0.427px; +`; diff --git a/src/pages/ProfileViewer/dto.ts b/src/pages/ProfileViewer/MenuItemDto.ts similarity index 60% rename from src/pages/ProfileViewer/dto.ts rename to src/pages/ProfileViewer/MenuItemDto.ts index ab2753b5..5081581e 100644 --- a/src/pages/ProfileViewer/dto.ts +++ b/src/pages/ProfileViewer/MenuItemDto.ts @@ -1,45 +1,6 @@ -import ReportIcon from '../../assets/ProfileViewer/carbon_warning.svg'; -import BlockIcon from '../../assets/ProfileViewer/block.svg'; - -export interface Post { - postId: number; - likes: number; - isRepresentative: boolean; - firstPhoto: string; -} - -export interface UserInfoProps { - id: number; - nickname: string; - bio: string; - userImg?: string; - isFriend?: boolean; - isInterested?: boolean; - postsCount?: number; - likesCount?: number; - posts?: Post[]; - status: 'blank' | 'unblocked' | 'blocked'; -} - -export interface PostItemProps { - post: Post; - isRepresentative: boolean; - firstPhoto: string; -} - -export interface RequestComponentProps { - userId: number; - nickname: string; - setFriend: (visible: boolean) => void; - setIsBottomSheetOpen: (visible: boolean) => void; - handleOpenModal: (message: string) => void; -} - -export interface ReportTextProps { - onCloseBottomSheet: () => void; - setIsInputVisible: (visible: boolean) => void; - handleOpenModal: (message: string) => void; -} +import BlockIcon from '../../assets/default/block.svg'; +import ReportIcon from '../../assets/default/report.svg'; +import { UserInfoProps } from './UserInfoProps'; export const mainMenuItems = ( userDetails: UserInfoProps, diff --git a/src/pages/ProfileViewer/ResponseDto/BlockDto.ts b/src/pages/ProfileViewer/ResponseDto/BlockDto.ts deleted file mode 100644 index 4fdf6b58..00000000 --- a/src/pages/ProfileViewer/ResponseDto/BlockDto.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface BlockDto { - message: string; -} diff --git a/src/pages/ProfileViewer/ResponseDto/PostListDto.ts b/src/pages/ProfileViewer/ResponseDto/GetPostListResult.ts similarity index 80% rename from src/pages/ProfileViewer/ResponseDto/PostListDto.ts rename to src/pages/ProfileViewer/ResponseDto/GetPostListResult.ts index f5e448dd..8601292e 100644 --- a/src/pages/ProfileViewer/ResponseDto/PostListDto.ts +++ b/src/pages/ProfileViewer/ResponseDto/GetPostListResult.ts @@ -1,10 +1,11 @@ -export interface PostListDto { +export interface GetPostListResult { isSuccess: boolean; code: number; message: string; result: { totalPosts: number; totalLikes: number; + commentsCount: number; posts: [ { postId: number; diff --git a/src/pages/ProfileViewer/ResponseDto/UserInfoDto.ts b/src/pages/ProfileViewer/ResponseDto/GetUserInfoResult.ts similarity index 86% rename from src/pages/ProfileViewer/ResponseDto/UserInfoDto.ts rename to src/pages/ProfileViewer/ResponseDto/GetUserInfoResult.ts index 59a05cc5..2545c189 100644 --- a/src/pages/ProfileViewer/ResponseDto/UserInfoDto.ts +++ b/src/pages/ProfileViewer/ResponseDto/GetUserInfoResult.ts @@ -1,4 +1,4 @@ -export interface UserInfoDto { +export interface GetUserInfoResult { isSuccess: boolean; code: number; message: string; diff --git a/src/pages/ProfileViewer/components/RequestComponent/ResponseDto.ts b/src/pages/ProfileViewer/ResponseDto/PostFriendRequestResult.ts similarity index 89% rename from src/pages/ProfileViewer/components/RequestComponent/ResponseDto.ts rename to src/pages/ProfileViewer/ResponseDto/PostFriendRequestResult.ts index 7a652242..bc566d1a 100644 --- a/src/pages/ProfileViewer/components/RequestComponent/ResponseDto.ts +++ b/src/pages/ProfileViewer/ResponseDto/PostFriendRequestResult.ts @@ -1,4 +1,4 @@ -export interface ResponseDto { +export interface PostFriendRequestResult { isSuccess: boolean; code: number; message: string; diff --git a/src/pages/ProfileViewer/ResponseDto/PostUserBlockResult.ts b/src/pages/ProfileViewer/ResponseDto/PostUserBlockResult.ts new file mode 100644 index 00000000..d90d9bc9 --- /dev/null +++ b/src/pages/ProfileViewer/ResponseDto/PostUserBlockResult.ts @@ -0,0 +1,3 @@ +export interface PostUserBlock { + message: string; +} diff --git a/src/pages/ProfileViewer/UserInfoProps.ts b/src/pages/ProfileViewer/UserInfoProps.ts new file mode 100644 index 00000000..0638e631 --- /dev/null +++ b/src/pages/ProfileViewer/UserInfoProps.ts @@ -0,0 +1,15 @@ +import { Post } from '../../components/PostItem/dto'; + +export interface UserInfoProps { + id: number; + nickname: string; + bio: string; + userImg?: string; + isFriend?: boolean; + isInterested?: boolean; + commentsCount?: number; + postsCount?: number; + likesCount?: number; + posts?: Post[]; + status: 'blank' | 'unblocked' | 'blocked'; +} diff --git a/src/pages/ProfileViewer/components/PostItem/index.tsx b/src/pages/ProfileViewer/components/PostItem/index.tsx deleted file mode 100644 index 47420486..00000000 --- a/src/pages/ProfileViewer/components/PostItem/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { StyledText } from '../../../../components/Text/StyledText'; -import theme from '../../../../styles/theme'; -import { PostItemContainer, PostImageContainer, PostImage, LikesCount, HeartIcon, LikesOverlay, PinSvg } from './style'; -import HeartSvg from '../../../../assets/ProfileViewer/heart.svg'; -import PinIcon from '../../../../assets/ProfileViewer/Group 87.svg'; -import { PostItemProps } from '../../dto'; - -const PostItem: React.FC = ({ post, isRepresentative, firstPhoto }) => { - const navigate = useNavigate(); - const imageUrl = firstPhoto || 'https://via.placeholder.com/72'; - const handleClick = () => { - navigate(`/post/${post.postId}`); - }; - return ( - - - - {isRepresentative && } - - - - - {post.likes} - - - - - - ); -}; - -export default PostItem; diff --git a/src/pages/ProfileViewer/components/ReportText/ReportTextProps.ts b/src/pages/ProfileViewer/components/ReportText/ReportTextProps.ts new file mode 100644 index 00000000..2f8df7a6 --- /dev/null +++ b/src/pages/ProfileViewer/components/ReportText/ReportTextProps.ts @@ -0,0 +1,5 @@ +export interface ReportTextProps { + onCloseBottomSheet: () => void; + setIsInputVisible: (visible: boolean) => void; + handleModalOpen: (message: string) => void; +} diff --git a/src/pages/ProfileViewer/components/ReportText/index.tsx b/src/pages/ProfileViewer/components/ReportText/index.tsx index a5cb0783..e1c0627c 100644 --- a/src/pages/ProfileViewer/components/ReportText/index.tsx +++ b/src/pages/ProfileViewer/components/ReportText/index.tsx @@ -2,13 +2,13 @@ import React, { useState } from 'react'; import { Textarea, ReportButton, ReportTextLayout } from './style'; import { StyledText } from '../../../../components/Text/StyledText'; import theme from '../../../../styles/theme'; -import { ReportTextProps } from '../../dto'; +import { ReportTextProps } from './ReportTextProps'; import request from '../../../../apis/core'; import { useParams } from 'react-router-dom'; -const ReportText: React.FC = ({ onCloseBottomSheet, setIsInputVisible, handleOpenModal }) => { +const ReportText: React.FC = ({ onCloseBottomSheet, setIsInputVisible, handleModalOpen }) => { const [inputValue, setInputValue] = useState(''); - const myid = localStorage.getItem('id'); + const LocalGetId = localStorage.getItem('id'); const { userId } = useParams<{ userId: string }>(); const userDetail = JSON.parse(localStorage.getItem(`userDetails_${userId}`) || '{}'); @@ -16,27 +16,26 @@ const ReportText: React.FC = ({ onCloseBottomSheet, setIsInputV setInputValue(event.target.value); }; - const handleReportSubmit = () => { - console.log('Reported with input:', inputValue); - Report(inputValue); - setInputValue(''); - onCloseBottomSheet(); // 바텀 시트 닫기 - setIsInputVisible(false); // 입력창 숨기기 - }; - - const Report = async (inputValue: string) => { + const postUserReport = async (inputValue: string) => { try { await request.patch(`/user-report`, { - fromUserId: Number.parseInt(myid as string), + fromUserId: Number.parseInt(LocalGetId as string), toUserId: Number.parseInt(userId as string), reason: inputValue, }); - handleOpenModal(`${userDetail.nickname}님을 \n'${inputValue}' 사유로 신고했어요.`); + handleModalOpen(`${userDetail.nickname}님을 \n'${inputValue}' 사유로 신고했어요.`); } catch (error) { console.error('Failed to fetch user details', error); } }; + const handleReportSubmit = () => { + console.log('Reported with input:', inputValue); + postUserReport(inputValue); + setInputValue(''); + onCloseBottomSheet(); // 바텀 시트 닫기 + setIsInputVisible(false); // 입력창 숨기기 + }; return (