diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d599c249..a25a2b3b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,17 +1,109 @@ module.exports = { root: true, - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/typescript', + 'plugin:import/recommended', + 'plugin:prettier/recommended', + ], parser: '@typescript-eslint/parser', - parserOptions: { project: ['./tsconfig.json'] }, - plugins: ['@typescript-eslint'], + parserOptions: { + project: ['./tsconfig.app.json', './tsconfig.node.json'], + }, + plugins: ['@typescript-eslint', 'eslint-plugin-import'], + settings: { + 'import/resolver': { + typescript: { + project: ['./tsconfig.app.json', './tsconfig.node.json'], + }, + }, + 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] }, + }, + env: { + node: true, + }, rules: { - '@typescript-eslint/strict-boolean-expressions': [ - 2, + 'prettier/prettier': ['error', { endOfLine: 'auto' }], + 'import/extensions': [ + 'error', + 'ignorePackages', { - allowString: false, - allowNumber: false, + ts: 'never', + tsx: 'never', + }, + ], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'index', 'type', 'parent', 'sibling', 'object'], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + 'newlines-between': 'always', + pathGroups: [ + { + pattern: 'react*', + group: 'builtin', + position: 'before', + }, + { + pattern: '@components/**/dto', + group: 'type', + }, + { + pattern: '@styles/**', + group: 'internal', + position: 'before', + }, + { + pattern: '@assets/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@components/Icons/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@components/**', + group: 'internal', + position: 'after', + }, + { + pattern: '../**/dto', + group: 'type', + position: 'after', + }, + { + pattern: './**/dto', + group: 'type', + position: 'after', + }, + { + pattern: '../**/index', + group: 'parent', + position: 'before', + }, + { + pattern: './**/index', + group: 'parent', + position: 'before', + }, + ], + pathGroupsExcludedImportTypes: ['react*'], }, ], }, - ignorePatterns: ['src/**/*.test.ts', 'src/frontend/generated/*'], + overrides: [ + { + files: ['*.ts', '*.tsx'], + rules: { + 'import/order': 'error', + }, + }, + ], + ignorePatterns: ['src/**/*.test.ts', 'src/frontend/generated/*', 'src/App.tsx'], }; diff --git a/.prettierrc b/.prettierrc index 6214cb00..5f866e68 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,9 +1,9 @@ { - "trailingComma": "all", - "tabWidth": 2, - "semi": true, - "singleQuote": true, - "printWidth": 120, - "arrowParens": "always", - "useTabs": true + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 120, + "arrowParens": "always", + "useTabs": true } diff --git a/package.json b/package.json index 6d4869c4..099fc075 100644 --- a/package.json +++ b/package.json @@ -14,45 +14,38 @@ "dependencies": { "@types/styled-components": "^5.1.34", "axios": "^1.7.2", - "body-parser": "^1.20.2", - "cors": "^2.8.5", "dayjs": "^1.11.12", - "express": "^4.19.2", "firebase": "^10.13.0", "heic2any": "^0.0.4", - "js-cookie": "^3.0.5", + "lodash": "^4.17.21", "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" }, "devDependencies": { - "@types/express": "^4.17.21", - "@types/js-cookie": "^3.0.6", + "@types/lodash": "^4.17.13", "@types/node": "^20.14.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.13.1", + "@typescript-eslint/eslint-plugin": "^8.18.1", + "@typescript-eslint/parser": "^8.18.1", "@vitejs/plugin-react": "^4.3.1", - "eslint": "^9.7.0", + "eslint": "^8.0.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.7", - "json-server": "^1.0.0-beta.1", "prettier": "^3.3.2", - "typescript": "^5.2.2", - "vite": "^5.3.1" - }, - "proxy": "https://localhost:3001" + "typescript": "^5.7.2", + "vite": "^6.0.3" + } } diff --git a/src/App.tsx b/src/App.tsx index a73e476a..e2069bac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,31 @@ import React from 'react'; import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'; -import Home from './pages/Home'; -import Login from './pages/Login'; -import SignUp from './pages/SignUp'; -import LoginComplete from './pages/Login/components/LoginComplete'; -import TermsAgreement from './pages/TermsAgreement'; -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 Home from '@pages/Home'; +import Login from '@pages/Login'; +import LoginComplete from '@pages/Login/LoginComplete'; -import ProfileViewer from './pages/ProfileViewer'; +import SignUp from '@pages/SignUp'; +import TermsAgreement from '@pages/SignUp/TermsAgreement'; -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 Profile from '@pages/Profile'; +import ProfileEdit from '@pages/Profile/ProfileEdit'; -import Chats from './pages/Chats'; -import ChatRoom from './pages/Chats/ChatRoom'; +import AccountSetting from '@pages/Account/AccountSetting'; +import AccountEdit from '@pages/Account/AccountEdit'; +import AccountCancel from '@pages/Account/AccountCancel'; +import Verification from '@pages/Account/Verification'; -import NotFound from './pages/NotFound'; +import Post from '@pages/Post'; +import PostUpload from '@pages/Post/PostUpload'; +import PostImageSelect from '@pages/Post/PostImageSelect'; +import PostInstaConnect from '@pages/Post/PostInstaConnect'; +import PostInstaFeedSelect from '@pages/Post/PostInstaFeedSelect'; + +import Chats from '@pages/Chats'; +import ChatRoom from '@pages/Chats/ChatRoom'; + +import NotFound from '@pages/NotFound'; const ProtectedRoute = ({ children }: { children: JSX.Element }) => { const isAuthenticated = Boolean(localStorage.getItem('new_jwt_token')); @@ -36,21 +36,21 @@ const ProtectedRoute = ({ children }: { children: JSX.Element }) => { const protectedRoutes = [ { path: '/', element: }, - // 사용자 프로필 및 계정 관리 - { path: '/mypage', element: }, + // profile + { path: '/profile/:userId', element: }, { path: '/profile/edit', element: }, - { path: '/account-setting', element: }, - { path: '/account-edit', element: }, - { path: '/account-cancel', element: }, - { path: '/verification', element: }, - { path: '/users/:userId', element: }, + + // account + { path: '/account/setting', element: }, + { path: '/account/edit', element: }, + { path: '/account/cancel', element: }, + { path: '/account/verification', 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: '/post/upload/photo/select', element: }, + { path: '/post/upload/instagram/connect', element: }, + { path: '/post/upload/instagram/select', element: }, + { path: '/post/upload/content', element: }, // 메시지/채팅 { path: '/chats', element: }, @@ -60,9 +60,10 @@ const protectedRoutes = [ // 인증이 필요 없는 페이지 배열 const publicRoutes = [ { path: '/login', element: }, - { path: '/signup', element: }, { path: '/login/complete', element: }, - { path: '/terms-agreement', element: }, + + { path: '/signup', element: }, + { path: '/signup/terms-agreement', element: }, ]; const App: React.FC = () => { diff --git a/src/apis/auth/dto.ts b/src/apis/auth/dto.ts index 969a3b91..e0490291 100644 --- a/src/apis/auth/dto.ts +++ b/src/apis/auth/dto.ts @@ -1,14 +1,15 @@ -import { BaseSuccessResponse } from '../core/dto'; +import type { BaseSuccessResponse } from '@apis/core/dto'; // jwt를 이용한 사용자 정보 조회 응답 export type getUserInfoByJwtResponse = BaseSuccessResponse; // jwt를 이용한 사용자 정보 조회 응답 데이터 export interface getUserInfoByJwtData { - userId: number; + id: number; name: string; phoneNumber: string; email: string; nickname: string; profilePictureUrl: string; bio: string; + birthDate: string; } diff --git a/src/apis/auth/index.ts b/src/apis/auth/index.ts index fea044da..dec241ca 100644 --- a/src/apis/auth/index.ts +++ b/src/apis/auth/index.ts @@ -1,5 +1,5 @@ -import { getUserInfoByJwtResponse } from './dto'; -import { newRequest } from '../core'; +import { newRequest } from '@apis/core'; +import type { getUserInfoByJwtResponse } from './dto'; // 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 deleted file mode 100644 index e69de29b..00000000 diff --git a/src/apis/chatting/dto.ts b/src/apis/chatting/dto.ts index 5734abc2..ee2dc1e8 100644 --- a/src/apis/chatting/dto.ts +++ b/src/apis/chatting/dto.ts @@ -2,7 +2,7 @@ // base response 형태를 따르지 않으므로 data 접미사를 사용했습니다. // response export interface ChatRoomData { - chatRoomId: number; + id: number; otherUser: OtherUserDto; latestMessage: LatestMessageDto; } @@ -10,7 +10,7 @@ export interface ChatRoomData { export interface OtherUserDto { id: number; nickname: string; - profileUrl: string; + profilePictureUrl: string; } export interface LatestMessageDto { diff --git a/src/apis/core/index.ts b/src/apis/core/index.ts index 3a66718e..a62602ba 100644 --- a/src/apis/core/index.ts +++ b/src/apis/core/index.ts @@ -5,7 +5,7 @@ import axios, { AxiosResponse, InternalAxiosRequestConfig, } from 'axios'; -import { JWT_KEY, NEW_JWT_KEY } from '../../config/constant'; +import { NEW_JWT_KEY } from '../../config/constant'; // 기존 서버 응답 타입 export type BaseResponse = { @@ -71,40 +71,3 @@ newRequest.interceptors.response.use( return Promise.reject(error); }, ); - -// 기존 서버 axios 인스턴스 -export const request: CustomInstance = axios.create({ - baseURL: import.meta.env.VITE_API_URL, - timeout: 20000, - headers: { - accept: 'application/json', - Authorization: `Bearer ${localStorage.getItem(JWT_KEY)}`, - }, -}); - -request.interceptors.request.use( - (config) => { - const jwt = window.localStorage.getItem(JWT_KEY); - config.headers.Authorization = `Bearer ${jwt}`; - return config; - }, - (error) => { - return Promise.reject(error); - }, -); - -request.interceptors.response.use( - (response) => { - console.log('network log', response); - if (response.status === 200 || response.status === 201) { - return response.data; - } else { - return Promise.reject(response.data.message); - } - }, - (error) => { - return Promise.reject(error.code === 'ERR_NETWORK' ? '허용되지 않은 네트워크 접근입니다.' : error); - }, -); - -export default request; diff --git a/src/apis/matching/dto.ts b/src/apis/matching/dto.ts index 3a79b40b..ccae2fc6 100644 --- a/src/apis/matching/dto.ts +++ b/src/apis/matching/dto.ts @@ -1,4 +1,4 @@ -import { BaseSuccessResponse } from '../core/dto'; +import type { BaseSuccessResponse } from '@apis/core/dto'; // 매칭 요청 // request @@ -12,9 +12,10 @@ export interface CreateMatchingRequest { export type CreateMatchingResponse = BaseSuccessResponse; export interface CreateMatchingData { + id: number; // matchingId chatRoomId: number; - fromUserId: number; - toUserId: number; + requesterId: number; + targetId: number; } // 매칭 리스트 조회 @@ -22,26 +23,26 @@ export interface CreateMatchingData { export type GetMatchingListResponse = BaseSuccessResponse; export interface GetMatchingListData { - isMatching: boolean; // 매칭 요청 존재 여부 - matchingsCount: number; // 매칭 요청 개수 + hasMatching: boolean; + matchingsCount: number; matching: MatchingDto[]; } export interface MatchingDto { - matchingId: number; + id: number; // matchingId requester: RequesterDto; - requesterPost: RequesterPostDto; } export interface RequesterDto { - requesterId: number; + id: number; // requesterId nickname: string; profilePictureUrl: string; + representativePost: RepresentativePost; } -export interface RequesterPostDto { - postImages: PostImageDto[]; // 대표 게시글 이미지 - styleTags: string[]; // 게시글 스타일 태그 +export interface RepresentativePost { + postImages: PostImageDto[]; + styleTags: string[]; } export interface PostImageDto { @@ -59,7 +60,7 @@ export interface ModifyMatchingStatusRequest { export type ModifyMatchingStatusResponse = BaseSuccessResponse; export interface ModifyMatchingStatusData { - matchingId: number; + id: number; // matchingId requesterId: number; targetId: number; requestStatus: string; diff --git a/src/apis/matching/index.ts b/src/apis/matching/index.ts index 62e2b595..39194b3b 100644 --- a/src/apis/matching/index.ts +++ b/src/apis/matching/index.ts @@ -1,5 +1,5 @@ -import { newRequest } from '../core'; -import { +import { newRequest } from '@apis/core'; +import type { CreateMatchingRequest, CreateMatchingResponse, GetMatchingListResponse, diff --git a/src/apis/post/dto.ts b/src/apis/post/dto.ts index 68d4c6eb..d3d86a9f 100644 --- a/src/apis/post/dto.ts +++ b/src/apis/post/dto.ts @@ -25,7 +25,7 @@ export interface PostBase { content: string; postImages: PostImage[]; postStyletags: string[]; - postClothings: PostClothing[] | null; + postClothings?: PostClothing[] | null; isRepresentative: boolean; } export interface CreatePostData extends PostBase {} @@ -36,7 +36,7 @@ export interface PostSummary { createdAt: Date; isPostLike: boolean; user: User; - requestStatus: boolean; + requestStatus: boolean | null; } export interface GetPostListData { post: PostSummary[]; @@ -60,7 +60,7 @@ export interface GetUserPostListData { meta: PaginationMeta; } export interface ModifyPostData extends PostBase { - postId: number; + id: number; userId: number; } export interface GetPostDetailData extends PostBase { diff --git a/src/apis/user-block/dto.ts b/src/apis/user-block/dto.ts index ed8a314a..f5ba92bb 100644 --- a/src/apis/user-block/dto.ts +++ b/src/apis/user-block/dto.ts @@ -1,6 +1,6 @@ // 차단/해제 요청 데이터 export interface PostUserBlockRequest { - fromUserId: number; - toUserId: number; + requesterId: number; + targetId: number; action: 'block' | 'unblock'; // 차단 또는 해제 } diff --git a/src/apis/user-block/index.ts b/src/apis/user-block/index.ts index d4c4c6ca..2faee8a2 100644 --- a/src/apis/user-block/index.ts +++ b/src/apis/user-block/index.ts @@ -1,6 +1,6 @@ -import { PostUserBlockRequest } from './dto'; -import { EmptySuccessResponse } from '../core/dto'; -import { newRequest } from '../core'; +import { newRequest } from '@apis/core'; +import type { EmptySuccessResponse } from '@apis/core/dto'; +import type { PostUserBlockRequest } from './dto'; // 유저 차단 api export const postUserBlockApi = (data: PostUserBlockRequest) => diff --git a/src/apis/user-report/dto.ts b/src/apis/user-report/dto.ts index dcf4ad7f..6c6add04 100644 --- a/src/apis/user-report/dto.ts +++ b/src/apis/user-report/dto.ts @@ -1,6 +1,6 @@ // 사용자 신고 요청 데이터 export interface PostUserReportRequest { - fromUserId: number; - toUserId: number; + requesterId: number; + targetId: number; reason: string; } diff --git a/src/apis/user-report/index.ts b/src/apis/user-report/index.ts index 2983fdf0..207f70a1 100644 --- a/src/apis/user-report/index.ts +++ b/src/apis/user-report/index.ts @@ -1,6 +1,6 @@ -import { PostUserReportRequest } from './dto'; -import { EmptySuccessResponse } from '../core/dto'; -import { newRequest } from '../core'; +import { newRequest } from '@apis/core'; +import type { EmptySuccessResponse } from '@apis/core/dto'; +import type { PostUserReportRequest } from './dto'; // 유저 신고 api export const postUserReportApi = (data: PostUserReportRequest) => diff --git a/src/apis/user/dto.ts b/src/apis/user/dto.ts index 00b20c4c..0cc1d5ff 100644 --- a/src/apis/user/dto.ts +++ b/src/apis/user/dto.ts @@ -1,15 +1,15 @@ -import { BaseSuccessResponse } from '../core/dto'; +import type { BaseSuccessResponse } from '@apis/core/dto'; // 사용자 정보 공통 인터페이스 export interface UserInfoData { - userId: number; + id: number; name: string; phoneNumber: string; email: string; nickname: string; profilePictureUrl: string; bio: string; - birthDate: string; // user 공통 인터페이스에 이 두 개는 안 나와있어서 일단 이것들만 optional 처리했습니다... + birthDate: string; isFriend: boolean; } @@ -37,5 +37,5 @@ export type PatchUserWithDrawResponse = BaseSuccessResponse; // 탈퇴 성공 시 항상 빈 객체가 응답으로 온다면 Record으로 타입 안정성 높일 수 있답니다 + data: Record; // 탈퇴 성공 시 항상 빈 객체가 응답으로 온다면 Record으로 타입 안정성 높일 수 있음 } diff --git a/src/apis/user/index.ts b/src/apis/user/index.ts index 03521c5f..40c61b9e 100644 --- a/src/apis/user/index.ts +++ b/src/apis/user/index.ts @@ -1,9 +1,14 @@ -import { GetUserInfoResponse, PatchUserInfoRequest, PatchUserInfoResponse, PatchUserWithDrawResponse } from './dto'; -import { newRequest } from '../core'; -import { EmptySuccessResponse } from '../core/dto'; +import { newRequest } from '@apis/core'; +import type { EmptySuccessResponse } from '@apis/core/dto'; +import type { + GetUserInfoResponse, + PatchUserInfoRequest, + PatchUserInfoResponse, + PatchUserWithDrawResponse, +} from './dto'; // 유저 정보 수정 api -export const patchUserInfoApi = (data: PatchUserInfoRequest, userId: number) => +export const patchUserInfoApi = (data: Partial, userId: number) => newRequest.patch(`/user/${userId}`, data); // 유저 탈퇴 api diff --git a/src/apis/util/dto.ts b/src/apis/util/dto.ts index ff6003e5..57da4d6c 100644 --- a/src/apis/util/dto.ts +++ b/src/apis/util/dto.ts @@ -18,5 +18,4 @@ export interface PaginationMeta { last_page: number; hasPreviousPage: boolean; hasNextPage: boolean; - totalItems: number; // 추가 } diff --git a/src/apis/util/handleError.ts b/src/apis/util/handleError.ts index d6dcf8f4..712a1b8d 100644 --- a/src/apis/util/handleError.ts +++ b/src/apis/util/handleError.ts @@ -1,4 +1,5 @@ import { AxiosError } from 'axios'; + import { ApiDomain, errorMessages } from './errorMessage'; export const handleError = (error: unknown, domain: ApiDomain = 'default') => { diff --git a/src/assets/arrow/bottom.svg b/src/assets/arrow/bottom.svg deleted file mode 100644 index faa0158c..00000000 --- a/src/assets/arrow/bottom.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/default/delete.svg b/src/assets/default/delete.svg index d610e95f..5f1e9b45 100644 --- a/src/assets/default/delete.svg +++ b/src/assets/default/delete.svg @@ -1,3 +1,3 @@ - - - \ No newline at end of file + + + diff --git a/src/assets/default/edit.svg b/src/assets/default/edit.svg index 90a5d2aa..67b3bee5 100644 --- a/src/assets/default/edit.svg +++ b/src/assets/default/edit.svg @@ -1,10 +1,10 @@ - - - - - - - - - - \ No newline at end of file + + + + + + + + + + diff --git a/src/assets/default/insta.svg b/src/assets/default/insta.svg deleted file mode 100644 index f35d7ec5..00000000 --- a/src/assets/default/insta.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/assets/default/like-white.svg b/src/assets/default/like-white.svg deleted file mode 100644 index b6225850..00000000 --- a/src/assets/default/like-white.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/default/oodd-white.svg b/src/assets/default/oodd-white.svg deleted file mode 100644 index bbee923c..00000000 --- a/src/assets/default/oodd-white.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/assets/default/photo.svg b/src/assets/default/photo.svg deleted file mode 100644 index e24e8825..00000000 --- a/src/assets/default/photo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/default/pin.svg b/src/assets/default/pin.svg index fe23fcc2..6ce911fc 100644 --- a/src/assets/default/pin.svg +++ b/src/assets/default/pin.svg @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/src/components/BottomButton/dto.ts b/src/components/BottomButton/dto.ts index af282ebb..7135a063 100644 --- a/src/components/BottomButton/dto.ts +++ b/src/components/BottomButton/dto.ts @@ -1,5 +1,5 @@ export interface BottomButtonProps { - content: String; + content: string; onClick: () => void; disabled?: boolean; } diff --git a/src/components/BottomButton/index.tsx b/src/components/BottomButton/index.tsx index ae90603e..393e50b8 100644 --- a/src/components/BottomButton/index.tsx +++ b/src/components/BottomButton/index.tsx @@ -1,17 +1,16 @@ -import React from 'react'; +import { StyledText } from '@components/Text/StyledText'; + +import type { BottomButtonProps } from './dto'; + import { ButtonWrapper, Button } from './styles'; -import { StyledText } from '../Text/StyledText'; -import { BottomButtonProps } from './dto'; const BottomButton: React.FC = ({ content, onClick, disabled = false }) => { return ( - <> - - - - + + + ); }; diff --git a/src/components/BottomButton/styles.tsx b/src/components/BottomButton/styles.tsx index d31c235f..7c21acb6 100644 --- a/src/components/BottomButton/styles.tsx +++ b/src/components/BottomButton/styles.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const ButtonWrapper = styled.div` display: flex; @@ -8,15 +8,16 @@ export const ButtonWrapper = styled.div` transform: translateX(-50%); width: 100%; height: 6.25rem; - background-color: ${({ theme }) => theme.colors.white}; + background-color: ${({ theme }) => theme.colors.background.primary}; justify-content: flex-end; z-index: 1; border: none; `; export const Button = styled.button<{ disabled: boolean }>` - background: ${({ disabled, theme }) => - disabled ? 'linear-gradient(93deg, #FFC1D6 1.22%, #F8D4D4 99.73%)' : theme.colors.gradient}; + background: ${( + { disabled, theme }, //theme에 없어서 그냥 뒀음 + ) => (disabled ? 'linear-gradient(93deg, #FFC1D6 1.22%, #F8D4D4 99.73%)' : theme.colors.brand.gradient)}; border-radius: 0.625rem; font-size: 1rem; width: calc(100% - 2.5rem); diff --git a/src/components/BottomSheetMenu/dto.ts b/src/components/BottomSheet/BottomSheetMenu/dto.ts similarity index 79% rename from src/components/BottomSheetMenu/dto.ts rename to src/components/BottomSheet/BottomSheetMenu/dto.ts index 4d2a3dd7..ddaa71f5 100644 --- a/src/components/BottomSheetMenu/dto.ts +++ b/src/components/BottomSheet/BottomSheetMenu/dto.ts @@ -1,12 +1,11 @@ -// SheetItemWithDivider에서 사용되는 Items 배열 요소 -export interface SheetItemDto { - text: string; - action: () => void; - icon?: string; // svg를 import하여 값으로 사용 -} - //TODO: marginBottom prop 제거 export interface BottomSheetMenuProps { items: SheetItemDto[]; marginBottom?: string; } + +export interface SheetItemDto { + text: string; + action: () => void; + icon?: string; // svg를 import하여 값으로 사용 +} diff --git a/src/components/BottomSheet/BottomSheetMenu/index.tsx b/src/components/BottomSheet/BottomSheetMenu/index.tsx new file mode 100644 index 00000000..af0c5bd9 --- /dev/null +++ b/src/components/BottomSheet/BottomSheetMenu/index.tsx @@ -0,0 +1,32 @@ +import { memo } from 'react'; + +import theme from '@styles/theme'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { BottomSheetMenuProps, SheetItemDto } from './dto'; + +import { BottomSheetMenuLayout, SheetItem, IconButton } from './styles'; + +const BottomSheetMenu: React.FC = memo(({ items }) => { + return ( + + {items.map((item: SheetItemDto, index) => ( +
+ + + {item.text} + + {item.icon && ( + + {`${item.text} + + )} + +
+ ))} +
+ ); +}); + +export default BottomSheetMenu; diff --git a/src/components/BottomSheetMenu/styles.tsx b/src/components/BottomSheet/BottomSheetMenu/styles.tsx similarity index 59% rename from src/components/BottomSheetMenu/styles.tsx rename to src/components/BottomSheet/BottomSheetMenu/styles.tsx index 6045d462..d944358b 100644 --- a/src/components/BottomSheetMenu/styles.tsx +++ b/src/components/BottomSheet/BottomSheetMenu/styles.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const BottomSheetMenuLayout = styled.ul` margin-bottom: 0.62rem; @@ -13,10 +13,13 @@ export const SheetItem = styled.li` align-items: center; justify-content: space-between; cursor: pointer; - border-bottom: 1px solid rgb(0, 0, 0, 0.2); + border-bottom: 1px solid ${({ theme }) => theme.colors.gray[300]}; `; -export const Icon = styled.img` +export const IconButton = styled.button` width: 1.5rem; height: 1.5rem; + display: flex; + align-items: center; + justify-content: center; `; diff --git a/src/components/Comment/dto.ts b/src/components/BottomSheet/CommentBottomSheet/Comment/dto.ts similarity index 100% rename from src/components/Comment/dto.ts rename to src/components/BottomSheet/CommentBottomSheet/Comment/dto.ts diff --git a/src/components/Comment/index.tsx b/src/components/BottomSheet/CommentBottomSheet/Comment/index.tsx similarity index 58% rename from src/components/Comment/index.tsx rename to src/components/BottomSheet/CommentBottomSheet/Comment/index.tsx index 54344f1e..4034ccf1 100644 --- a/src/components/Comment/index.tsx +++ b/src/components/BottomSheet/CommentBottomSheet/Comment/index.tsx @@ -1,14 +1,18 @@ -import { StyledText } from '../Text/StyledText'; -import { CommentLayout, SendContainer, CommentTextarea, SendImg } from './styles'; -import Send from '../../assets/default/send-comment.svg'; -import React, { useEffect, useRef, useState } from 'react'; -import { CommentProps } from './dto'; +import { useEffect, useRef, useState } from 'react'; -const Comment: React.FC = ({ content, sendComment, isModal }) => { +import send from '@assets/default/send-comment.svg'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { CommentProps } from './dto'; + +import { CommentLayout, SendContainer, CommentTextarea, SendButton } from './styles'; + +const Comment: React.FC = ({ content, sendComment, isModal = false }) => { const [comment, setComment] = useState(''); const textareaRef = useRef(null); - // textarea 높이 조정 함수 + // textarea 높이 조정 const adjustTextareaHeight = () => { if (textareaRef.current) { textareaRef.current.style.height = '1.2rem'; // 초기 높이 설정 @@ -16,17 +20,13 @@ const Comment: React.FC = ({ content, sendComment, isModal }) => { } }; - useEffect(() => { - adjustTextareaHeight(); - }, [comment]); // comment가 변경될 때만 높이 재조정 - - const handleChangeComment = (e: React.ChangeEvent) => { + const handleCommentChange = (e: React.ChangeEvent) => { if (e.target.value.length <= 100) { setComment(e.target.value); } }; - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleEnterKeyDown = (e: React.KeyboardEvent) => { if (comment === '') { e.preventDefault(); return; @@ -48,18 +48,25 @@ const Comment: React.FC = ({ content, sendComment, isModal }) => { } }; + // comment가 변경될 때만 높이 재조정 + useEffect(() => { + adjustTextareaHeight(); + }, [comment]); + return ( - {content} + {content} - + + 메시지 전송 아이콘 + ); diff --git a/src/components/Comment/styles.tsx b/src/components/BottomSheet/CommentBottomSheet/Comment/styles.tsx similarity index 60% rename from src/components/Comment/styles.tsx rename to src/components/BottomSheet/CommentBottomSheet/Comment/styles.tsx index 98fadefe..beda7373 100644 --- a/src/components/Comment/styles.tsx +++ b/src/components/BottomSheet/CommentBottomSheet/Comment/styles.tsx @@ -1,6 +1,6 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; -export const CommentLayout = styled.div<{ $isModal: boolean | undefined }>` +export const CommentLayout = styled.div<{ $isModal: boolean }>` margin: 1.38rem auto 1.25rem auto; width: 100%; display: flex; @@ -13,7 +13,7 @@ export const SendContainer = styled.div` width: 100%; min-height: 2.5rem; display: flex; - border: 1px solid #ff2389; + border: 1px solid ${({ theme }) => theme.colors.border.active}; border-radius: 0.5rem; align-items: center; `; @@ -27,16 +27,15 @@ export const CommentTextarea = styled.textarea` background-color: transparent; resize: none; overflow: hidden; - font-family: 'Pretendard Variable'; - font-size: 1rem; - font-style: normal; - font-weight: 300; - line-height: 1.2rem; + color: ${({ theme }) => theme.colors.text.primary}; + ${({ theme }) => theme.fontStyles['body2-regular']}; `; -export const SendImg = styled.img` +export const SendButton = styled.button` width: 2.5rem; height: 2.5rem; - cursor: pointer; + display: flex; + justify-content: center; + align-items: center; margin-right: 0.62rem; `; diff --git a/src/components/CommentBottomSheet/dto.ts b/src/components/BottomSheet/CommentBottomSheet/dto.ts similarity index 75% rename from src/components/CommentBottomSheet/dto.ts rename to src/components/BottomSheet/CommentBottomSheet/dto.ts index 54b25fda..b3f0b1c0 100644 --- a/src/components/CommentBottomSheet/dto.ts +++ b/src/components/BottomSheet/CommentBottomSheet/dto.ts @@ -1,4 +1,4 @@ -import { CommentProps } from '../Comment/dto'; +import { CommentProps } from './Comment/dto'; export interface CommentBottomSheetProps { isBottomSheetOpen: boolean; diff --git a/src/components/CommentBottomSheet/index.tsx b/src/components/BottomSheet/CommentBottomSheet/index.tsx similarity index 71% rename from src/components/CommentBottomSheet/index.tsx rename to src/components/BottomSheet/CommentBottomSheet/index.tsx index 06a870fe..a4c3234d 100644 --- a/src/components/CommentBottomSheet/index.tsx +++ b/src/components/BottomSheet/CommentBottomSheet/index.tsx @@ -1,10 +1,16 @@ -import { StyledText } from '../Text/StyledText'; -import theme from '../../styles/theme'; +import theme from '@styles/theme'; -import BottomSheet from '../BottomSheet'; -import Comment from '../Comment'; -import { BottomSheetProps } from '../BottomSheet/dto'; -import { CommentBottomSheetProps } from './dto'; +import closeIcon from '@assets/default/modal-close-white.svg'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { BottomSheetProps } from '../dto'; + +import type { CommentBottomSheetProps } from './dto'; + +import BottomSheet from '../index'; + +import Comment from './Comment/index'; import { CommentBottomSheetLayout, @@ -13,7 +19,7 @@ import { CommentModalHeader, CommentModalLayout, CommentModalWrapper, - XButton, + CloseButton, } from './styles'; const CommentBottomSheet: React.FC = ({ @@ -21,13 +27,6 @@ const CommentBottomSheet: React.FC = ({ commentProps, handleCloseBottomSheet, }) => { - const bottomSheetProps: BottomSheetProps = { - isOpenBottomSheet: isBottomSheetOpen, - Component: Comment, - componentProps: commentProps, - onCloseBottomSheet: handleCloseBottomSheet, - }; - const handleBackgroundClick = (e: React.MouseEvent) => { e.stopPropagation(); if (e.target === e.currentTarget) { @@ -35,10 +34,17 @@ const CommentBottomSheet: React.FC = ({ } }; - const handleButtonClick = () => { + const handleCloseButtonClick = () => { handleCloseBottomSheet(); }; + const bottomSheetProps: BottomSheetProps = { + isOpenBottomSheet: isBottomSheetOpen, + Component: Comment, + componentProps: commentProps, + onCloseBottomSheet: handleCloseBottomSheet, + }; + return ( <> {/* 모바일 & 태블릿 UI */} @@ -51,10 +57,12 @@ const CommentBottomSheet: React.FC = ({ - - 메시지 보내기 + + 매칭 요청 - + + 닫기 + diff --git a/src/components/CommentBottomSheet/styles.tsx b/src/components/BottomSheet/CommentBottomSheet/styles.tsx similarity index 73% rename from src/components/CommentBottomSheet/styles.tsx rename to src/components/BottomSheet/CommentBottomSheet/styles.tsx index d0cc03fb..f98a22d2 100644 --- a/src/components/CommentBottomSheet/styles.tsx +++ b/src/components/BottomSheet/CommentBottomSheet/styles.tsx @@ -1,5 +1,4 @@ -import styled from 'styled-components'; -import CloseIcon from '../../assets/default/modal-close-white.svg'; +import { styled } from 'styled-components'; export const CommentBottomSheetLayout = styled.div` ${({ theme }) => theme.visibleOnMobileTablet}; @@ -34,7 +33,7 @@ export const CommentModalContainer = styled.div` justify-content: center; align-items: center; border-radius: 0.38rem; - background-color: ${({ theme }) => theme.colors.white}; + background-color: ${({ theme }) => theme.colors.background.primary}; `; export const CommentModalHeader = styled.header` @@ -45,7 +44,7 @@ export const CommentModalHeader = styled.header` justify-content: space-between; align-items: center; border-radius: 0.38rem 0.38rem 0 0; - background: ${({ theme }) => theme.colors.gradient}; + background: ${({ theme }) => theme.colors.brand.gradient}; `; export const CommentModalBox = styled.section` @@ -54,13 +53,11 @@ export const CommentModalBox = styled.section` width: 100%; `; -export const XButton = styled.button` +export const CloseButton = styled.button` width: 1.875rem; height: 1.875rem; - margin: auto 0 auto auto; - background-image: url(${CloseIcon}); - background-repeat: no-repeat; - background-size: 1.875rem; - background-position: center; + display: flex; + justify-content: center; + align-items: center; opacity: 0.5; `; diff --git a/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/index.tsx b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/index.tsx index 6f5f05ec..c5b06122 100644 --- a/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/index.tsx +++ b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/index.tsx @@ -1,29 +1,19 @@ -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'; +import { useState, useRef, useCallback, useEffect, memo } from 'react'; -const ReportBottomSheetMenu: React.FC = React.memo( - ({ onCloseReportSheet, onOpenStatusModal, sendReport, isUserReport }) => { - const [inputValue, setInputValue] = useState(''); - const textareaRef = useRef(null); - const [isVisibleTextarea, setIsTextareaVisible] = useState(false); +import BottomButton from '@components/BottomButton'; - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.focus(); // 마운트 또는 업데이트 시 textarea에 포커스 유지 - } - }, []); +import type { ReportBottomSheetMenuProps } from './dto'; - const handleInputChange = useCallback((e: React.ChangeEvent) => { - setInputValue(e.target.value); - }, []); +import { SheetItemDto } from '../../BottomSheetMenu/dto'; +import BottomSheetMenu from '../../BottomSheetMenu/index'; - const handleSubmit = useCallback(() => { - sendReport(inputValue); - }, [onCloseReportSheet, onOpenStatusModal]); +import { InputLayout, ReportBottomSheetMenuWrappar } from './styles'; + +const ReportBottomSheetMenu: React.FC = memo( + ({ onCloseReportSheet, onOpenStatusModal, sendReport, isUserReport }) => { + const [isVisibleTextarea, setIsTextareaVisible] = useState(false); + const [inputValue, setInputValue] = useState(''); + const textareaRef = useRef(null); // 유저 신고 사유 목록 const userReportItems: SheetItemDto[] = [ @@ -111,6 +101,20 @@ const ReportBottomSheetMenu: React.FC = React.memo( }, ]; + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setInputValue(e.target.value); + }, []); + + const handleSubmit = useCallback(() => { + sendReport(inputValue); + }, [onCloseReportSheet, onOpenStatusModal]); + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus(); // 마운트 또는 업데이트 시 textarea에 포커스 유지 + } + }, []); + return ( diff --git a/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/styles.tsx b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/styles.tsx index 52a418e7..75420aa3 100644 --- a/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/styles.tsx +++ b/src/components/BottomSheet/OptionsBottomSheet/ReportBottomSheetMenu/styles.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const ReportBottomSheetMenuWrappar = styled.div` display: flex; @@ -11,21 +11,27 @@ export const InputLayout = styled.div` flex-direction: column; justify-content: center; align-items: center; + background-color: ${({ theme }) => theme.colors.background.primary}; + padding: 0 1rem; + margin-bottom: 6.25rem; + margin-top: -1rem; textarea { display: block; width: 100%; height: 5.25rem; - border-radius: 0.5rem; - border: 0.0625rem solid #ededed; - margin-bottom: 5.25rem; - margin-top: -1rem; + padding: 0.8rem 0.9375rem; outline: none; - padding: 1rem 0.9375rem; + border-radius: 0.5rem; + border: 0.0625rem solid ${({ theme }) => theme.colors.border.divider}; ${({ theme }) => theme.fontStyles['body1-medium']}; - line-height: 1.25%; - color: #1d1d1d; - background-color: #f8f8f8; + color: ${({ theme }) => theme.colors.text.primary}; + background-color: ${({ theme }) => theme.colors.background.secondary}; resize: none; + + &::placeholder { + color: ${({ theme }) => theme.colors.text.tertiary}; + ${({ theme }) => theme.fontStyles['body2-regular']}; + } } `; diff --git a/src/components/BottomSheet/OptionsBottomSheet/dto.ts b/src/components/BottomSheet/OptionsBottomSheet/dto.ts index 047a8e3f..67fe62fd 100644 --- a/src/components/BottomSheet/OptionsBottomSheet/dto.ts +++ b/src/components/BottomSheet/OptionsBottomSheet/dto.ts @@ -9,10 +9,3 @@ export interface OptionsBottomSheetProps { isBottomSheetOpen: boolean; onClose: () => void; } - -export interface BlockInfoDto { - userId: number; - friendId: number; - friendName: string; - action: 'toggle'; -} diff --git a/src/components/BottomSheet/OptionsBottomSheet/index.tsx b/src/components/BottomSheet/OptionsBottomSheet/index.tsx index 5241b99a..c66597d7 100644 --- a/src/components/BottomSheet/OptionsBottomSheet/index.tsx +++ b/src/components/BottomSheet/OptionsBottomSheet/index.tsx @@ -1,23 +1,35 @@ import { useState } from 'react'; -import BottomSheet from '..'; -import BottomSheetMenu from '../../BottomSheetMenu'; -import ReportBottomSheetMenu from './ReportBottomSheetMenu'; -import Modal from '../../Modal'; - -import { OptionsBottomSheetProps } from './dto'; -import { BottomSheetProps } from '../dto'; -import { BottomSheetMenuProps } from '../../BottomSheetMenu/dto'; -import { ReportBottomSheetMenuProps } from './ReportBottomSheetMenu/dto'; -import { ModalProps } from '../../Modal/dto'; - -import { SendPostReportRequest } from '../../../apis/post-report/dto'; -import { PostUserReportRequest } from '../../../apis/user-report/dto'; -import { PostUserBlockRequest } from '../../../apis/user-block/dto'; - -import { StyledText } from '../../Text/StyledText'; -import { handleError } from '../../../apis/util/handleError'; -import blockIcon from '../../../assets/default/block.svg'; -import reportIcon from '../../../assets/default/report.svg'; + +import theme from '@styles/theme'; + +import { sendPostReportApi } from '@apis/post-report'; +import { postUserBlockApi } from '@apis/user-block'; +import { postUserReportApi } from '@apis/user-report'; +import { handleError } from '@apis/util/handleError'; +import { getCurrentUserId } from '@utils/getCurrentUserId'; + +import blockIcon from '@assets/default/block.svg'; +import closeIcon from '@assets/default/modal-close-white.svg'; +import reportIcon from '@assets/default/report.svg'; + +import Modal from '@components/Modal'; +import { StyledText } from '@components/Text/StyledText'; + +import type { SendPostReportRequest } from '@apis/post-report/dto'; +import type { PostUserBlockRequest } from '@apis/user-block/dto'; +import type { PostUserReportRequest } from '@apis/user-report/dto'; +import type { ModalProps } from '@components/Modal/dto'; + +import type { BottomSheetMenuProps } from '../BottomSheetMenu/dto'; +import type { BottomSheetProps } from '../dto'; + +import type { OptionsBottomSheetProps } from './dto'; +import type { ReportBottomSheetMenuProps } from './ReportBottomSheetMenu/dto'; + +import BottomSheetMenu from '../BottomSheetMenu/index'; +import BottomSheet from '../index'; + +import ReportBottomSheetMenu from './ReportBottomSheetMenu/index'; import { ReportBottomSheetLayout, @@ -25,13 +37,9 @@ import { ReportModalWrapper, ReportModalContainer, ReportModalHeader, - XButton, + CloseButton, ReportModalBox, } from './styles'; -import theme from '../../../styles/theme'; -import { postUserBlockApi } from '../../../apis/user-block'; -import { postUserReportApi } from '../../../apis/user-report'; -import { sendPostReportApi } from '../../../apis/post-report'; const OptionsBottomSheet: React.FC = ({ domain, @@ -44,18 +52,25 @@ const OptionsBottomSheet: React.FC = ({ const [isReportBottomSheetOpen, setIsReportBottomSheetOpen] = useState(false); const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.'); - const storageValue = localStorage.getItem('my_id'); - const userId = Number(storageValue); + const currentUserId = getCurrentUserId(); + + const handleBackgroundClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (e.target === e.currentTarget) { + setIsReportBottomSheetOpen(false); + } + }; - const sendBlock = async () => { + // 유저 차단 api + const postUserBlock = async () => { try { - const blockRequest: PostUserBlockRequest = { - fromUserId: userId, - toUserId: targetId.userId || -1, + const request: PostUserBlockRequest = { + requesterId: currentUserId, + targetId: targetId.userId || -1, action: 'block', }; - const response = await postUserBlockApi(blockRequest); - + const response = await postUserBlockApi(request); + if (response.isSuccess) { setModalContent('정상적으로 처리되었습니다.'); } @@ -68,19 +83,20 @@ const OptionsBottomSheet: React.FC = ({ } }; + // 유저 또는 게시글 신고 api const sendReport = async (reason: string) => { try { let reportData: PostUserReportRequest | SendPostReportRequest; if (domain === 'user') { reportData = { - fromUserId: userId, - toUserId: targetId.userId || -1, + requesterId: currentUserId, + targetId: targetId.userId || -1, reason: reason, }; } else { reportData = { - requesterId: userId, + requesterId: currentUserId, postId: targetId.postId || -1, reason: reason, }; @@ -91,7 +107,6 @@ const OptionsBottomSheet: React.FC = ({ ? await postUserReportApi(reportData as PostUserReportRequest) : await sendPostReportApi(reportData as SendPostReportRequest); - // response.isSuccess if (response.isSuccess) { setModalContent('정상적으로 처리되었습니다.'); } @@ -126,7 +141,7 @@ const OptionsBottomSheet: React.FC = ({ ], }; - // 더보기(kebab) 메뉴 바텀시트 + // 더보기 바텀시트 const optionsBottomSheetProps: BottomSheetProps = { isOpenBottomSheet: isBottomSheetOpen, Component: BottomSheetMenu, @@ -143,7 +158,7 @@ const OptionsBottomSheet: React.FC = ({ content: `${targetNickname || '알수없음'} 님을\n정말로 차단하시겠어요?`, button: { content: '차단하기', - onClick: sendBlock, + onClick: postUserBlock, }, }; @@ -177,13 +192,6 @@ const OptionsBottomSheet: React.FC = ({ }, }; - const handleBackgroundClick = (e: React.MouseEvent) => { - e.stopPropagation(); - if (e.target === e.currentTarget) { - setIsReportBottomSheetOpen(false); - } - }; - return ( <> @@ -198,14 +206,16 @@ const OptionsBottomSheet: React.FC = ({ - + 신고 사유 선택 - { setIsReportBottomSheetOpen(false); }} - /> + > + 닫기 + diff --git a/src/components/BottomSheet/OptionsBottomSheet/styles.tsx b/src/components/BottomSheet/OptionsBottomSheet/styles.tsx index 2b6226d0..a2a75c3d 100644 --- a/src/components/BottomSheet/OptionsBottomSheet/styles.tsx +++ b/src/components/BottomSheet/OptionsBottomSheet/styles.tsx @@ -1,5 +1,4 @@ -import styled from 'styled-components'; -import CloseIcon from '../../../assets/default/modal-close-white.svg'; +import { styled } from 'styled-components'; export const ReportBottomSheetLayout = styled.div` ${({ theme }) => theme.visibleOnMobileTablet}; @@ -34,7 +33,7 @@ export const ReportModalContainer = styled.div` justify-content: center; align-items: center; border-radius: 0.38rem; - background-color: ${({ theme }) => theme.colors.white}; + background-color: ${({ theme }) => theme.colors.background.primary}; `; export const ReportModalHeader = styled.header` @@ -45,7 +44,7 @@ export const ReportModalHeader = styled.header` justify-content: space-between; align-items: center; border-radius: 0.38rem 0.38rem 0 0; - background: ${({ theme }) => theme.colors.gradient}; + background: ${({ theme }) => theme.colors.brand.gradient}; `; export const ReportModalBox = styled.section` @@ -54,13 +53,11 @@ export const ReportModalBox = styled.section` width: 100%; `; -export const XButton = styled.button` +export const CloseButton = styled.button` width: 1.875rem; height: 1.875rem; - margin: auto 0 auto auto; - background-image: url(${CloseIcon}); - background-repeat: no-repeat; - background-size: 1.875rem; - background-position: center; + display: flex; + justify-content: center; + align-items: center; opacity: 0.5; `; diff --git a/src/components/BottomSheet/dto.ts b/src/components/BottomSheet/dto.ts index 69c83235..59aea91a 100644 --- a/src/components/BottomSheet/dto.ts +++ b/src/components/BottomSheet/dto.ts @@ -4,5 +4,4 @@ export interface BottomSheetProps { Component: React.ComponentType; // BottomSheet 내부에 전달할 컴포넌트 componentProps?: T; // props가 있는 경우 객체 형태로 전달 onCloseBottomSheet: () => void; // BottomSheet을 닫는 함수 - initialTab?: 'likes' | 'comments'; // 추가: initialTab 속성 } diff --git a/src/components/BottomSheet/index.tsx b/src/components/BottomSheet/index.tsx index e0f813cf..00260425 100644 --- a/src/components/BottomSheet/index.tsx +++ b/src/components/BottomSheet/index.tsx @@ -1,11 +1,15 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { BottomSheetProps } from './dto'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import closeIcon from '@assets/default/x.svg'; + +import type { BottomSheetProps } from './dto'; + import { BottomSheetWrapper, BottomSheetLayout, Handler, SideBarLayout, - XButton, + CloseButton, SideBarTopBar, ComponentBox, } from './styles'; @@ -17,43 +21,44 @@ const BottomSheet: React.FC = ({ componentProps, onCloseBottomSheet, }) => { - const startY = useRef(null); - const [isSideBarOpen, setIsSideBarOpen] = useState(false); const [isInitialRender, setisInitialRender] = useState(true); const [isRendered, setIsRendered] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [isSideBarOpen, setIsSideBarOpen] = useState(false); const [currentTranslateY, setCurrentTranslateY] = useState(0); - const [isDragging, setIsDragging] = useState(false); + const startY = useRef(null); - useEffect(() => { - if (isOpenBottomSheet) { - setIsSideBarOpen(true); - setisInitialRender(false); - setIsRendered(true); - setCurrentTranslateY(0); - } else { - setIsSideBarOpen(false); - setIsRendered(false); + // BottomSheet 외부를 클릭할 경우 BottomSheet 닫음 + const handleBackgroundClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (!isDragging && e.target === e.currentTarget) { + onCloseBottomSheet(); } - }, [isOpenBottomSheet]); + }; - // 드래그 시작 시점의 y값 - const onPointerDown = useCallback((event: React.PointerEvent | React.TouchEvent) => { - if ('touches' in event) { - startY.current = event.touches[0].clientY; + const handleCloseButtonClick = () => { + onCloseBottomSheet(); + }; + + // 드래그 시작 + const handlePointerDown = useCallback((e: React.PointerEvent | React.TouchEvent) => { + if ('touches' in e) { + startY.current = e.touches[0].clientY; } else { - startY.current = event.clientY; + startY.current = e.clientY; } setIsDragging(true); }, []); - const onPointerMove = useCallback( - (event: PointerEvent | TouchEvent) => { + // 드래그 중 + const handlePointerMove = useCallback( + (e: PointerEvent | TouchEvent) => { if (startY.current !== null) { let currentY; - if ('touches' in event) { - currentY = event.touches[0].clientY; + if ('touches' in e) { + currentY = e.touches[0].clientY; } else { - currentY = event.clientY; + currentY = e.clientY; } const deltaY = currentY - startY.current; if (deltaY > 0) { @@ -64,15 +69,15 @@ const BottomSheet: React.FC = ({ [startY], ); - // 드래그 종료 시점의 y값 - const onPointerUp = useCallback( - (event: PointerEvent | TouchEvent) => { + // 드래그 종료 + const handlePonterUp = useCallback( + (e: PointerEvent | TouchEvent) => { if (startY.current !== null) { let endY; - if ('changedTouches' in event) { - endY = event.changedTouches[0].clientY; + if ('changedTouches' in e) { + endY = e.changedTouches[0].clientY; } else { - endY = event.clientY; + endY = e.clientY; } // 두 값의 변화량이 50px보다 크면 아래로 드래그한 것으로 간주하여 바텀시트 닫음 @@ -85,7 +90,7 @@ const BottomSheet: React.FC = ({ } startY.current = null; // 초기화 - // PointerUp 직후 onClick 동작 방지 + // pointerUp 직후 onClick 동작 방지 setTimeout(() => { setIsDragging(false); }, 100); @@ -94,48 +99,52 @@ const BottomSheet: React.FC = ({ [startY, onCloseBottomSheet], ); + // 초기 렌더링 시 window에 이벤트 리스너 등록 + // 이벤트 리스너는 on[event] 형식으로 작성했습니다 useEffect(() => { // 데스크탑 - const handlePointerUp = (event: PointerEvent) => onPointerUp(event); - const handlePointerMove = (event: PointerEvent) => onPointerMove(event); + const onPointerUp = (e: PointerEvent) => handlePonterUp(e); + const onPointerMove = (e: PointerEvent) => handlePointerMove(e); // 모바일 & 태블릿 - const handleTouchEnd = (event: TouchEvent) => onPointerUp(event); - const handleTouchMove = (event: TouchEvent) => onPointerMove(event); + const onTouchEnd = (e: TouchEvent) => handlePonterUp(e); + const onTouchMove = (e: TouchEvent) => handlePointerMove(e); - window.addEventListener('pointermove', handlePointerMove); - window.addEventListener('touchmove', handleTouchMove); - window.addEventListener('pointerup', handlePointerUp); - window.addEventListener('touchend', handleTouchEnd); + window.addEventListener('pointermove', onPointerMove); + window.addEventListener('pointerup', onPointerUp); + window.addEventListener('touchmove', onTouchMove); + window.addEventListener('touchend', onTouchEnd); + // 언마운트 시 이벤트리스너 제거 return () => { - window.removeEventListener('pointermove', handlePointerMove); - window.removeEventListener('touchmove', handleTouchMove); - window.removeEventListener('pointerup', handlePointerUp); - window.removeEventListener('touchend', handleTouchEnd); + window.removeEventListener('pointermove', onPointerMove); + window.removeEventListener('pointerup', onPointerUp); + window.removeEventListener('touchmove', onTouchMove); + window.removeEventListener('touchend', onTouchEnd); }; - }, [onPointerMove, onPointerUp]); - - // 초기 렌더링 시 바텀시트 안 보이게 설정 - if (isInitialRender && !isOpenBottomSheet) return null; + }, [handlePointerMove, handlePonterUp]); - // BottomSheet 외부를 클릭할 경우 BottomSheet 닫음 - const handleBackgroundClick = (e: React.MouseEvent) => { - e.stopPropagation(); - if (!isDragging && e.target === e.currentTarget) { - onCloseBottomSheet(); + // 바텀시트 초기 렌더링 버그를 해결하기 위한 상태값 업데이트 + useEffect(() => { + if (isOpenBottomSheet) { + setisInitialRender(false); + setIsRendered(true); + setIsSideBarOpen(true); + setCurrentTranslateY(0); + } else { + setIsRendered(false); + setIsSideBarOpen(false); } - }; + }, [isOpenBottomSheet]); - const handleButtonClick = () => { - onCloseBottomSheet(); - }; + // 부모 요소 초기 렌더링 시 바텀시트 안 보이게 설정 + if (isInitialRender && !isOpenBottomSheet) return null; return ( {/* 모바일 & 태블릿 UI */} @@ -145,7 +154,9 @@ const BottomSheet: React.FC = ({ {/* 데스크탑 UI */} - + + 닫기 + diff --git a/src/components/BottomSheet/styles.tsx b/src/components/BottomSheet/styles.tsx index 3e37a96a..91e876df 100644 --- a/src/components/BottomSheet/styles.tsx +++ b/src/components/BottomSheet/styles.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import styled from 'styled-components'; -import XIcon from '../../assets/default/x.svg'; +import { memo } from 'react'; + +import { styled } from 'styled-components'; export const BottomSheetWrapper = styled.div<{ $isBottomSheetOpen: boolean }>` position: fixed; @@ -32,7 +32,7 @@ export const BottomSheetLayout = styled.div.attrs<{ $currentTranslateY: number; max-width: 512px; left: 50%; border-radius: 0.938rem 0.938rem 0 0; - background-color: ${({ theme }) => theme.colors.white}; + background-color: ${({ theme }) => theme.colors.background.primary}; padding: 0 1.25rem; z-index: 200; user-select: none; @@ -40,11 +40,11 @@ export const BottomSheetLayout = styled.div.attrs<{ $currentTranslateY: number; transition: transform 0.3s; `; -export const Handler = React.memo(styled.hr` +export const Handler = memo(styled.hr` width: 2.88rem; margin: 0.6rem auto 0 auto; height: 0.125rem; - background-color: #d9d9d9; + background-color: ${({ theme }) => theme.colors.gray[300]}; border: none; border-radius: 0.125rem; z-index: 300; @@ -57,7 +57,7 @@ export const SideBarLayout = styled.div<{ $isSideBarOpen: boolean }>` height: 100%; position: fixed; right: 0; - background: white; + background: ${({ theme }) => theme.colors.background.primary}; transform: translateX(${({ $isSideBarOpen }) => ($isSideBarOpen ? 0 : '100%')}); transition: transform 0.3s; `; @@ -65,18 +65,17 @@ export const SideBarLayout = styled.div<{ $isSideBarOpen: boolean }>` export const SideBarTopBar = styled.header` display: flex; width: 100%; - padding: 0.5rem 1rem; + padding: 1rem 1.25rem 1rem 1rem; margin-top: 0; + justify-content: flex-end; `; -export const XButton = styled.button` - width: 2.25rem; - height: 2.25rem; - margin: auto 0 auto auto; - background-image: url(${XIcon}); - background-repeat: no-repeat; - background-size: 17px; - background-position: center; +export const CloseButton = styled.button` + width: 1.875rem; + height: 1.875rem; + display: flex; + justify-content: center; + align-items: center; `; export const ComponentBox = styled.section` diff --git a/src/components/BottomSheetMenu/index.tsx b/src/components/BottomSheetMenu/index.tsx deleted file mode 100644 index 7d47ea39..00000000 --- a/src/components/BottomSheetMenu/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { StyledText } from '../Text/StyledText'; -import { BottomSheetMenuLayout, SheetItem, Icon } from './styles'; -import { BottomSheetMenuProps, SheetItemDto } from './dto'; -import React from 'react'; - -const BottomSheetMenu: React.FC = React.memo(({ items }) => { - return ( - - {items.map((item: SheetItemDto, index) => ( -
- - - {item.text} - - {item.icon && } - -
- ))} -
- ); -}); - -export default BottomSheetMenu; diff --git a/src/components/ClothingInfoItem/dto.ts b/src/components/ClothingInfoItem/dto.ts index 374d876b..be69a5ed 100644 --- a/src/components/ClothingInfoItem/dto.ts +++ b/src/components/ClothingInfoItem/dto.ts @@ -1,6 +1,6 @@ -import { PostClothing } from '../../apis/post/dto'; +import type { PostClothing } from '@apis/post/dto'; -export interface ClothingInfo extends PostClothing {} +export type ClothingInfo = PostClothing; export interface ClothingInfoItemProps { clothingObj: ClothingInfo; diff --git a/src/components/ClothingInfoItem/index.tsx b/src/components/ClothingInfoItem/index.tsx index 6aa9162b..888b03f7 100644 --- a/src/components/ClothingInfoItem/index.tsx +++ b/src/components/ClothingInfoItem/index.tsx @@ -1,8 +1,10 @@ -import React from 'react'; -import { StyledText } from '../Text/StyledText'; -import X from '../../assets/default/x.svg'; -import Right from '../../assets/arrow/right.svg'; -import { ClothingInfoItemProps } from './dto'; +import Right from '@assets/arrow/right.svg'; +import X from '@assets/default/x.svg'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { ClothingInfoItemProps } from './dto'; + import { ClothingInfoItemContainer, ClothingInfoLeft, ClothingImage, ClothingInfoRight, ClothingModel } from './styles'; const ClothingInfoItem: React.FC = ({ clothingObj, onDelete }) => { diff --git a/src/components/ClothingInfoItem/styles.tsx b/src/components/ClothingInfoItem/styles.tsx index 3a3d5139..5eb00bfb 100644 --- a/src/components/ClothingInfoItem/styles.tsx +++ b/src/components/ClothingInfoItem/styles.tsx @@ -1,12 +1,13 @@ -import styled from 'styled-components'; -import { StyledText } from '../../components/Text/StyledText'; +import { styled } from 'styled-components'; + +import { StyledText } from '@components/Text/StyledText'; export const ClothingInfoItemContainer = styled.li` position: relative; display: flex; flex-direction: row; align-items: center; - border: 0.0625rem solid ${({ theme }) => theme.colors.pink2}; + border: 0.0625rem solid ${({ theme }) => theme.colors.brand.primaryLight}; border-radius: 0.5rem; padding: 10px; min-width: 20.9375rem; @@ -25,7 +26,6 @@ export const ClothingInfoLeft = styled.div` .infoDetail { overflow: hidden; - white-space: nowrap; text-overflow: ellipsis; width: 75%; display: flex; @@ -40,7 +40,13 @@ export const ClothingInfoLeft = styled.div` .model { margin-right: auto; color: ${({ theme }) => theme.colors.black}; - //overflow-x: hidden; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; /* 최대 2줄로 제한 */ + -webkit-box-orient: vertical; + word-break: keep-all; /* 단어 단위로 줄바꿈 */ + overflow-wrap: break-word; /* 단어가 너무 길 경우 다음 줄로 넘김 */ } `; diff --git a/src/components/ConfirmationModal/index.tsx b/src/components/ConfirmationModal/index.tsx index e9d7e17b..697982ec 100644 --- a/src/components/ConfirmationModal/index.tsx +++ b/src/components/ConfirmationModal/index.tsx @@ -1,7 +1,10 @@ -import { StyledText } from '../Text/StyledText'; -import theme from '../../styles/theme'; +import theme from '@styles/theme'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { ConfirmationModalProps } from './dto'; + import { ConfirmationModalLayout, ConfirmationModalWrapper, ContentBox, ButtonContainer, Button } from './styles'; -import { ConfirmationModalProps } from './dto'; const ConfirmationModal: React.FC = ({ content, diff --git a/src/components/ConfirmationModal/styles.tsx b/src/components/ConfirmationModal/styles.tsx index 9ca45ca4..d1788694 100644 --- a/src/components/ConfirmationModal/styles.tsx +++ b/src/components/ConfirmationModal/styles.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const ConfirmationModalWrapper = styled.div` position: fixed; diff --git a/src/components/Frame/Frame.tsx b/src/components/Frame/Frame.tsx index 3e9d398c..f19a2c4e 100644 --- a/src/components/Frame/Frame.tsx +++ b/src/components/Frame/Frame.tsx @@ -1,10 +1,10 @@ -import styled from 'styled-components'; -import theme from '../../styles/theme'; +import { styled } from 'styled-components'; + +import theme from '@styles/theme'; -// 공통 레이아웃 -> 모두 적용해주세요 export const OODDFrame = styled.div` ${theme.breakPoints}; - background-color: ${({ theme }) => theme.colors.white}; + background-color: ${({ theme }) => theme.colors.background.primary}; height: 100vh; margin: auto; display: flex; diff --git a/src/components/Icons/Alarm.tsx b/src/components/Icons/Alarm.tsx new file mode 100644 index 00000000..52e98d62 --- /dev/null +++ b/src/components/Icons/Alarm.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import type { IconsProps } from './dto'; + +const Alarm: React.FC = ({ isFilled = false, width = '14', height = '18' }) => { + return ( + + + {isFilled && } + + ); +}; + +export default Alarm; diff --git a/src/components/Icons/Heart.tsx b/src/components/Icons/Heart.tsx new file mode 100644 index 00000000..4fe45fa0 --- /dev/null +++ b/src/components/Icons/Heart.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import type { IconsProps } from './dto'; + +const Heart: React.FC = ({ isFilled = false, width = '56', height = '56' }) => { + return ( + + + + + + + + + + + + ); +}; + +export default Heart; diff --git a/src/components/Icons/Home.tsx b/src/components/Icons/Home.tsx new file mode 100644 index 00000000..05091369 --- /dev/null +++ b/src/components/Icons/Home.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { IconsProps } from './dto'; + +const Home: React.FC = ({ color = '', isFilled = false, width = '14', height = '14' }) => { + return ( + <> + {isFilled ? ( // isFilled가 true일 때 원하는 색 사용 (desktopNavBar의 home-fill는 color을 black으로, default는 color을 white로) + + + + + ) : ( + // isFilled가 false일 때 원하는 색 사용 (desktopNavBar의 home은은 color을 black으로, default는 color을 white로) + + + + )} + + ); +}; + +export default Home; diff --git a/src/components/Icons/Like.tsx b/src/components/Icons/Like.tsx new file mode 100644 index 00000000..435b3b14 --- /dev/null +++ b/src/components/Icons/Like.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import type { IconsProps } from './dto'; + +const Like: React.FC = ({ isFilled = false, color = '', width = '16', height = '14' }) => { + return ( + <> + {isFilled ? ( // like-fill.svg (isFilled = true) + + + + ) : ( + // like.svg + + + + )} + + ); +}; + +export default Like; diff --git a/src/components/Icons/Message.tsx b/src/components/Icons/Message.tsx new file mode 100644 index 00000000..9b724558 --- /dev/null +++ b/src/components/Icons/Message.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import type { IconsProps } from './dto'; + +const Message: React.FC = ({ color = '', isFilled = false, width = '14', height = '12' }) => { + return ( + <> + {isFilled ? ( // isFilled가 true일 때 원하는 색 사용 (desktopNavBar의 message-fill는 color을 black으로, default는 color을 white로) + + + + + + ) : ( + // isFilled가 false일 때 원하는 색 사용 (desktopNavBar의 message는 color을 black으로, default는 color을 #8E8E8E로, default의 message-white는 color을 white로) + + + + )} + + ); +}; + +export default Message; diff --git a/src/components/Icons/MyPage.tsx b/src/components/Icons/MyPage.tsx new file mode 100644 index 00000000..51088e34 --- /dev/null +++ b/src/components/Icons/MyPage.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import type { IconsProps } from './dto'; + +const MyPage: React.FC = ({ color = '', isFilled = false, width = '16', height = '16' }) => { + return ( + <> + {isFilled ? ( // isFilled가 true일 때 원하는 색 사용 (desktopNavBar의 my-page-fill은 color을 black으로, default는 color을 white로) + + + + + + + ) : ( + // isFilled가 false일 때 원하는 색 사용 (desktopNavBar의 my-page는 color을 black으로, default는 color을 #8E8E8E로, default의 my-page-white는 color을 white로) + + + + )} + + ); +}; + +export default MyPage; diff --git a/src/components/Icons/Photo.tsx b/src/components/Icons/Photo.tsx new file mode 100644 index 00000000..af92fa55 --- /dev/null +++ b/src/components/Icons/Photo.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import type { IconsProps } from './dto'; + +const Photo: React.FC = ({ width = '18', height = '18', color = '#8E8E8E' }) => { + return ( + // photo-big는 크기와 높이를 100, 100으로 설정 () / photo-white는 color을 white로 설정 () + + + + ); +}; + +export default Photo; diff --git a/src/components/Icons/dto.ts b/src/components/Icons/dto.ts new file mode 100644 index 00000000..059da825 --- /dev/null +++ b/src/components/Icons/dto.ts @@ -0,0 +1,6 @@ +export interface IconsProps { + width?: string; + height?: string; + color?: string; + isFilled?: boolean; +} diff --git a/src/components/Loading/styles.tsx b/src/components/Loading/styles.tsx index 1849d7ed..a403180d 100644 --- a/src/components/Loading/styles.tsx +++ b/src/components/Loading/styles.tsx @@ -1,4 +1,4 @@ -import styled, { keyframes } from 'styled-components'; +import { styled, keyframes } from 'styled-components'; const bounceGroup = keyframes` 0%, 50%, 100% { @@ -35,9 +35,9 @@ export const Dot = styled.hr<{ $index: number }>` z-index: 200; border-radius: 50%; border: none; - background-color: ${({ theme }) => theme.colors.gray2}; + background-color: ${({ theme }) => theme.colors.gray[300]}; // 각 점에 대해 딜레이를 적용하여 순차적으로 애니메이션을 시작 animation: ${bounceGroup} 2s ease-in-out infinite; - animation-delay: ${({ $index }) => `${($index % 3) * 0.2}s`}}; + animation-delay: ${({ $index }) => `${($index % 3) * 0.2}s`}; `; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index b1ecabfd..5be679a3 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,9 +1,12 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; +import { createPortal } from 'react-dom'; + +import closeIcon from '@assets/default/x.svg'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { ModalProps } from './dto'; + import { ModalWrapper, ModalContainer, CloseButton, ConfirmButton } from './styles'; -import { StyledText } from '../Text/StyledText'; -import { ModalProps } from './dto'; -import XIcon from '../../assets/default/x.svg'; const Modal: React.FC = ({ isCloseButtonVisible, onClose, content, button }) => { const handleBackgroundClick = (e: React.MouseEvent) => { @@ -21,12 +24,12 @@ const Modal: React.FC = ({ isCloseButtonVisible, onClose, content, b } }; - return ReactDOM.createPortal( + return createPortal( {isCloseButtonVisible && ( - + )} {content} diff --git a/src/components/Modal/styles.tsx b/src/components/Modal/styles.tsx index 7dd1de82..9306d773 100644 --- a/src/components/Modal/styles.tsx +++ b/src/components/Modal/styles.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const ModalWrapper = styled.div` position: fixed; @@ -20,13 +20,13 @@ export const ModalContainer = styled.div<{ $isCloseButtonVisible: boolean }>` align-items: center; justify-content: center; text-align: center; - gap: 1rem; + gap: 1.5rem; width: 21.25rem; max-width: calc(100% - 2.5rem); max-height: 30%; - padding: 1.25rem; + padding: 2rem 1.5rem 1.5rem 1.5rem; ${({ $isCloseButtonVisible }) => ($isCloseButtonVisible ? 'padding-top: 2.5rem' : '')}; - background-color: ${({ theme }) => theme.colors.white}; + background-color: ${({ theme }) => theme.colors.background.primary}; border-radius: 0.625rem; box-shadow: 0 -0.125rem 0.625rem rgba(0, 0, 0, 0.1); z-index: 999; @@ -49,7 +49,7 @@ export const ConfirmButton = styled.button` padding: 0.625rem 0.875rem; justify-content: center; align-items: center; - background: ${({ theme }) => theme.colors.gradient}; + background: ${({ theme }) => theme.colors.brand.gradient}; border-radius: 0.5rem; color: white; ${({ theme }) => theme.fontStyles['body1-medium']} diff --git a/src/components/NavBar/index.tsx b/src/components/NavBar/index.tsx index faaf5dda..a1e6395f 100644 --- a/src/components/NavBar/index.tsx +++ b/src/components/NavBar/index.tsx @@ -1,9 +1,26 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; + +import theme from '@styles/theme'; + +import { getCurrentUserId } from '@utils/getCurrentUserId'; + +import logout from '@assets/default/leave.svg'; +import logo from '@assets/default/oodd.svg'; + +import Alarm from '@components/Icons/Alarm'; +import Home from '@components/Icons/Home'; +import Message from '@components/Icons/Message'; +import MyPage from '@components/Icons/MyPage'; + +import Modal from '@components/Modal'; +import { StyledText } from '@components/Text/StyledText'; + +import type { ModalProps } from '@components/Modal/dto'; + import { NavBarContainer, NavBarWrapper, - IconImg, IconWrapper, SideNavBarContainer, SideNavBarList, @@ -11,44 +28,39 @@ import { SideNavBarButton, SideNavBarHeader, SideNavBarFooter, + Icon, } from './styles'; -import Chat_s from './../../assets/default/message-white.svg'; -import Chat_f from './../../assets/default/message-fill.svg'; -import Home_s from './../../assets/default/home.svg'; -import Home_f from './../../assets/default/home-fill.svg'; -import Profile_s from './../../assets/default/my-page-white.svg'; -import Profile_f from './../../assets/default/my-page-fill.svg'; -import logo from './../../assets/default/oodd.svg'; -import alarm from './../../assets/default/alarm.svg'; -// import clickedAlarm from './../../assets/default/alarm-on.svg'; -import chatDesktopIcon from './../../assets/default/desktopNavBar/message.svg'; -import chatFillDesktopIcon from './../../assets/default/desktopNavBar/message-fill.svg'; -import homeDesktopIcon from './../../assets/default/desktopNavBar/home.svg'; -import homeFillDesktopIcon from './../../assets/default/desktopNavBar/home-fill.svg'; -import profileDesktopIcon from './../../assets/default/desktopNavBar/my-page.svg'; -import profileFillDesktopIcon from './../../assets/default/desktopNavBar/my-page-fill.svg'; -import logout from './../../assets/default/leave.svg'; -import { StyledText } from '../Text/StyledText'; -import Modal from '../Modal'; -import { ModalProps } from '../Modal/dto'; - -const tabs = [ - { name: 'Chats', iconSelected: Chat_f, iconUnselected: Chat_s, route: '/chats' }, - { name: 'Home', iconSelected: Home_f, iconUnselected: Home_s, route: '/' }, - { name: 'Profile', iconSelected: Profile_f, iconUnselected: Profile_s, route: '/mypage' }, -]; - -const desktopTabs = [ - { name: 'Home', iconSelected: homeFillDesktopIcon, iconUnselected: homeDesktopIcon, route: '/' }, - { name: 'Chats', iconSelected: chatFillDesktopIcon, iconUnselected: chatDesktopIcon, route: '/chats' }, - { name: 'Profile', iconSelected: profileFillDesktopIcon, iconUnselected: profileDesktopIcon, route: '/mypage' }, -]; const NavBar: React.FC = () => { - const [selectedTab, setSelectedTab] = useState(''); const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); + const [selectedTab, setSelectedTab] = useState(''); const navigate = useNavigate(); const location = useLocation(); + const currentUserId = getCurrentUserId(); + + const tabs = [ + { + name: 'Chats', + Icon: (isSelected: boolean, isDesktop: boolean) => ( + + ), + route: '/chats', + }, + { + name: 'Home', + Icon: (isSelected: boolean, isDesktop: boolean) => ( + + ), + route: '/', + }, + { + name: 'Profile', + Icon: (isSelected: boolean, isDesktop: boolean) => ( + + ), + route: `/profile/${currentUserId}`, + }, + ]; useEffect(() => { const currentTab = tabs.find((tab) => tab.route === location.pathname); @@ -66,7 +78,7 @@ const NavBar: React.FC = () => { } }; - const handleConfirmLogout = () => { + const handleLogoutConfirmButtonClick = () => { localStorage.clear(); setIsLogoutModalOpen(false); @@ -85,18 +97,17 @@ const NavBar: React.FC = () => { content: '이 기기에서 정말 로그아웃 할까요?', button: { content: '로그아웃', - onClick: handleConfirmLogout, + onClick: handleLogoutConfirmButtonClick, }, }; return ( <> - {isLogoutModalOpen && } {tabs.map((tab) => ( handleTabClick(tab)}> - + {tab.Icon(selectedTab === tab.name, false)}

{tab.name}

))} @@ -106,20 +117,18 @@ const NavBar: React.FC = () => { oodd - {desktopTabs.map((tab) => ( + {tabs.map((tab) => ( handleTabClick(tab)}> - + {tab.name} @@ -130,11 +139,12 @@ const NavBar: React.FC = () => { + {isLogoutModalOpen && } ); }; diff --git a/src/components/NavBar/styles.tsx b/src/components/NavBar/styles.tsx index 62eb1fd9..394653bd 100644 --- a/src/components/NavBar/styles.tsx +++ b/src/components/NavBar/styles.tsx @@ -1,19 +1,17 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const NavBarContainer = styled.nav` - // fixed 포지션에 breakPoint를 적용하는 방법 position: fixed; - ${({ theme }) => theme.visibleOnMobileTablet}; // breakPoint 미디어쿼리 - bottom: 0; // 경우에 따라 top 0 등으로 작성 - left: 50%; // 수직 중앙에 위치 - transform: translateX(-50%); // width에 따른 수직 중앙 조정 - width: 100%; // brakePoint에 따른 width에 따르도록 설정 + ${({ theme }) => theme.visibleOnMobileTablet}; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; - // 이후로 자기 코드에 맞춰서 작성 height: 5.5rem; justify-content: center; align-items: center; - background: ${({ theme }) => theme.colors.gradient}; // 여기서 그래디언트 색상 사용 + background: ${({ theme }) => theme.colors.brand.gradient}; margin: 0; border-radius: 1.25rem 1.25rem 0 0; filter: drop-shadow(0rem 0rem 0.25rem rgba(0, 0, 0, 0.25)); @@ -35,7 +33,6 @@ export const IconWrapper = styled.div` cursor: pointer; gap: 10px; - p { margin: 0; bottom: 0; @@ -49,7 +46,7 @@ export const IconWrapper = styled.div` } `; -export const IconImg = styled.img` +export const Icon = styled.div` width: 1.13rem; height: 1.13rem; object-fit: cover; @@ -67,7 +64,7 @@ export const SideNavBarContainer = styled.nav` padding: 2.5rem 1.5rem; border-radius: 0 3rem 3rem 0; position: fixed; - background: white; + background: ${({ theme }) => theme.colors.background.primary}; filter: drop-shadow(0rem 0rem 0.25rem rgba(0, 0, 0, 0.25)); `; @@ -91,7 +88,7 @@ export const SideNavBarHeader = styled.header` } button:hover { - background: rgba(0, 0, 0, 0.1); + background: ${({ theme }) => theme.colors.gray[200]}; } `; @@ -111,7 +108,7 @@ export const SideNavBarButton = styled.label` align-items: center; border-radius: 50%; padding: 0.6rem; - background: white; + background: ${({ theme }) => theme.colors.background.primary}; transition: background 0.2s; box-shadow: @@ -121,7 +118,7 @@ export const SideNavBarButton = styled.label` } button:hover { - background: rgba(0, 0, 0, 0.1); + background: ${({ theme }) => theme.colors.gray[200]}; } img { diff --git a/src/components/NavbarProfile/index.tsx b/src/components/NavbarProfile/index.tsx deleted file mode 100644 index 0e068075..00000000 --- a/src/components/NavbarProfile/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { Nav, IconContainer } from './styles'; -import { Link } from 'react-router-dom'; -import settingIcon from '../../assets/default/setting.svg'; -import { StyledText } from '../../components/Text/StyledText'; -import theme from '../../styles/theme'; - -const NavbarProfile: React.FC = () => { - return ( - - ); -}; - -export default NavbarProfile; diff --git a/src/components/NavbarProfile/styles.tsx b/src/components/NavbarProfile/styles.tsx deleted file mode 100644 index 6cfb4d00..00000000 --- a/src/components/NavbarProfile/styles.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import styled from 'styled-components'; - -export const Nav = styled.nav` - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - margin-left: 0px; - position: fixed; /* 화면 상단에 고정 */ - top: 0; /* 상단에 위치 */ - left: 0; /* 왼쪽에 위치 */ - width: 100%; /* 화면의 전체 너비 차지 */ - background-color: white; /* 배경색 설정 (필요에 따라 변경) */ - z-index: 1000; /* 다른 요소들보다 위에 오도록 설정 */ - - position: sticky; - top: 0; /* 페이지의 최상단에 고정 */ - z-index: 998; /* 다른 요소 위에 표시되도록 설정 */ -`; - -export const IconContainer = styled.div` - display: flex; - align-items: center; - margin-right: 18px; - - a { - display: flex; - align-items: center; - } - - img { - width: 1.5rem; /* 24px */ - height: 1.5rem; /* 24px */ - } -`; diff --git a/src/components/PostItem/dto.ts b/src/components/PostItem/dto.ts index 23cd3a3c..edf1b89b 100644 --- a/src/components/PostItem/dto.ts +++ b/src/components/PostItem/dto.ts @@ -1,6 +1,6 @@ -import { UserPostSummary } from '../../apis/post/dto'; +import { UserPostSummary } from '@apis/post/dto'; -export interface Post extends UserPostSummary {} +export type Post = UserPostSummary; export interface PostItemProps { post: Post; diff --git a/src/components/PostItem/index.tsx b/src/components/PostItem/index.tsx index 4a4596b0..4af33db2 100644 --- a/src/components/PostItem/index.tsx +++ b/src/components/PostItem/index.tsx @@ -1,46 +1,44 @@ -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 }) => { + +import theme from '@styles/theme'; + +import PinIcon from '@assets/default/pin.svg'; + +import Like from '@components/Icons/Like'; +import Message from '@components/Icons/Message'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { PostItemProps } from './dto'; + +import { PostItemLayout, PostImageContainer, PostImage, LikesOverlay, Pin } from './style'; + +const PostItem: React.FC = ({ post }) => { const navigate = useNavigate(); - const imageUrl = post.imageUrl; + const postImageUrl = post.imageUrl; - const handleClick = () => { - const path = isMyPost ? `/my-post/${post.id}` : `/post/${post.id}`; + const handlePostItemClick = () => { + const path = `/post/${post.id}`; navigate(path); }; return ( - + - - {post.isRepresentative && } + + {post.isRepresentative && } - - + + {post.postLikesCount} - - - + + + {post.postCommentsCount} - + - + ); }; diff --git a/src/components/PostItem/style.tsx b/src/components/PostItem/style.tsx index 83c253f1..2df8df22 100644 --- a/src/components/PostItem/style.tsx +++ b/src/components/PostItem/style.tsx @@ -1,7 +1,6 @@ -import styled from 'styled-components'; -import { StyledText } from '../Text/StyledText'; +import { styled } from 'styled-components'; -export const PostItemContainer = styled.article` +export const PostItemLayout = styled.article` flex: 1 1 calc(50% - 0.5rem); /* 기본적으로 두 개씩 배치되도록 설정 */ width: 100%; max-width: 67.5rem; /* 최대 너비 설정 */ @@ -43,27 +42,21 @@ export const PostImage = styled.img` export const LikesOverlay = styled.div` position: absolute; - bottom: 0; /* 하단에 배치 */ + bottom: 0.625rem; /* 하단에 배치 */ + right: 0.625rem; width: 100%; display: flex; - align-items: center; justify-content: flex-end; + align-items: center; box-sizing: border-box; + gap: 5px; `; -export const Icon = styled.img` - margin-bottom: 0.5rem; -`; - -export const LikesCountStyledText = styled(StyledText)` - margin: 0 8px 0.5rem 4px; -`; - -export const PinSvg = styled.img` +export const Pin = styled.img` display: flex; position: absolute; top: 0.75rem; - left: 1.25rem; + left: 0.75rem; justify-content: center; align-items: center; `; diff --git a/src/components/Text/StyledText.tsx b/src/components/Text/StyledText.tsx index 331135a2..91d52ea3 100644 --- a/src/components/Text/StyledText.tsx +++ b/src/components/Text/StyledText.tsx @@ -1,29 +1,14 @@ -import styled from 'styled-components'; -import theme from '../../styles/theme'; import { useMediaQuery } from 'react-responsive'; -export type FontStyleKey = keyof typeof theme.fontStyles; +import { styled } from 'styled-components'; -// 플랫폼 별 폰트가 다른 경우 -interface FontStylesByPlatform { - mobile: FontStyleKey; - tablet: FontStyleKey; - desktop: FontStyleKey; -} +import theme from '@styles/theme'; -export interface StyledTextProps { - $textTheme: { - style: FontStyleKey | FontStylesByPlatform; - lineHeight?: number; - }; - color?: string; - children: any; -} +import type { StyledTextProps } from './dto'; export const StyledText = styled.div` - color: ${(props) => props.color || theme.colors.black}; + color: ${(props) => props.color || theme.colors.text.primary}; 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' }); @@ -31,9 +16,12 @@ export const StyledText = styled.div` const isDesktop = useMediaQuery({ minWidth: '1220px' }); let fontStyle; + if (typeof props.$textTheme.style === 'string') { + // style이 문자열이면 일괄적으로 사용 fontStyle = theme.fontStyles[props.$textTheme.style]; } else if (typeof props.$textTheme.style === 'object') { + // style이 객체면 기기에 따른 분기 if (isMobile) { fontStyle = theme.fontStyles[props.$textTheme.style.mobile]; } else if (isTabletPortrait || isTabletLandscape) { diff --git a/src/components/Text/dto.ts b/src/components/Text/dto.ts new file mode 100644 index 00000000..59aaadad --- /dev/null +++ b/src/components/Text/dto.ts @@ -0,0 +1,19 @@ +import theme from '@styles/theme'; + +// 플랫폼 별 폰트가 다른 경우 +interface FontStylesByPlatform { + mobile: FontStyleKey; + tablet: FontStyleKey; + desktop: FontStyleKey; +} + +export interface StyledTextProps { + $textTheme: { + style: FontStyleKey | FontStylesByPlatform; + lineHeight?: number; + }; + color?: string; + children: unknown; +} + +export type FontStyleKey = keyof typeof theme.fontStyles; diff --git a/src/components/TopBar/dto.ts b/src/components/TopBar/dto.ts index 1f7579f0..84ff4939 100644 --- a/src/components/TopBar/dto.ts +++ b/src/components/TopBar/dto.ts @@ -1,12 +1,12 @@ export interface TopBarProps { - text?: string; // 텍스트, optional prop - RightButtonSrc?: string; // KebabMenuButton src의 Optional prop - LeftButtonSrc?: string; // BackButton src의 Optional prop - onLeftClick?: () => void; // BackButton src의 Optional prop - onRightClick?: () => void; // KebabMenuButton src의 Optional prop + text?: string; + RightButtonSrc?: string; + LeftButtonSrc?: string; + onClickLeftButton?: () => void; + onClickRightButton?: () => void; $withBorder?: boolean; } -export interface TopbarLayoutProps { +export interface TopBarLayoutProps { $withBorder?: boolean; } diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index 11d57864..f02d32b1 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -1,48 +1,49 @@ -import theme from '../../styles/theme'; -import { TopbarLayout, StyledTextLayout, LeftButton, RightButton } from './styles'; import { useNavigate } from 'react-router-dom'; -import { TopBarProps } from './dto'; + +import theme from '@styles/theme'; + +import type { TopBarProps } from './dto'; + +import { TopBarLayout, StyledTextWrapper, LeftButton, RightButton } from './styles'; const TopBar: React.FC = ({ text = '', RightButtonSrc, LeftButtonSrc, - onLeftClick, - onRightClick, + onClickLeftButton, + onClickRightButton, $withBorder = false, }) => { - const nav = useNavigate(); + const navigate = useNavigate(); return ( - <> - - { - if (onLeftClick) { - onLeftClick(); - } else { - nav(-1); - } - }} - > - 뒤로가기 - - - {text} - - { - if (onRightClick) { - onRightClick(); - } - }} - > - 메뉴 - - - + + { + if (onClickLeftButton) { + onClickLeftButton(); + } else { + navigate(-1); + } + }} + > + 뒤로가기 + + + {text} + + { + if (onClickRightButton) { + onClickRightButton(); + } + }} + > + 메뉴 + + ); }; diff --git a/src/components/TopBar/styles.tsx b/src/components/TopBar/styles.tsx index 8ee422ca..2ab92929 100644 --- a/src/components/TopBar/styles.tsx +++ b/src/components/TopBar/styles.tsx @@ -1,24 +1,27 @@ -import styled from 'styled-components'; -import { TopbarLayoutProps } from './dto'; -import { StyledText } from '../Text/StyledText'; +import { styled } from 'styled-components'; -export const TopbarLayout = styled.header` +import theme from '@styles/theme'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { TopBarLayoutProps } from './dto'; +export const TopBarLayout = styled.header` display: flex; position: sticky; top: 0; /* 부모 요소의 상단에 붙도록 설정 */ - z-index: 1; - background-color: white; + z-index: 99; + background-color: ${theme.colors.background.primary}; width: 100%; /* 부모 너비에 맞춤 */ align-items: center; padding: 0.5rem 1.25rem; ${({ $withBorder, theme }) => $withBorder && ` - border-bottom: solid 0.0625rem ${theme.colors.gray2}; + border-bottom: solid 0.0625rem ${theme.colors.border.divider}; `} `; -export const StyledTextLayout = styled(StyledText)` +export const StyledTextWrapper = styled(StyledText)` flex-direction: column; align-items: center; `; diff --git a/src/components/UserProfile/dto.ts b/src/components/UserProfile/dto.ts new file mode 100644 index 00000000..d8a1140e --- /dev/null +++ b/src/components/UserProfile/dto.ts @@ -0,0 +1,5 @@ +export interface UserProfileProps { + userImg?: string; // string | undefined + bio?: string; + nickname: string; +} diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx index 04df42f0..7ec9ec54 100644 --- a/src/components/UserProfile/index.tsx +++ b/src/components/UserProfile/index.tsx @@ -1,26 +1,25 @@ -import React from 'react'; -import { StyledText } from '../Text/StyledText'; -import theme from '../../styles/theme'; -import { UserProfileContainer, UserImg, UserDetails, BioStyledText } from './style'; +import { memo } from 'react'; -interface UserProfileProps { - userImg?: string; // string | undefined - bio?: string; - nickname: string; -} +import theme from '@styles/theme'; -const UserProfile: React.FC = React.memo(({ userImg, bio = '', nickname }) => { +import { StyledText } from '@components/Text/StyledText'; + +import type { UserProfileProps } from './dto'; + +import { UserProfileLayout, UserImg, UserDetailsContainer, StyledBio } from './style'; + +const UserProfile: React.FC = memo(({ userImg, bio = '', nickname }) => { const truncatedBio = bio ? (bio.length > 50 ? bio.substring(0, 50) + '...' : bio) : ''; return ( - + - + {nickname} - + {truncatedBio} - - - + + + ); }); diff --git a/src/components/UserProfile/style.tsx b/src/components/UserProfile/style.tsx index 709717e0..4d2d07aa 100644 --- a/src/components/UserProfile/style.tsx +++ b/src/components/UserProfile/style.tsx @@ -1,7 +1,8 @@ -import styled from 'styled-components'; -import { StyledText } from '../Text/StyledText'; +import { styled } from 'styled-components'; -export const UserProfileContainer = styled.section` +import { StyledText } from '@components/Text/StyledText'; + +export const UserProfileLayout = styled.section` display: flex; flex-direction: row; `; @@ -12,7 +13,7 @@ export const UserImg = styled.img` border-radius: 50%; `; -export const UserDetails = styled.section` +export const UserDetailsContainer = styled.section` display: flex; flex-direction: column; justify-content: center; @@ -21,7 +22,7 @@ export const UserDetails = styled.section` margin-left: 1rem; `; -export const BioStyledText = styled(StyledText)` +export const StyledBio = styled(StyledText)` display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; diff --git a/src/config/constant.ts b/src/config/constant.ts index bc777657..4a9b72ea 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,2 +1 @@ -export const JWT_KEY = 'jwt_token'; export const NEW_JWT_KEY = 'new_jwt_token'; diff --git a/src/context/SocketProvider.tsx b/src/context/SocketProvider.tsx index eb6e9665..a77d7108 100644 --- a/src/context/SocketProvider.tsx +++ b/src/context/SocketProvider.tsx @@ -1,4 +1,5 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react'; + import { io, Socket } from 'socket.io-client'; const SocketContext = createContext(null); diff --git a/src/main.tsx b/src/main.tsx index 9f9855dc..223211e3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,16 @@ -import ReactDOM from 'react-dom/client'; -import App from './App.tsx'; -import { ThemeProvider } from 'styled-components'; -import theme from './styles/theme'; -import GlobalStyle from './styles/GlobalStyles'; +import { createRoot } from 'react-dom/client'; import { RecoilRoot } from 'recoil'; -import './styles/fonts/font.css'; -import { SocketProvider } from './context/SocketProvider.tsx'; +import { ThemeProvider } from 'styled-components'; + +import GlobalStyle from '@styles/GlobalStyles'; +import theme from '@styles/theme'; + +import '@styles/fonts/font.css'; +import { SocketProvider } from '@context/SocketProvider'; + +import App from './App'; -ReactDOM.createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Account/AccountCancel/index.tsx b/src/pages/Account/AccountCancel/index.tsx new file mode 100644 index 00000000..1c30b279 --- /dev/null +++ b/src/pages/Account/AccountCancel/index.tsx @@ -0,0 +1,136 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import theme from '@styles/theme'; + +import { patchUserWithdrawApi } from '@apis/user'; + +import back from '@assets/arrow/left.svg'; + +import BottomButton from '@components/BottomButton/index'; +import { OODDFrame } from '@components/Frame/Frame'; +import Modal from '@components/Modal/index'; +import { StyledText } from '@components/Text/StyledText'; +import TopBar from '@components/TopBar/index'; + +import { + CancelContainer, + SubTitle, + Text, + InfoBox, + InfoItem, + CheckboxWrapper, + CheckboxInput, + Label, + StyledCheckboxText, + StyledDiv, +} from './styles'; // 상대 경로 index 명시 + +const AccountCancel: React.FC = () => { + const [isChecked, setIsChecked] = useState(false); + const [modalContent, setModalContent] = useState(null); + const [isModalVisible, setIsModalVisible] = useState(false); + const navigate = useNavigate(); + + const handleCheckboxChange = () => { + setIsChecked(!isChecked); + }; + + const handleModalClose = () => { + setIsModalVisible(false); + setModalContent(null); + }; + + const handleDeleteAccount = async () => { + try { + if (!isChecked) { + setModalContent('탈퇴 안내사항에 동의해야 합니다.'); + setIsModalVisible(true); + return; + } + + const storedUserId = Number(localStorage.getItem('my_id')); + const token = localStorage.getItem('new_jwt_token'); + + if (!storedUserId || !token) { + setModalContent('사용자 정보를 찾을 수 없습니다.'); + setIsModalVisible(true); + return; + } + + // API 요청 + const response = await patchUserWithdrawApi(storedUserId); + + if (response.isSuccess) { + setModalContent('계정이 성공적으로 삭제되었습니다.'); + setIsModalVisible(true); + + // 계정 삭제 시 localStorage에서 사용자 정보 제거 + localStorage.clear(); + + setTimeout(() => { + navigate('/login'); + }, 2000); + } else { + setModalContent(response.code || '알 수 없는 오류가 발생했습니다.'); + setIsModalVisible(true); + } + } catch (error) { + console.error('계정 삭제하는데 오류남:', error); + setModalContent('계정을 삭제하는 동안 오류가 발생했습니다. 다시 시도해 주세요.'); + setIsModalVisible(true); + } + }; + + return ( + + + navigate(-1)} /> + + + + OOTD 탈퇴 전 확인하세요! + + + + + {`탈퇴하시면 이용 중인 서비스가 폐쇄되며,\n모든 데이터는 복구할 수 없습니다.`} + + + + + + 지금까지 OODD를 이용해주셔서 감사합니다! + + + + + + + + + + + {isModalVisible && ( + + )} + + ); +}; + +export default AccountCancel; diff --git a/src/pages/Account/AccountCancel/styles.tsx b/src/pages/Account/AccountCancel/styles.tsx new file mode 100644 index 00000000..3f1e21e2 --- /dev/null +++ b/src/pages/Account/AccountCancel/styles.tsx @@ -0,0 +1,100 @@ +import { styled } from 'styled-components'; + +import { StyledText } from '@components/Text/StyledText'; + +export const CancelContainer = styled.div` + margin: 0 auto; + width: 100%; + flex-grow: 1; + display: flex; + flex-direction: column; +`; + +export const SubTitle = styled.h3` + font-size: 1rem; + font-weight: bold; + margin-bottom: 0.625rem; + text-align: center; + text-align: left; + margin-top: 10px; + padding: 1.25rem; +`; + +export const Text = styled.p` + font-size: 0.875rem; + margin-bottom: 5px; + text-align: left; + margin-top: 10px; + padding: 0rem 1.25rem; +`; + +export const InfoBox = styled.div` + background: ${({ theme }) => theme.colors.background.secondary}; + padding: 70px; + margin-top: 10px; + border-radius: 10px; + margin: 10px 20px 1.25rem 20px; +`; + +export const InfoItem = styled.p` + font-size: 0.875rem; + margin-bottom: 0.625rem; + padding: 2px 10px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + height: 100%; +`; + +export const CheckboxWrapper = styled.div` + display: flex; + align-items: center; + margin-bottom: 1.25rem; + padding: 0rem 15px; + + input[type='checkbox'] { + margin-right: 0.625rem; + } +`; + +export const CheckboxInput = styled.input` + margin-right: 0.625rem; + cursor: pointer; + appearance: none; + width: 1.25rem; + height: 1.25rem; + border: 0.125rem solid ${({ theme }) => theme.colors.gray[200]}; + border-radius: 0.25rem; + position: relative; + &:checked { + background-color: ${({ theme }) => theme.colors.background.primaryLight}; + border-color: ${({ theme }) => theme.colors.brand.primary}; + } + + &:checked::after { + content: '✓'; + color: ${({ theme }) => theme.colors.contrast}; + font-size: 0.875rem; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +`; + +export const Label = styled.label` + display: flex; + align-items: center; + cursor: pointer; +`; + +export const StyledCheckboxText = styled(StyledText)` + color: ${({ theme }) => theme.colors.text.caption}; +`; + +export const StyledDiv = styled.div<{ isChecked: boolean }>` + background-color: ${({ isChecked, theme }) => (isChecked ? theme.colors.primary : theme.colors.gray[300])}; + color: ${({ isChecked, theme }) => (isChecked ? theme.colors.contrast : theme.colors.caption)}; + cursor: ${({ isChecked }) => (isChecked ? 'pointer' : 'not-allowed')}; +`; diff --git a/src/pages/AccountEdit/index.tsx b/src/pages/Account/AccountEdit/index.tsx similarity index 57% rename from src/pages/AccountEdit/index.tsx rename to src/pages/Account/AccountEdit/index.tsx index 598bf59d..fb0bb67a 100644 --- a/src/pages/AccountEdit/index.tsx +++ b/src/pages/Account/AccountEdit/index.tsx @@ -1,4 +1,16 @@ -import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import theme from '@styles/theme'; + +import back from '@assets/arrow/left.svg'; +import kakao from '@assets/default/snsIcon/kakao.svg'; +import naver from '@assets/default/snsIcon/naver.svg'; + +import BottomButton from '@components/BottomButton/index'; +import { OODDFrame } from '@components/Frame/Frame'; +import { StyledText } from '@components/Text/StyledText'; +import TopBar from '@components/TopBar/index'; + import { ProfileEditContainer, Section, @@ -13,46 +25,34 @@ import { Label, Info, } from './styles'; -import { OODDFrame } from '../../components/Frame/Frame'; - -import BottomButton from '../../components/BottomButton'; // BottomButton 컴포넌트 임포트 - -import { useNavigate } from 'react-router-dom'; -import { StyledText } from '../../components/Text/StyledText'; -import theme from '../../styles/theme'; - -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/arrow/left.svg'; const AccountEdit: React.FC = () => { const navigate = useNavigate(); // useNavigate 훅 사용 // 본인 인증 페이지로 이동하는 함수 const handleVerification = () => { - navigate('/Verification'); + navigate('/account/verification'); }; return ( - navigate(-1)} /> + navigate(-1)} />
- + 로그인 정보 - + SNS 연결 - + 연결된 SNS계정으로 로그인되었습니다. @@ -64,14 +64,14 @@ const AccountEdit: React.FC = () => {
- + 회원 정보 @@ -79,7 +79,7 @@ const AccountEdit: React.FC = () => { diff --git a/src/pages/Account/AccountEdit/styles.tsx b/src/pages/Account/AccountEdit/styles.tsx new file mode 100644 index 00000000..2ed8d886 --- /dev/null +++ b/src/pages/Account/AccountEdit/styles.tsx @@ -0,0 +1,91 @@ +import { styled } from 'styled-components'; + +export const ProfileEditContainer = styled.div` + max-width: 512px; + display: flex; + flex-direction: column; + position: relative; +`; + +export const Section = styled.div` + margin-top: 1.875rem; + margin-bottom: 1.875rem; + width: 100%; + padding: 0px 30px; +`; + +export const SectionTitle = styled.div` + font-size: 1.125rem; + font-weight: bold; + margin-bottom: 0.625rem; + margin-top: 1.125rem; + text-align: left; +`; + +export const SNSInfo = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 0.625rem; + margin-top: 3.125rem; +`; + +export const SNSInfoRow = styled.div` + display: flex; + align-items: center; + margin-bottom: 0.625rem; +`; + +export const SNSIcon = styled.img` + width: 2.5rem; + height: 2.5rem; + margin-right: 0.625rem; + margin-top: 1.875rem; + flex-shrink: 0; + object-fit: cover; +`; + +export const Text = styled.div` + font-size: 0.875rem; + color: ${({ theme }) => theme.colors.tertiary}; + margin-top: 2.1875rem; + text-align: left; +`; + +export const SnsConnection = styled.div` + font-size: 1rem; + font-weight: bold; + color: ${({ theme }) => theme.colors.gray[700]}; + margin-bottom: 0.625rem; + text-align: left; +`; + +export const MemberInfo = styled.div` + display: flex; + flex-direction: column; + margin-top: 35px; + width: 100%; +`; + +export const MemberInfoRow = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + margin-bottom: 0.625rem; + margin-top: 10px; +`; + +export const Label = styled.div` + font-size: 0.875rem; + color: ${({ theme }) => theme.colors.gray[700]}; + display: flex; + align-items: center; + width: 6.25rem; +`; + +export const Info = styled.div` + font-size: 0.875rem; + color: ${({ theme }) => theme.colors.caption}; + margin-left: 0.625rem; + flex-grow: 1; + text-align: left; +`; diff --git a/src/pages/AccountSetting/index.tsx b/src/pages/Account/AccountSetting/index.tsx similarity index 65% rename from src/pages/AccountSetting/index.tsx rename to src/pages/Account/AccountSetting/index.tsx index aee54ca6..5abb4b9f 100644 --- a/src/pages/AccountSetting/index.tsx +++ b/src/pages/Account/AccountSetting/index.tsx @@ -1,19 +1,24 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import theme from '@styles/theme'; + +import { getUserInfoApi } from '@apis/user'; + +import back from '@assets/arrow/left.svg'; +import imageBasic from '@assets/default/defaultProfile.svg'; +import leave from '@assets/default/leave.svg'; +import Profile_s from '@assets/default/my-page.svg'; + +import ConfirmationModal from '@components/ConfirmationModal/index'; +import { OODDFrame } from '@components/Frame/Frame'; +import Loading from '@components/Loading/index'; +import { StyledText } from '@components/Text/StyledText'; +import TopBar from '@components/TopBar/index'; + +import type { UserInfoData } from '@apis/user/dto'; // type 명시 + import { ProfileEditContainer, ProfilePic, ProfilePicWrapper, Label, Row, List, ListItem } from './styles'; -import { OODDFrame } from '../../components/Frame/Frame'; -import ConfirmationModal from '../../components/ConfirmationModal'; -import { StyledText } from '../../components/Text/StyledText'; -import theme from '../../styles/theme'; -import TopBar from '../../components/TopBar'; -import back from '../../assets/arrow/left.svg'; -import imageBasic from '../../assets/default/defaultProfile.svg'; -import Profile_s from './../../assets/default/my-page.svg'; -import leave from '../../assets/default/leave.svg'; -import { getUserInfoApi } from '../../apis/user'; -import { UserInfoData } from '../../apis/user/dto'; -import Loading from '../../components/Loading'; const AccountSetting: React.FC = () => { const navigate = useNavigate(); @@ -63,17 +68,17 @@ const AccountSetting: React.FC = () => { const handleDeleteAccountClick = () => { // 회원 탈퇴 로직 추가 - navigate('/account-cancel'); + navigate('/account/cancel'); }; if (isLoading) { - return ; + return ; } return ( - navigate(-1)} /> + navigate(-1)} /> @@ -81,15 +86,15 @@ const AccountSetting: React.FC = () => { @@ -98,13 +103,13 @@ const AccountSetting: React.FC = () => { 로그아웃 아이콘 - + Logout 회원 탈퇴 아이콘 - + 회원탈퇴 diff --git a/src/pages/AccountSetting/styles.tsx b/src/pages/Account/AccountSetting/styles.tsx similarity index 58% rename from src/pages/AccountSetting/styles.tsx rename to src/pages/Account/AccountSetting/styles.tsx index cbf82ed4..5b1ce005 100644 --- a/src/pages/AccountSetting/styles.tsx +++ b/src/pages/Account/AccountSetting/styles.tsx @@ -1,10 +1,9 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const ProfileEditContainer = styled.div` margin: 0 auto; width: 100%; - flex-grow: 1; /* flexbox에서 공간을 채우도록 설정 */ - + flex-grow: 1; display: flex; flex-direction: column; align-items: center; @@ -14,18 +13,18 @@ export const ProfilePicWrapper = styled.div` display: flex; flex-direction: column; align-items: center; - margin-bottom: 1.25rem; /* 20px */ + margin-bottom: 1.25rem; margin-top: 24px; `; export const ProfilePic = styled.div` - width: 7.25rem; /* 116px */ - height: 7.25rem; /* 116px */ + width: 7.25rem; + height: 7.25rem; flex-shrink: 0; border-radius: 50%; overflow: hidden; - margin-top: 2.125rem; /* 34px */ - margin-bottom: 1.375rem; /* 22px */ + margin-top: 2.125rem; + margin-bottom: 1.375rem; img { width: 100%; @@ -43,7 +42,7 @@ export const Row = styled.div` justify-content: center; align-items: center; width: 100%; - margin-bottom: 10px; + margin-bottom: 10px; ${Label} { width: auto; @@ -51,7 +50,6 @@ export const Row = styled.div` } `; - export const FileInput = styled.input` display: none; `; @@ -59,34 +57,34 @@ export const FileInput = styled.input` export const List = styled.ul` width: 100%; padding: 0; - margin: 0; + margin: 0; list-style: none; - border-top: 0px solid #eee; + border-top: 0px solid ${({ theme }) => theme.colors.background.divider}; position: absolute; - bottom: 20px; + bottom: 20px; `; export const ListItem = styled.li` display: flex; align-items: center; - padding: 15px 1.25rem; /* 15px 20px */ - border-bottom: 0px solid #eee; + padding: 15px 1.25rem; + border-bottom: 0px solid ${({ theme }) => theme.colors.background.divider}; cursor: pointer; & img:first-child { - margin-right: 1rem; /* 첫 번째 이미지(왼쪽 아이콘)의 오른쪽 간격 설정 */ + margin-right: 1rem; } & img:last-child { - margin-left: auto; /* 마지막 이미지(오른쪽 화살표 아이콘)를 오른쪽으로 정렬 */ + margin-left: auto; } &:hover { - background: #f9f9f9; + background: ${({ theme }) => theme.colors.background.secondary}; } span { flex: 1; - text-align: left; /* 텍스트 왼쪽 정렬 */ + text-align: left; } `; diff --git a/src/pages/verification/dto.tsx b/src/pages/Account/Verification/dto.tsx similarity index 100% rename from src/pages/verification/dto.tsx rename to src/pages/Account/Verification/dto.tsx diff --git a/src/pages/verification/index.tsx b/src/pages/Account/Verification/index.tsx similarity index 86% rename from src/pages/verification/index.tsx rename to src/pages/Account/Verification/index.tsx index 6891d6f2..6bf7c02f 100644 --- a/src/pages/verification/index.tsx +++ b/src/pages/Account/Verification/index.tsx @@ -1,4 +1,14 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import theme from '@styles/theme'; + +import back from '@assets/arrow/left.svg'; + +import { OODDFrame } from '@components/Frame/Frame'; +import { StyledText } from '@components/Text/StyledText'; +import TopBar from '@components/TopBar'; + import { Container, Title, @@ -13,12 +23,6 @@ import { InputWrapper, } from './styles'; -import { OODDFrame } from '../../components/Frame/Frame'; -import { StyledText } from '../../components/Text/StyledText'; -import TopBar from '../../components/TopBar'; -import back from '../../assets/arrow/left.svg'; -import { useNavigate } from 'react-router-dom'; - const Verification: React.FC = () => { const navigate = useNavigate(); // useNavigate 훅 사용 @@ -104,11 +108,11 @@ const Verification: React.FC = () => { return ( - navigate(-1)} /> + navigate(-1)} /> - <StyledText $textTheme={{ style: 'body1-medium', lineHeight: 2 }} color="7B7B7B"> + <StyledText $textTheme={{ style: 'body1-medium' }} color={theme.colors.tertiary}> 휴대전화번호로 본인인증하기 </StyledText> @@ -120,7 +124,6 @@ const Verification: React.FC = () => { value={name} onChange={handleNameChange} data-theme-style="heading1-regular" - data-theme-lineheight="1" /> @@ -129,8 +132,7 @@ const Verification: React.FC = () => { placeholder="전화번호" value={phone} onChange={handlePhoneChange} - data-theme-style="body2-light" - data-theme-lineheight="1" + data-theme-style="body1-medium" /> {isVerificationSent && 인증번호 새로 받기} @@ -141,8 +143,7 @@ const Verification: React.FC = () => { placeholder="인증번호를 입력하세요" value={verificationCode} onChange={handleVerificationCodeChange} - data-theme-style="body2-light" - data-theme-lineheight="1" + data-theme-style="body1-medium" /> {formatTime(timer)} diff --git a/src/pages/verification/styles.tsx b/src/pages/Account/Verification/styles.tsx similarity index 68% rename from src/pages/verification/styles.tsx rename to src/pages/Account/Verification/styles.tsx index 48e43123..78d6b9a4 100644 --- a/src/pages/verification/styles.tsx +++ b/src/pages/Account/Verification/styles.tsx @@ -1,24 +1,24 @@ -import styled from 'styled-components'; +import { styled } from 'styled-components'; export const VerificationWrapper = styled.div` margin: 0 auto; width: 100%; - flex-grow: 1; /* flexbox에서 공간을 채우도록 설정 */ - padding: 1.25rem; /* 20px */ + flex-grow: 1; + padding: 1.25rem; display: flex; flex-direction: column; align-items: center; `; export const Container = styled.div` - margin-top: 5px; /* TopBar 높이만큼 위로 밀기 */ + margin-top: 5px; padding: 1rem; `; export const Title = styled.h1` font-size: 1.125rem; margin-bottom: 1.25rem; -s`; +`; export const Form = styled.form` display: flex; @@ -28,7 +28,7 @@ export const Form = styled.form` export const Input = styled.input` padding: 0.625rem; - border: 1px solid #ccc; + border: 1px solid ${({ theme }) => theme.colors.gray[300]}; border-radius: 0.25rem; font-size: 1rem; width: 100%; @@ -45,14 +45,14 @@ export const Button = styled.button` padding: 1.25rem; margin-top: 300px; font-size: 0.875rem; - color: #fff; - background-color: ${({ disabled }) => (disabled ? '#ccc' : '#000')}; + color: ${({ theme }) => theme.colors.contrast}; + background-color: ${({ theme, disabled }) => (disabled ? theme.colors.gray[300] : theme.colors.black)}; border: none; border-radius: 0.3125rem; cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; &:hover { - background-color: ${({ disabled }) => (disabled ? '#ccc' : '#333')}; + background-color: ${({ theme, disabled }) => (disabled ? theme.colors.gray[300] : theme.colors.gray[700])}; } `; @@ -64,7 +64,7 @@ export const VerificationInputWrapper = styled.div` export const VerificationInput = styled.input` padding: 0.625rem; - border: 1px solid #ccc; + border: 1px solid ${({ theme }) => theme.colors.gray[300]}; border-radius: 0.25rem; font-size: 1rem; width: 100%; @@ -76,7 +76,7 @@ export const Timer = styled.div` top: 50%; transform: translateY(-50%); font-size: 1rem; - color: red; + color: ${({ theme }) => theme.colors.red || theme.colors.brand.primary}; `; export const ResendButton = styled.button` @@ -86,7 +86,7 @@ export const ResendButton = styled.button` transform: translateY(-50%); padding: 0.625rem; font-size: 0.875rem; - color: #000; + color: ${({ theme }) => theme.colors.black}; background: none; border: none; cursor: pointer; @@ -94,29 +94,15 @@ export const ResendButton = styled.button` export const StyledInput = styled.input` padding: 0.625rem; - border: 1px solid #ccc; + border: 1px solid ${({ theme }) => theme.colors.gray[300]}; border-radius: 0.25rem; font-size: 1rem; width: 100%; - - ${({ theme }) => theme.fontStyles['heading1-regular']} - - &.body2-light { - font-family: 'Pretendard Variable'; - font-weight: 300; // light - font-size: 1rem; // 16px - } `; export const StyledVerificationInput = styled.input` padding: 0.625rem; - border: 1px solid #ccc; + border: 1px solid ${({ theme }) => theme.colors.gray[300]}; border-radius: 0.25rem; font-size: 1rem; width: 100%; - - &.body2-light { - font-family: 'Pretendard Variable'; - font-weight: 300; // light - font-size: 1rem; // 16px - } `; diff --git a/src/pages/AccountCancel/index.tsx b/src/pages/AccountCancel/index.tsx deleted file mode 100644 index 3877683d..00000000 --- a/src/pages/AccountCancel/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useState } from 'react'; -import { CancelContainer, SubTitle, Text, InfoBox, InfoItem, CheckboxWrapper } from './styles'; -import { StyledText } from '../../components/Text/StyledText'; -import theme from '../../styles/theme'; -import { OODDFrame } from '../../components/Frame/Frame'; -import { useNavigate } from 'react-router-dom'; - -import TopBar from '../../components/TopBar'; -import back from '../../assets/arrow/left.svg'; - -import BottomButton from '../../components/BottomButton'; -import { patchUserWithdrawApi } from '../../apis/user'; -import Modal from '../../components/Modal'; - -const AccountCancel: React.FC = () => { - const [isChecked, setIsChecked] = useState(false); - const [modalContent, setModalContent] = useState(null); - const [isModalVisible, setIsModalVisible] = useState(false); - const navigate = useNavigate(); - - const handleCheckboxChange = () => { - setIsChecked(!isChecked); - }; - - const handleModalClose = () => { - setIsModalVisible(false); - setModalContent(null); - }; - - const handleDeleteAccount = async () => { - try { - if (!isChecked) { - setModalContent('탈퇴 안내사항에 동의해야 합니다.'); - setIsModalVisible(true); - return; - } - - const storedUserId = Number(localStorage.getItem('my_id')); - const token = localStorage.getItem('new_jwt_token'); - - if (!storedUserId || !token) { - setModalContent('사용자 정보를 찾을 수 없습니다.'); - setIsModalVisible(true); - return; - } - - // API 요청 - const response = await patchUserWithdrawApi(storedUserId); - - if (response.isSuccess) { - setModalContent('계정이 성공적으로 삭제되었습니다.'); - setIsModalVisible(true); - - // 계정 삭제 시 localStorage에서 사용자 정보 제거 - localStorage.clear(); - - setTimeout(() => { - navigate('/login'); - }, 2000); - } else { - setModalContent(response.code || '알 수 없는 오류가 발생했습니다.'); - setIsModalVisible(true); - } - } catch (error) { - console.error('계정 삭제하는데 오류남:', error); - setModalContent('계정을 삭제하는 동안 오류가 발생했습니다. 다시 시도해 주세요.'); - setIsModalVisible(true); - } - }; - - return ( - - - navigate(-1)} /> - - - - OOTD 탈퇴 전 확인하세요! - - - - - 탈퇴하시면 이용 중인 서비스가 폐쇄되며, - - - - - 모든 데이터는 복구할 수 없습니다. - - - - - - 지금까지 OODD를 이용해주셔서 감사합니다! - - - - - - - - -
- -
- {isModalVisible && ( - -)} - -
- ); -}; - -export default AccountCancel; diff --git a/src/pages/AccountCancel/styles.tsx b/src/pages/AccountCancel/styles.tsx deleted file mode 100644 index 9b759cf8..00000000 --- a/src/pages/AccountCancel/styles.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import styled from 'styled-components'; - -interface ButtonProps { - isChecked: boolean; -} - -export const CancelContainer = styled.div` - margin: 0 auto; - width: 100%; - flex-grow: 1; /* flexbox에서 공간을 채우도록 설정 */ - padding: 1.25rem; /* 20px */ - display: flex; - flex-direction: column; -`; - -export const SubTitle = styled.h3` - font-size: 1rem; /* 16px */ - font-weight: bold; - margin-bottom: 0.625rem; /* 10px */ - text-align: center; - text-align: left; - margin-top: 10px; - padding: 1.25rem; /* 20px */ -`; - -export const Text = styled.p` - font-size: 0.875rem; /* 14px */ - margin-bottom: 5px; /* 20px */ - text-align: left; - margin-top: 10px; - padding: 0rem 1.25rem; /* 20px */ -`; - -export const InfoBox = styled.div` - background: #f5f5f5; - padding: 70px; /* 20px */ - margin-top: 10px; - border-radius: 10px; - margin: 10px 20px 1.25rem 20px; /* 10px 위 여백, 20px 좌우 여백, 20px 아래 여백 */ -`; - -export const InfoItem = styled.p` - font-size: 0.875rem; /* 14px */ - margin-bottom: 0.625rem; /* 10px */ - padding: 2px 10px; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - height: 100%; /* 부모 컨테이너의 높이에 맞추기 */ -`; - -export const CheckboxWrapper = styled.div` - display: flex; - align-items: center; - margin-bottom: 1.25rem; - padding: 0rem 15px; - - input[type='checkbox'] { - margin-right: 0.625rem; /* 10px */ - border-radius: 50%; - - } -`; - -export const StyledButton = styled.button` - margin-top: 18.75rem; /* 300px */ - background: ${(props) => (props.isChecked ? 'black' : '#ccc')}; - border-radius: 0.5rem; /* 8px */ - border: none; - padding: 1.5625rem; /* 25px */ - text-align: center; - font-size: 1rem; /* 16px */ - color: white; - cursor: ${(props) => (props.isChecked ? 'pointer' : 'not-allowed')}; - - &:disabled { - background: #00000080; - } -`; diff --git a/src/pages/AccountEdit/styles.tsx b/src/pages/AccountEdit/styles.tsx deleted file mode 100644 index ac5ea79b..00000000 --- a/src/pages/AccountEdit/styles.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import styled from 'styled-components'; - -export const ProfileEditContainer = styled.div` - max-width: 512px; /* 32rem */ - display: flex; - flex-direction: column; - position: relative; -`; - -export const Section = styled.div` - margin-top: 1.875rem; /* 30px */ - - margin-bottom: 1.875rem; /* 30px */ - width: 100%; /* Section이 부모 컨테이너의 전체 너비를 차지하도록 설정 */ - padding: 0px 30px; -`; - -export const SectionTitle = styled.div` - font-size: 1.125rem; /* 18px */ - font-weight: bold; - margin-bottom: 0.625rem; /* 10px */ - margin-top: 1.125rem; /* 18px */ - text-align: left; /* 텍스트를 왼쪽 정렬 */ -`; - -export const SNSInfo = styled.div` - display: flex; - flex-direction: column; - margin-bottom: 0.625rem; /* 10px */ - margin-top: 3.125rem; /* 50px */ -`; - -export const SNSInfoRow = styled.div` - display: flex; - align-items: center; - margin-bottom: 0.625rem; /* 10px */ -`; - -export const SNSIcon = styled.img` - width: 2.5rem; /* 40px */ - height: 2.5rem; /* 40px */ - margin-right: 0.625rem; /* 10px */ - margin-top: 1.875rem; - flex-shrink: 0; - object-fit: cover; -`; - -export const Text = styled.div` - font-size: 0.875rem; /* 14px */ - color: #666; - margin-top: 2.1875rem; - text-align: left; /* 텍스트를 왼쪽 정렬 */ -`; - -export const SnsConnection = styled.div` - font-size: 1rem; /* 16px */ - font-weight: bold; - color: #333; - margin-bottom: 0.625rem; /* 10px */ - text-align: left; /* 텍스트를 왼쪽 정렬 */ -`; - -export const MemberInfo = styled.div` - display: flex; - flex-direction: column; - margin-top: 35px; - width: 100%; /* 부모 컨테이너의 전체 너비를 차지하도록 설정 */ -`; - -export const MemberInfoRow = styled.div` - display: flex; - align-items: center; - justify-content: flex-start; /* 아이템들을 왼쪽으로 정렬 */ - margin-bottom: 0.625rem; /* 10px */ - margin-top: 10px; -`; - -export const Label = styled.div` - font-size: 0.875rem; /* 14px */ - color: #333; - display: flex; - align-items: center; - width: 6.25rem; /* 100px, 라벨의 고정 너비 설정 */ -`; - -export const Info = styled.div` - font-size: 0.875rem; /* 14px */ - color: #999; - margin-left: 0.625rem; /* 10px */ - flex-grow: 1; /* 라벨과 함께 라인을 맞추기 위해 넓이를 확장 */ - text-align: left; /* 텍스트를 왼쪽 정렬 */ -`; diff --git a/src/pages/Chats/ChatRoom/ChatBox/index.tsx b/src/pages/Chats/ChatRoom/ChatBox/index.tsx index 553df215..95b85d7d 100644 --- a/src/pages/Chats/ChatRoom/ChatBox/index.tsx +++ b/src/pages/Chats/ChatRoom/ChatBox/index.tsx @@ -1,21 +1,30 @@ -import { ChatBoxContainer, Textarea, SendButton } from './styles'; import { useEffect, useRef, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { useParams } from 'react-router-dom'; -import { OpponentInfoAtom } from '../../../../recoil/util/OpponentInfo'; -import { useSocket } from '../../../../context/SocketProvider'; + +import { useRecoilValue } from 'recoil'; + +import { useSocket } from '@context/SocketProvider'; +import { OtherUserAtom } from '@recoil/util/OtherUser'; +import { getCurrentUserId } from '@utils/getCurrentUserId'; + +import { ChatBoxContainer, Textarea, SendButton } from './styles'; const ChatBox: React.FC = () => { - const opponentInfo = useRecoilValue(OpponentInfoAtom); - const storageValue = localStorage.getItem('my_id'); - const userId = storageValue ? Number(storageValue) : -1; - const { chatRoomId } = useParams(); + const [newMessage, setNewMessage] = useState(''); const textareaRef = useRef(null); + const socket = useSocket(); - const [newMessage, setNewMessage] = useState(''); + const { chatRoomId } = useParams(); + const currentUserId = getCurrentUserId(); + const otherUser = useRecoilValue(OtherUserAtom); + const isOtherUserValid = !!(otherUser && otherUser.id); - const socket = useSocket(); - const isOpponentValid = !!(opponentInfo && opponentInfo.id); + useEffect(() => { + if (textareaRef.current && !isOtherUserValid) { + textareaRef.current.disabled = true; + textareaRef.current.placeholder = '메시지를 보낼 수 없습니다.'; + } + }, []); // textarea 내용에 따라 높이 조정 useEffect(() => { @@ -25,56 +34,48 @@ const ChatBox: React.FC = () => { } }, [newMessage]); - useEffect(() => { - if (textareaRef.current && !isOpponentValid) { - textareaRef.current.disabled = true; - textareaRef.current.placeholder = '메시지를 보낼 수 없습니다.'; - } - }, []); - - const onChangeMessage = (e: React.ChangeEvent): void => { + const handleMessageChange = (e: React.ChangeEvent) => { setNewMessage(e.target.value); }; - const sendNewMessage = (): void => { + const handleEnterKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleNewMessageSubmit(); + } + }; + + const handleNewMessageSubmit = () => { if (newMessage === '') { return; } - // 메시지 전송 + // 메시지 전송 api if (socket) { const sendMessageRequest = { chatRoomId: Number(chatRoomId), - toUserId: opponentInfo?.id, + toUserId: otherUser?.id, content: newMessage, - fromUserId: userId, + fromUserId: currentUserId, createdAt: new Date().toISOString(), }; - socket.emit('sendMessage', sendMessageRequest); setNewMessage(''); } }; - const onKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendNewMessage(); - } - }; - return (