diff --git a/.github/workflows/cd-frontend.yml b/.github/workflows/cd-frontend.yml index f10f5712d..799596283 100644 --- a/.github/workflows/cd-frontend.yml +++ b/.github/workflows/cd-frontend.yml @@ -12,6 +12,4 @@ jobs: steps: - name: deploy run: | - cd ~/deploy && ./deploy-fe.sh - - + cd ~/deploy && ./deploy-fe-dev.sh diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index d9907c57f..53a6ca036 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '20.15.0' + node-version: "20.15.0" - name: Install Dependencies run: npm install --frozen-lockfile @@ -35,6 +35,7 @@ jobs: - name: Create .env file run: | echo "BASE_URL=${{ secrets.BASE_URL }}" > .env + echo "API_BASE_URL=${{ secrets.API_BASE_URL }}" >> .env echo "REACT_APP_GOOGLE_ANALYTICS=${{ secrets.REACT_APP_GOOGLE_ANALYTICS }}" >> .env echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env diff --git a/.gitignore b/.gitignore index 4e8677ffd..48bdd00f3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ /backend/out /frontend/.idea /backend/htmlReport +.DS_Store *.pem backend/src/main/resources/auth/AuthKey.p8 + diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index cfa17dedf..fffbe4481 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -1,11 +1,12 @@ +import { Global, ThemeProvider } from '@emotion/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { initialize, mswDecorator } from 'msw-storybook-addon'; + +import { BrowserRouter } from 'react-router-dom'; import type { Preview } from '@storybook/react'; import React from 'react'; import reset from '../src/common/reset.style'; -import { Global, ThemeProvider } from '@emotion/react'; import { theme } from '../src/common/theme/theme.style'; -import { initialize, mswDecorator } from 'msw-storybook-addon'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { BrowserRouter } from 'react-router-dom'; initialize(); @@ -15,7 +16,6 @@ const preview: Preview = { controls: { matchers: { color: /(background|color)$/i, - date: /Date$/i, }, }, }, diff --git a/frontend/jest.config.json b/frontend/jest.config.json index b0a3761a6..449b84df9 100644 --- a/frontend/jest.config.json +++ b/frontend/jest.config.json @@ -2,8 +2,15 @@ "preset": "ts-jest", "testEnvironment": "jest-environment-jsdom", + "transform": { + "^.+\\.(ts|tsx)$": [ + "ts-jest", + { + "tsconfig": "tsconfig.jest.json" + } + ] + }, - "moduleNameMapper": { "^@_apis/(.*)$": "/src/apis/$1", "^@_constants/(.*)$": "/src/constants/$1", @@ -17,9 +24,12 @@ "^@_routes/(.*)$": "/src/routes/$1", "^@_mocks/(.*)$": "/src/mocks/$1" }, + "testEnvironmentOptions": { "customExportConditions": [""] }, + "setupFiles": ["dotenv/config", "./jest.polyfills.js"], + "setupFilesAfterEnv": ["/jest.setup.ts"] -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cb3c3d3d0..4989388ea 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@sentry/react": "^8.24.0", "@sentry/webpack-plugin": "^2.22.0", "firebase": "^10.12.5", + "heic2any": "^0.0.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-ga4": "^2.1.0", @@ -81,6 +82,7 @@ "typescript": "^5.5.3", "undici": "^6.19.5", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" } @@ -4862,6 +4864,12 @@ "node": ">= 8" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -10653,6 +10661,12 @@ "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==", "dev": true }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -11061,6 +11075,12 @@ "url": "https://dotenvx.com" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", @@ -13412,6 +13432,21 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -13551,6 +13586,11 @@ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", "dev": true }, + "node_modules/heic2any": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz", + "integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -17514,6 +17554,15 @@ "node": ">=0.4.0" } }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -18215,6 +18264,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -20171,6 +20229,20 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -21696,6 +21768,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -22667,6 +22748,86 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 74568bd5d..f4e13a849 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -83,6 +83,7 @@ "typescript": "^5.5.3", "undici": "^6.19.5", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" }, @@ -93,6 +94,7 @@ "@sentry/react": "^8.24.0", "@sentry/webpack-plugin": "^2.22.0", "firebase": "^10.12.5", + "heic2any": "^0.0.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-ga4": "^2.1.0", diff --git a/frontend/public/firebase-messaging-sw.js b/frontend/public/firebase-messaging-sw.js index 34a2fdd96..52cfc43b0 100644 --- a/frontend/public/firebase-messaging-sw.js +++ b/frontend/public/firebase-messaging-sw.js @@ -1,45 +1,47 @@ /* eslint-disable no-undef */ importScripts( - "https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js" + 'https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js', ); importScripts( - "https://www.gstatic.com/firebasejs/10.8.0/firebase-messaging-compat.js" + 'https://www.gstatic.com/firebasejs/10.8.0/firebase-messaging-compat.js', ); importScripts('/firebaseConfig.js'); -self.addEventListener("install", function () { +self.addEventListener('install', function () { self.skipWaiting(); }); -self.addEventListener("activate", function () { - console.log("fcm service worker가 실행되었습니다."); +self.addEventListener('activate', function () { + // console.log("fcm service worker가 실행되었습니다."); }); -self.addEventListener('notificationclick', function(event) { - console.log('[firebase-messaging-sw.js] 알림이 클릭되었습니다.'); +self.addEventListener('notificationclick', function (event) { + // console.log('[firebase-messaging-sw.js] 알림이 클릭되었습니다.'); // 알림 데이터를 가져오기 - const link = event.notification.data.FCM_MSG.notification.click_action; + const link = event.notification.data.FCM_MSG.data.link; event.notification.close(); // 알림 닫기 // 사용자가 알림을 클릭했을 때 해당 링크로 이동 if (link) { event.waitUntil( - clients.matchAll({ type: 'window', includeUncontrolled: true }).then(windowClients => { - // 이미 열린 창이 있는지 확인 - for (let i = 0; i < windowClients.length; i++) { - const client = windowClients[i]; - if (client.url === link && 'focus' in client) { - return client.focus(); + clients + .matchAll({ type: 'window', includeUncontrolled: true }) + .then((windowClients) => { + // 이미 열린 창이 있는지 확인 + for (let i = 0; i < windowClients.length; i++) { + const client = windowClients[i]; + if (client.url === link && 'focus' in client) { + return client.focus(); + } } - } - // 새 창을 열거나 이미 있는 창으로 이동 - if (clients.openWindow) { - return clients.openWindow(link); - } - }) + // 새 창을 열거나 이미 있는 창으로 이동 + if (clients.openWindow) { + return clients.openWindow(link); + } + }), ); } }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a307c65f3..5e1d8cf47 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,11 @@ import { Global, ThemeProvider } from '@emotion/react'; -import { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; +import BackgroundShadow from '@_components/BackgroundShadow/BackgroundShadow'; import { QueryClientProvider } from '@tanstack/react-query'; import { RouterProvider } from 'react-router-dom'; import createQueryClient from './queryClient'; import fonts from '@_common/font.style'; -import { removeInviteCode } from '@_common/inviteCodeManager'; import reset from './common/reset.style'; import router from '@_routes/router'; import { theme } from '@_common/theme/theme.style'; @@ -13,18 +13,15 @@ import { theme } from '@_common/theme/theme.style'; export default function App() { const queryClient = useMemo(createQueryClient, []); - useEffect(() => { - window.addEventListener('beforeunload', removeInviteCode); - - return window.removeEventListener('beforeunload', removeInviteCode); - }); - return ( - - - - - - + <> + + + + + + + + ); } diff --git a/frontend/src/apis/apiClient.ts b/frontend/src/apis/apiClient.ts index 6022682f5..41ddb0199 100644 --- a/frontend/src/apis/apiClient.ts +++ b/frontend/src/apis/apiClient.ts @@ -1,48 +1,50 @@ import { ApiError } from '@_utils/customError/ApiError'; -import { getLastDarakbangId } from '@_common/lastDarakbangManager'; -import { getToken } from '@_utils/tokenManager'; +import { getAccessToken } from '@_utils/tokenManager'; +import { addBaseUrl } from './endPoints'; type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; -const DEFAULT_HEADERS = { - 'Content-Type': 'application/json', -}; +// const DEFAULT_HEADERS = { +// 'Content-Type': 'application/json', +// }; -const BASE_URL = `${process.env.BASE_URL}/v1`; +function getHeaders(isRequiredAuth: boolean, isFormData: boolean = false) { + const headers = new Headers(); -function addBaseUrl(endpoint: string, isNeedLastDarakbang: boolean = false) { - if (isNeedLastDarakbang) - endpoint = '/darakbang/' + (getLastDarakbangId() || 0) + endpoint; - return BASE_URL + endpoint; -} + // FormData가 아닌 경우에만 Content-Type 설정 + if (!isFormData) { + headers.append('Content-Type', 'application/json'); + } -function getHeaders(isRequiredAuth: boolean) { - const headers = new Headers(DEFAULT_HEADERS); + // 인증이 필요한 경우 Authorization 헤더 추가 if (isRequiredAuth) { - const token = getToken(); + const token = getAccessToken(); headers.append('Authorization', `Bearer ${token}`); } + return headers; } async function request( method: Method, endpoint: string, - data: object = {}, + data: object | FormData = {}, config: RequestInit = {}, isRequiredAuth: boolean = false, isRequiredLastDarakbang: boolean = false, ) { const url = addBaseUrl(endpoint, isRequiredLastDarakbang); - + // data가 FormData인지 확인하여 헤더 설정 + const isFormData = data instanceof FormData; + const headers = getHeaders(isRequiredAuth, isFormData); const options: RequestInit = { method, - headers: getHeaders(isRequiredAuth), + headers: headers, ...config, }; if (method !== 'GET') { - options.body = JSON.stringify(data); + options.body = isFormData ? data : JSON.stringify(data); } const response = await fetch(url, options); @@ -73,7 +75,7 @@ async function get( async function post( endpoint: string, - data: object = {}, + data: object | FormData = {}, config: RequestInit = {}, isRequiredAuth: boolean = false, isRequiredLastDarakbang: boolean = false, @@ -169,7 +171,7 @@ const ApiClient = { }, async postWithLastDarakbangId( endpoint: string, - data: object = {}, + data: object | FormData = {}, config: RequestInit = {}, ) { return post(endpoint, data, config, true, true); diff --git a/frontend/src/apis/auth.ts b/frontend/src/apis/auth.ts index 5b77c6e66..da3cdf0bb 100644 --- a/frontend/src/apis/auth.ts +++ b/frontend/src/apis/auth.ts @@ -1,17 +1,17 @@ import ApiClient from './apiClient'; -export const login = async (loginInputInfo: { nickname: string }) => { - const response = await ApiClient.postWithoutAuth( - '/auth/login', - loginInputInfo, - ); - return response.json(); -}; - export const kakaoOAuth = async (code: string) => { - const response = await ApiClient.postWithoutAuth('/auth/kakao/oauth', { + await ApiClient.postWithAuth('/auth/kakao', { code, }); - console.log(response); +}; +export const googleOAuth = async ( + idToken: string, + memberId: string | null = null, +) => { + const response = await ApiClient.postWithoutAuth('/auth/google', { + idToken, + memberId, + }); return response.json(); }; diff --git a/frontend/src/apis/deletes.ts b/frontend/src/apis/deletes.ts index 9126fbd0e..e3c5405ca 100644 --- a/frontend/src/apis/deletes.ts +++ b/frontend/src/apis/deletes.ts @@ -5,3 +5,7 @@ export const deleteCancelChamyo = async (moimId: number) => { moimId, }); }; + +export const deleteMyInfo = async () => { + await ApiClient.deleteWithAuth(`/auth`); +}; diff --git a/frontend/src/apis/endPoints.ts b/frontend/src/apis/endPoints.ts index dde077503..589b4f9cf 100644 --- a/frontend/src/apis/endPoints.ts +++ b/frontend/src/apis/endPoints.ts @@ -1,16 +1,55 @@ -const getEndpoint = (string: string) => { - return `${process.env.BASE_URL}/${string}`; +import { getLastDarakbangId } from '@_common/lastDarakbangManager'; + +const API_BASE_URL = `${process.env.API_BASE_URL}/v1`; +export function addBaseUrl( + endpoint: string, + isNeedLastDarakbang: boolean = false, +) { + if (isNeedLastDarakbang) + endpoint = '/darakbang/' + (getLastDarakbangId() || 0) + endpoint; + return API_BASE_URL + endpoint; +} + +const API_URL = { + darakbang: { + role: addBaseUrl('/role', true), + mine: addBaseUrl('/darakbang/mine', false), + name: addBaseUrl('', true), + }, + moim: addBaseUrl('/moim', true), + moims: addBaseUrl('/moim', true), + auth: addBaseUrl('/auth', true), + chamyo: addBaseUrl('/chamyo', true), + chat: addBaseUrl('/chat', true), + zzim: addBaseUrl('/zzim', true), + interest: addBaseUrl('/interest', true), + please: addBaseUrl('/please', true), + notification: addBaseUrl('/notification', true), + bet: { + all: addBaseUrl('/bet', true), + detail: (betId: number) => addBaseUrl(`/bet/${betId}`, true), + create: addBaseUrl('/bet', true), + participate: (betId: number) => addBaseUrl(`/bet/${betId}`, true), + result: (betId: number) => addBaseUrl(`/bet/${betId}/result`, true), + }, + kakaoOAuth: addBaseUrl('/auth/kakao/oauth', false), + myInfo: addBaseUrl('/member/mine', true), + + profile: (darakbangMemberId: number) => + addBaseUrl(`/members/${darakbangMemberId}/profile`, true), }; const ENDPOINTS = { - moim: getEndpoint('v1/moim'), - moims: getEndpoint('v1/moim'), - auth: getEndpoint('v1/auth'), - chamyo: getEndpoint('v1/chamyo'), - chat: getEndpoint('v1/chat'), - zzim: getEndpoint('v1/zzim'), - interest: getEndpoint('v1/interest'), - please: getEndpoint('v1/please'), - notification: getEndpoint('v1/notification'), + moim: 'moim', + moims: 'moim', + auth: 'auth', + chamyo: 'chamyo', + chat: 'chat', + zzim: 'zzim', + interest: 'interest', + please: 'please', + notification: 'notification', + bet: 'bet', }; -export default ENDPOINTS; + +export { ENDPOINTS, API_URL }; diff --git a/frontend/src/apis/gets.ts b/frontend/src/apis/gets.ts index 89d161672..4b9c16c15 100644 --- a/frontend/src/apis/gets.ts +++ b/frontend/src/apis/gets.ts @@ -1,16 +1,25 @@ import { + BetChatRoomDetail, Chat, + ChatRoomDetail, + ChatRoomType, ChattingPreview, + MoimChatRoomDetail, MoimInfo, Participation, Role, } from '@_types/index'; import { + GetBet, + GetBetDetail, + GetBets, GetChamyoAll, GetChamyoMine, GetChat, + GetChatRoomDetail, GetChattingPreview, GetDarakbangInviteCode, + GetDarakbangMemberProfile, GetDarakbangMembers, GetDarakbangMine, GetDarakbangNameByCode, @@ -24,8 +33,8 @@ import { } from './responseTypes'; import ApiClient from './apiClient'; -import { Filter } from '@_components/MyMoimListFilters/MyMoimListFilters'; import { ApiError } from '@_utils/customError/ApiError'; +import { Filter } from '@_pages/Moim/MainPage/components/HomeMainContent/MyMoim/MyMoimListFilters/MyMoimListFilters'; export const getMoims = async (): Promise => { const response = await ApiClient.getWithLastDarakbangId('/moim'); @@ -59,19 +68,38 @@ export const getMoim = async (moimId: number): Promise => { return json.data; }; -export const getChatPreview = async (): Promise => { - const response = await ApiClient.getWithLastDarakbangId(`/chat/preview`); +export const getChatPreview = async ( + chatRoomType: ChatRoomType, +): Promise => { + const response = await ApiClient.getWithLastDarakbangId( + `/chatroom/preview?chatRoomType=${chatRoomType}`, + ); const json: GetChattingPreview = await response.json(); - return json.data.chatPreviewResponses; + return json.data.previews; +}; + +export const getChatRoomDetail = async ( + chatRoomId: number, +): Promise => { + const response = await ApiClient.getWithLastDarakbangId( + `/chatroom/${chatRoomId}/details`, + ); + + const json: GetChatRoomDetail = await response.json(); + const chatRoomDetail = json.data; + if (chatRoomDetail.type === 'BET') return chatRoomDetail as BetChatRoomDetail; + if (chatRoomDetail.type === 'MOIM') + return chatRoomDetail as MoimChatRoomDetail; + return chatRoomDetail; }; export const getChat = async ( - moimId: number, + chatRoomId: number, recentChatId?: number, ): Promise => { const response = await ApiClient.getWithLastDarakbangId( - `/chat?moimId=${moimId}&recentChatId=${recentChatId || 0}`, + `/chatroom/${chatRoomId}?recentChatId=${recentChatId || 0}`, ); const json: GetChat = await response.json(); @@ -153,7 +181,7 @@ export const getDarakbangMembers = async () => { const response = await ApiClient.getWithLastDarakbangId('/members'); const json: GetDarakbangMembers = await response.json(); - return json.data.darakbangMemberResponses; + return json.data.responses; }; export const getDarakbangInviteCode = async () => { @@ -187,3 +215,35 @@ export const getDarakbangNameById = async () => { const json: GetDarakbangNameByCode = await response.json(); return json.data.name; }; + +export const getBets = async () => { + const response = await ApiClient.getWithLastDarakbangId('/bet'); + + const json: GetBets = await response.json(); + return json.data.bets; +}; + +export const getBet = async (betId: number) => { + const response = await ApiClient.getWithLastDarakbangId(`/bet/${betId}`); + + const json: GetBet = await response.json(); + return json.data; +}; + +export const getBetResult = async (betId: number) => { + const response = await ApiClient.getWithLastDarakbangId( + `/bet/${betId}/result`, + ); + + const json: GetBetDetail = await response.json(); + return json.data.nickname; +}; + +export const getDarakbangMemberProfile = async (darakbangMemberId: number) => { + const response = await ApiClient.getWithLastDarakbangId( + `/members/${darakbangMemberId}/profile`, + ); + + const json: GetDarakbangMemberProfile = await response.json(); + return json.data; +}; diff --git a/frontend/src/apis/patches.ts b/frontend/src/apis/patches.ts index f7930b6c8..1bc2320a9 100644 --- a/frontend/src/apis/patches.ts +++ b/frontend/src/apis/patches.ts @@ -35,5 +35,9 @@ export const patchReopenMoim = async (moimId: number) => { }; export const patchOpenChat = async (moimId: number) => { - await ApiClient.patchWithLastDarakbangId(`/chat/open?moimId=${moimId}`); + const response = await ApiClient.patchWithLastDarakbangId( + `/chatroom/open?moimId=${moimId}`, + ); + const json = await response.json(); + return json.data; }; diff --git a/frontend/src/apis/posts.ts b/frontend/src/apis/posts.ts index 67f8d5cf2..1aaff80d5 100644 --- a/frontend/src/apis/posts.ts +++ b/frontend/src/apis/posts.ts @@ -1,5 +1,5 @@ -import { MoimInputInfo, PleaseInfoInput } from '@_types/index'; -import { PostMoim, PostMoimBody } from './responseTypes'; +import { BetInputInfo, MoimInputInfo, PleaseInfoInput } from '@_types/index'; +import { PostBet, PostMoim, PostMoimBody } from './responseTypes'; import ApiClient from './apiClient'; @@ -45,9 +45,8 @@ export const postWriteComment = async ( } }; -export const postChat = async (moimId: number, content: string) => { - await ApiClient.postWithLastDarakbangId('/chat', { - moimId, +export const postChat = async (chatRoomId: number, content: string) => { + await ApiClient.postWithLastDarakbangId(`/chatroom/${chatRoomId}`, { content, }); }; @@ -69,22 +68,23 @@ export const postLastReadChatId = async ( }; export const postConfirmDatetime = async ( - moimId: number, + chatRoomId: number, date: string, time: string, ) => { - await ApiClient.postWithLastDarakbangId('/chat/datetime', { - moimId, + await ApiClient.postWithLastDarakbangId(`/chatroom/${chatRoomId}/datetime`, { date, time, }); + return chatRoomId; }; -export const postConfirmPlace = async (moimId: number, place: string) => { - await ApiClient.postWithLastDarakbangId('/chat/place', { - moimId, +export const postConfirmPlace = async (chatRoomId: number, place: string) => { + await ApiClient.postWithLastDarakbangId(`/chatroom/${chatRoomId}/place`, { place, }); + + return chatRoomId; }; export const postPlease = async (please: PleaseInfoInput) => { @@ -132,3 +132,22 @@ export const postDarakbangEntrance = async ({ return json.data as number; }; + +export const postBet = async (bet: BetInputInfo) => { + const data = await ApiClient.postWithLastDarakbangId('/bet', bet); + + const json: PostBet = await data.json(); + return json.data.betId; +}; + +export const postBetResult = async (betId: number) => { + await ApiClient.postWithLastDarakbangId(`/bet/${betId}/result`); +}; + +export const postJoinBet = async (betId: number) => { + await ApiClient.postWithLastDarakbangId(`/bet/${betId}`); +}; + +export const patchMyInfo = async (myInfo: FormData) => { + await ApiClient.postWithLastDarakbangId(`/member/mine`, myInfo); +}; diff --git a/frontend/src/apis/responseTypes.ts b/frontend/src/apis/responseTypes.ts index 6a9e6acdd..1afb1df48 100644 --- a/frontend/src/apis/responseTypes.ts +++ b/frontend/src/apis/responseTypes.ts @@ -1,5 +1,8 @@ import { + BetDetail, + BetSummary, Chat, + ChatRoomDetail, ChattingPreview, Darakbang, DarakbangRole, @@ -31,7 +34,11 @@ export interface PostMoim { } export interface GetChattingPreview { - data: { chatPreviewResponses: ChattingPreview[] }; + data: { previews: ChattingPreview[] }; +} + +export interface GetChatRoomDetail { + data: ChatRoomDetail; } export interface GetChat { data: { chats: Chat[] }; @@ -63,8 +70,10 @@ export interface GetPleases { export interface GetMyInfo { data: { + name: string; nickname: string; profile: string; + description: string; }; } @@ -88,8 +97,8 @@ export interface GetMyRoleInDarakbang { export interface GetDarakbangMembers { data: { - darakbangMemberResponses: { - memberId: number; + responses: { + darakbangMemberId: number; nickname: string; profile: string; }[]; @@ -107,3 +116,33 @@ export interface GetDarakbangNameByCode { name: string; }; } + +export interface GetBets { + data: { bets: BetSummary[] }; +} + +export interface GetBet { + data: BetDetail; +} + +export interface GetBetDetail { + data: { + nickname: string; + }; +} + +export interface PostBet { + data: { + betId: number; + }; +} + +export interface GetDarakbangMemberProfile { + data: { + darakbangMemberId: number; + name: string; + nickname: string; + profile: string; + description: string; + }; +} diff --git a/frontend/src/common/assets/apple_cirecle.svg b/frontend/src/common/assets/apple_cirecle.svg new file mode 100644 index 000000000..129960286 --- /dev/null +++ b/frontend/src/common/assets/apple_cirecle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/common/assets/default_profile.png b/frontend/src/common/assets/default_profile.png deleted file mode 100644 index 3143fc761..000000000 Binary files a/frontend/src/common/assets/default_profile.png and /dev/null differ diff --git a/frontend/src/common/assets/default_profile.svg b/frontend/src/common/assets/default_profile.svg new file mode 100644 index 000000000..92d57c053 --- /dev/null +++ b/frontend/src/common/assets/default_profile.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/common/assets/edit.svg b/frontend/src/common/assets/edit.svg new file mode 100644 index 000000000..7da343920 --- /dev/null +++ b/frontend/src/common/assets/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/common/assets/empty_profile.svg b/frontend/src/common/assets/empty_profile.svg deleted file mode 100644 index ecb9cc7fa..000000000 --- a/frontend/src/common/assets/empty_profile.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/src/common/assets/fonts/DNFBitBitTTF.ttf b/frontend/src/common/assets/fonts/DNFBitBitTTF.ttf new file mode 100644 index 000000000..276c54824 Binary files /dev/null and b/frontend/src/common/assets/fonts/DNFBitBitTTF.ttf differ diff --git a/frontend/src/common/assets/fonts/PartialSansKR/PartialSansKR-Regular.otf b/frontend/src/common/assets/fonts/PartialSansKR/PartialSansKR-Regular.otf new file mode 100644 index 000000000..a51f1d7b9 Binary files /dev/null and b/frontend/src/common/assets/fonts/PartialSansKR/PartialSansKR-Regular.otf differ diff --git a/frontend/src/common/assets/fonts/PartialSansKR/PartialSansKR-Regular.woff2 b/frontend/src/common/assets/fonts/PartialSansKR/PartialSansKR-Regular.woff2 new file mode 100644 index 000000000..40937142b Binary files /dev/null and b/frontend/src/common/assets/fonts/PartialSansKR/PartialSansKR-Regular.woff2 differ diff --git a/frontend/src/common/assets/fonts/woff2/PretendardVariable.woff2 b/frontend/src/common/assets/fonts/woff2/PretendardVariable.woff2 deleted file mode 100644 index 49c54b515..000000000 Binary files a/frontend/src/common/assets/fonts/woff2/PretendardVariable.woff2 and /dev/null differ diff --git a/frontend/src/common/assets/googleLogin.svg b/frontend/src/common/assets/googleLogin.svg new file mode 100644 index 000000000..2cba6b9a7 --- /dev/null +++ b/frontend/src/common/assets/googleLogin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/common/assets/happy_logo.svg b/frontend/src/common/assets/happy_logo.svg new file mode 100644 index 000000000..42d9ad155 --- /dev/null +++ b/frontend/src/common/assets/happy_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/common/assets/kakaoCircleOuathLogin.svg b/frontend/src/common/assets/kakaoCircleOuathLogin.svg new file mode 100644 index 000000000..253a1318c --- /dev/null +++ b/frontend/src/common/assets/kakaoCircleOuathLogin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/common/assets/loading.gif b/frontend/src/common/assets/loading.gif new file mode 100644 index 000000000..c5126ed9c Binary files /dev/null and b/frontend/src/common/assets/loading.gif differ diff --git a/frontend/src/common/assets/main_logo.svg b/frontend/src/common/assets/main_logo.svg new file mode 100644 index 000000000..c28fbe176 --- /dev/null +++ b/frontend/src/common/assets/main_logo.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/common/assets/missing_logo.svg b/frontend/src/common/assets/missing_logo.svg new file mode 100644 index 000000000..2823fd65f --- /dev/null +++ b/frontend/src/common/assets/missing_logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/common/assets/refresh.svg b/frontend/src/common/assets/refresh.svg new file mode 100644 index 000000000..ebb7d37b3 --- /dev/null +++ b/frontend/src/common/assets/refresh.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/common/assets/regret_cat.png b/frontend/src/common/assets/regret_cat.png deleted file mode 100644 index c63876d37..000000000 Binary files a/frontend/src/common/assets/regret_cat.png and /dev/null differ diff --git a/frontend/src/common/assets/regret_cat.webp b/frontend/src/common/assets/regret_cat.webp new file mode 100644 index 000000000..afcdd236f Binary files /dev/null and b/frontend/src/common/assets/regret_cat.webp differ diff --git a/frontend/src/common/assets/setting.svg b/frontend/src/common/assets/setting.svg new file mode 100644 index 000000000..b443fe125 --- /dev/null +++ b/frontend/src/common/assets/setting.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/common/common.style.ts b/frontend/src/common/common.style.ts index 6b34ab544..2feb14c81 100644 --- a/frontend/src/common/common.style.ts +++ b/frontend/src/common/common.style.ts @@ -1,10 +1,23 @@ import { css } from '@emotion/react'; export const common = { - nonScroll: css` + nonDrag: css` user-select: none; `, cursorPointer: css` cursor: pointer; `, + nonScroll: css` + &::-webkit-scrollbar { + display: none; + } + `, + iphoneBottom: css` + ${navigator.userAgent.toLowerCase().includes('iphone') + ? `/* stylelint-disable */ + padding-bottom: constant(safe-area-inset-bottom); + /* stylelint-enable */ + 'padding-bottom: env(safe-area-inset-bottom);` + : ''} + `, }; diff --git a/frontend/src/common/font.style.ts b/frontend/src/common/font.style.ts index 666f43481..856984f6c 100644 --- a/frontend/src/common/font.style.ts +++ b/frontend/src/common/font.style.ts @@ -1,4 +1,6 @@ +import bitbit from './assets/fonts/DNFBitBitTTF.ttf'; import { css } from '@emotion/react'; +import partialSansKRRegular from './assets/fonts/PartialSansKR/PartialSansKR-Regular.woff2'; import pretendardBlackWoff2 from './assets/fonts/woff2-subset/Pretendard-Black.subset.woff2'; import pretendardBoldWoff2 from './assets/fonts/woff2-subset/Pretendard-Bold.subset.woff2'; import pretendardExtraBoldWoff2 from './assets/fonts/woff2-subset/Pretendard-ExtraBold.subset.woff2'; @@ -8,7 +10,6 @@ import pretendardMediumWoff2 from './assets/fonts/woff2-subset/Pretendard-Medium import pretendardRegularWoff2 from './assets/fonts/woff2-subset/Pretendard-Regular.subset.woff2'; import pretendardSemiboldWoff2 from './assets/fonts/woff2-subset/Pretendard-SemiBold.subset.woff2'; import pretendardThinWoff2 from './assets/fonts/woff2-subset/Pretendard-Thin.subset.woff2'; -import pretendardVariableWoff2 from './assets/fonts/woff2/PretendardVariable.woff2'; const fonts = css` @font-face { @@ -78,11 +79,15 @@ const fonts = css` } @font-face { - font-family: 'Pretendard Variable'; - font-weight: 45 920; - font-style: normal; + font-family: PartialSansKR; + font-weight: 400; font-display: swap; - src: url(${pretendardVariableWoff2}) format('woff2-variations'); + src: url(${partialSansKRRegular}) format('woff2'); + } + + @font-face { + font-family: bitbit; + src: url(${bitbit}); } `; diff --git a/frontend/src/common/getRoutes.ts b/frontend/src/common/getRoutes.ts index 7049b8938..4405fde76 100644 --- a/frontend/src/common/getRoutes.ts +++ b/frontend/src/common/getRoutes.ts @@ -14,17 +14,39 @@ const GET_ROUTES = { moimParticipateComplete: () => getNowDarakbangRoute() + '/moim/participation-complete', modify: (moimId: number) => getNowDarakbangRoute() + '/modify/' + moimId, + chat: () => getNowDarakbangRoute() + '/chat', chattingRoom: (moimId: number) => getNowDarakbangRoute() + '/chatting-room/' + moimId, + please: () => getNowDarakbangRoute() + '/please', addPlease: () => getNowDarakbangRoute() + '/please/creation', + myPage: () => getNowDarakbangRoute() + '/my-page', + notification: () => getNowDarakbangRoute() + '/notification', + darakbangManagement: () => getNowDarakbangRoute() + '/darakbang-management', darakbangMembers: () => getNowDarakbangRoute() + '/darakbang-members', darakbangInvitation: () => getNowDarakbangRoute() + '/darakbang-invitation', darakbangLanding: () => getNowDarakbangRoute() + '/darakbang-landing', + + bet: () => getNowDarakbangRoute() + '/bet', + betCreation: () => getNowDarakbangRoute() + '/bet/creation', + betDetail: (betId: number) => getNowDarakbangRoute() + '/bet/' + betId, + betResult: (betId: number) => + getNowDarakbangRoute() + '/bet/' + betId + '/result', + setting: () => getNowDarakbangRoute() + '/my-page/setting', + }, + default: { + notFound: '/*', + main: '/', + home: '/home', + kakaoSelection: '/oauth-migration', + oAuthSelection: '/oauth-select', + resultMigration: '/oauth-migration', + oAuthGoogle: '/oauth', + oAuth: '/oauth/:provider', }, }; export default GET_ROUTES; diff --git a/frontend/src/common/lastDarakbangManager.tsx b/frontend/src/common/lastDarakbangManager.tsx index 2788ce1bc..52479edea 100644 --- a/frontend/src/common/lastDarakbangManager.tsx +++ b/frontend/src/common/lastDarakbangManager.tsx @@ -7,5 +7,6 @@ export const setLastDarakbangId = (lastDarakbangId: number): void => { export const getLastDarakbangId = () => { const lastDarakbangId = localStorage.getItem(LAST_DARAKBANG_ID_KEY); if (!lastDarakbangId) return null; + else if (process.env.MSW === 'true') return 0; return +lastDarakbangId; }; diff --git a/frontend/src/common/reset.style.ts b/frontend/src/common/reset.style.ts index ab4045a8b..975029632 100644 --- a/frontend/src/common/reset.style.ts +++ b/frontend/src/common/reset.style.ts @@ -111,6 +111,7 @@ const reset = css` body { position: relative; + overflow-y: scroll; ${layout.default} margin: 0 auto; line-height: 1; diff --git a/frontend/src/common/theme/theme.type.ts b/frontend/src/common/theme/theme.type.ts index c8823d499..02f48f236 100644 --- a/frontend/src/common/theme/theme.type.ts +++ b/frontend/src/common/theme/theme.type.ts @@ -40,6 +40,7 @@ export interface Typography { small: SerializedStyles; Tiny: SerializedStyles; tag: SerializedStyles; + partialSansKR: SerializedStyles; } export interface ColoredTypography { @@ -66,6 +67,7 @@ export interface ColoredTypography { small: (fontColor: string | SerializedStyles) => SerializedStyles; Tiny: (fontColor: string | SerializedStyles) => SerializedStyles; tag: (fontColor: string | SerializedStyles) => SerializedStyles; + partialSansKR: (fontColor: string | SerializedStyles) => SerializedStyles; } export interface Layout { default: SerializedStyles; diff --git a/frontend/src/common/theme/typography.ts b/frontend/src/common/theme/typography.ts index ce6ed78b8..a8cfd1b98 100644 --- a/frontend/src/common/theme/typography.ts +++ b/frontend/src/common/theme/typography.ts @@ -5,110 +5,110 @@ const typography: Typography = { h1: css` font-family: Pretendard; font-size: 4.8rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 5.8rem; `, h2: css` font-family: Pretendard; font-size: 4rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 4.8rem; `, h3: css` font-family: Pretendard; font-size: 3.2rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 3.8rem; `, h4: css` font-family: Pretendard; font-size: 2.8rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 3.4rem; `, h5: css` font-family: Pretendard; font-size: 2.4rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 2.8rem; `, s1: css` font-family: Pretendard; font-size: 1.8rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 2.8rem; `, s2: css` font-family: Pretendard; font-size: 1.6rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 2.4rem; `, b1: css` font-family: Pretendard; font-size: 1.6rem; - font-style: normal; font-weight: 500; + font-style: normal; line-height: 2.4rem; `, b2: css` font-family: Pretendard; font-size: 1.6rem; - font-style: normal; font-weight: 400; + font-style: normal; line-height: 2.4rem; `, b3: css` font-family: Pretendard; font-size: 1.4rem; - font-style: normal; font-weight: 500; + font-style: normal; line-height: 2rem; `, b4: css` font-family: Pretendard; font-size: 1.4rem; - font-style: normal; font-weight: 400; + font-style: normal; line-height: 2rem; `, c1: css` font-family: Pretendard; font-size: 1.2rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 1.6rem; `, c2: css` font-family: Pretendard; font-size: 1.2rem; - font-style: normal; font-weight: 400; + font-style: normal; line-height: 1.6rem; `, c3: css` font-family: Pretendard; font-size: 1rem; - font-style: normal; font-weight: 400; + font-style: normal; line-height: 1.4rem; `, label: css` font-family: Pretendard; font-size: 1.2rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 1.6rem; text-transform: uppercase; `, @@ -116,62 +116,68 @@ const typography: Typography = { ButtonFont: css` font-family: Inter; font-size: 2.4rem; - font-style: normal; font-weight: 600; + font-style: normal; line-height: 2.8rem; `, Typeface: css` font-family: Inter; font-size: 1.6rem; - font-style: normal; font-weight: 400; + font-style: normal; line-height: 2.4rem; `, Giant: css` font-family: Pretendard; font-size: 1.8rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 2.4rem; `, Large: css` font-family: Pretendard; font-size: 1.6rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 2rem; `, Medium: css` font-family: Pretendard; font-size: 1.4rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 1.6rem; `, small: css` font-family: Pretendard; font-size: 1.2rem; - font-style: normal; font-weight: 700; + font-style: normal; line-height: 1.6rem; `, Tiny: css` font-family: Pretendard; font-size: 1rem; - font-style: normal; font-weight: 800; + font-style: normal; line-height: 1.2rem; `, tag: css` font-family: Pretendard; font-size: 1.2rem; - font-style: normal; font-weight: 600; + font-style: normal; line-height: 130%; `, + partialSansKR: css` + font-family: PartialSansKR; + font-size: 2rem; + font-weight: 400; + font-style: normal; + `, }; export default typography; diff --git a/frontend/src/components/BackgroundShadow/BackgroundShadow.styles.ts b/frontend/src/components/BackgroundShadow/BackgroundShadow.styles.ts new file mode 100644 index 000000000..403564415 --- /dev/null +++ b/frontend/src/components/BackgroundShadow/BackgroundShadow.styles.ts @@ -0,0 +1,19 @@ +import { css } from '@emotion/react'; + +export const shadow = css` + position: fixed; + z-index: -1; + top: 0; + left: 50%; + transform: translateX(-50%); + + width: 600px; + height: 100vh; + margin: 0 auto; + + line-height: 1; + + box-shadow: + 0 10px 20px rgb(0 0 0 / 19%), + 0 6px 6px rgb(0 0 0 / 23%); +`; diff --git a/frontend/src/components/BackgroundShadow/BackgroundShadow.tsx b/frontend/src/components/BackgroundShadow/BackgroundShadow.tsx new file mode 100644 index 000000000..34cfcf150 --- /dev/null +++ b/frontend/src/components/BackgroundShadow/BackgroundShadow.tsx @@ -0,0 +1,5 @@ +import * as S from './BackgroundShadow.styles'; + +export default function BackgroundShadow() { + return
; +} diff --git a/frontend/src/components/BottomSheet/BottomSheet.stories.tsx b/frontend/src/components/BottomSheet/BottomSheet.stories.tsx new file mode 100644 index 000000000..a9c3f5a48 --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheet.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import BottomSheet from './BottomSheet'; +import Button from '@_components/Button/Button'; + +const meta = { + component: BottomSheet, + title: 'Components/BottomSheet', +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isOpen: true, + onDimmerClick: () => {}, + header: Header, + cta: ( + + + + ), + }, + render: (args) => ( + + Content + + ), +}; diff --git a/frontend/src/components/BottomSheet/BottomSheet.tsx b/frontend/src/components/BottomSheet/BottomSheet.tsx new file mode 100644 index 000000000..4fd37b2ea --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheet.tsx @@ -0,0 +1,136 @@ +import Dimmer from '@_components/Dimmer/Dimmer'; +import { PropsWithChildren, useEffect, useState } from 'react'; +import BottomSheetContainer from './BottomSheetContainer/BottomSheetContainer'; +import BottomSheetBody from './BottomSheetBody/BottomSheetBody'; +import BottomSheetHandle from './BottomSheetHandle/BottomSheetHandle'; + +interface BottomSheetProps { + isOpen: boolean; + onDimmerClick: () => void; + + header?: React.ReactNode; + cta?: React.ReactNode; + + size?: 'small' | 'medium' | 'large' | 'full'; +} + +export default function BottomSheet( + props: PropsWithChildren, +) { + const { isOpen, onDimmerClick, header, cta, children, size } = props; + + const [startY, setStartY] = useState(0); // 터치 시작 Y좌표 + const [currentY, setCurrentY] = useState(window.innerHeight); // 초기에는 화면 아래에 위치 + const [isDragging, setIsDragging] = useState(false); // 드래그 중인지 여부 + const [isClosing, setIsClosing] = useState(false); // 닫히는 애니메이션 여부 + + // Bottom Sheet가 열릴 때 애니메이션으로 위로 올라옴 + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; // 스크롤 비활성화 + + // 열릴 때 100px 아래에서 0으로 부드럽게 올라오도록 애니메이션 시작 + setTimeout(() => { + setCurrentY(0); // Y값을 0으로 변경 -> 밑에서 위로 올라오는 애니메이션 + }, 10); // 딜레이를 줘야 애니메이션이 자연스럽게 동작 + } else { + setCurrentY(window.innerHeight); // 닫힐 때 다시 화면 아래로 + document.body.style.overflow = 'auto'; // 스크롤 활성화 + } + + return () => { + document.body.style.overflow = 'auto'; + }; + }, [isOpen]); + + useEffect(() => { + if (!isOpen) { + setCurrentY(window.innerHeight); // 닫힐 때 높이를 초기화 + setIsClosing(false); // 닫힘 애니메이션 초기화 + } + }, [isOpen]); + + // Dimmer 클릭 시 애니메이션으로 밑으로 내려간 후 닫힘 + const handleDimmerClick = () => { + setIsClosing(true); + setCurrentY(window.innerHeight); // 화면 아래로 내려가는 애니메이션 + setTimeout(() => { + onDimmerClick(); // 300ms 후 실제로 닫기 + }, 300); // 애니메이션 시간이 0.3초이므로 300ms 후에 닫음 + }; + + // 드래그 시작 + const handleTouchStart = (event: React.TouchEvent) => { + setStartY(event.touches[0].clientY); + setIsDragging(true); + }; + + // 드래그 중 + const handleTouchMove = (event: React.TouchEvent) => { + if (!isDragging) return; + + const currentTouchY = event.touches[0].clientY; + const deltaY = currentTouchY - startY; + + // Y축으로만 움직임을 감지 + if (deltaY > 0) { + setCurrentY(deltaY); // 드래그된 만큼 값을 저장 + } + }; + + // 드래그 종료 + const handleTouchEnd = () => { + if (!isDragging) return; + setIsDragging(false); + + // 드래그가 일정 값 이상이면 Bottom Sheet를 닫음 + if (currentY > 100) { + // 애니메이션으로 Bottom Sheet를 아래로 내리고 닫기 + setIsClosing(true); + setCurrentY(window.innerHeight); // 화면 하단으로 내리는 애니메이션 + setTimeout(() => { + onDimmerClick(); // 애니메이션 후 실제로 닫기 + }, 300); // 애니메이션 시간이 0.3초이므로 300ms 후에 닫음 + } else { + setCurrentY(0); // 원래 위치로 되돌림 + } + }; + + if (!isOpen && !isClosing) { + return null; + } + + return ( + + + + + {header} + {children} + {cta} + + + ); +} + +BottomSheet.Header = function BottomSheetHeader(props: PropsWithChildren) { + const { children } = props; + + return
{children}
; +}; + +BottomSheet.Main = function BottomSheetMain(props: PropsWithChildren) { + const { children } = props; + + return
{children}
; +}; + +BottomSheet.CTA = function BottomSheetCTA(props: PropsWithChildren) { + const { children } = props; + + return
{children}
; +}; diff --git a/frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.style.ts b/frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.style.ts new file mode 100644 index 000000000..1f7d907ec --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.style.ts @@ -0,0 +1,39 @@ +import { css, Theme } from '@emotion/react'; + +export const body = ({ + theme, + currentY, + size, + isDragging, +}: { + theme: Theme; + currentY: number; + size?: 'small' | 'medium' | 'large' | 'full'; + isDragging: boolean; +}) => css` + z-index: 2; + + /* 터치 드래그에 따른 Y축 이동 */ + transform: translateY(${currentY}px); + + display: flex; + flex-direction: column; + gap: 24px; + + width: 100%; + max-width: 600px; + height: ${size === 'medium' + ? '50vh' + : size === 'large' + ? '80vh' + : size === 'full' + ? '100vh' + : 'auto'}; + padding-bottom: 32px; + + background-color: ${theme.colorPalette.white[100]}; + border-radius: 28px 28px 0 0; + + /* 터치 드래그에 따른 Y축 이동 */ + transition: ${isDragging ? 'none' : 'transform 0.3s ease'}; +`; diff --git a/frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.tsx b/frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.tsx new file mode 100644 index 000000000..1004f138b --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.tsx @@ -0,0 +1,21 @@ +import { useTheme } from '@emotion/react'; +import { PropsWithChildren } from 'react'; +import * as S from './BottomSheetBody.style'; + +interface BottomSheetBodyProps { + currentY: number; + size?: 'small' | 'medium' | 'large' | 'full'; + isDragging: boolean; +} + +export default function BottomSheetBody( + props: PropsWithChildren, +) { + const { currentY, size, isDragging, children } = props; + + const theme = useTheme(); + + return ( +
{children}
+ ); +} diff --git a/frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.style.ts b/frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.style.ts new file mode 100644 index 000000000..066adbe41 --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.style.ts @@ -0,0 +1,15 @@ +import { css } from '@emotion/react'; + +export const container = css` + position: fixed; + z-index: 1; + inset: 0; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + + width: 100%; + height: 100%; +`; diff --git a/frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.tsx b/frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.tsx new file mode 100644 index 000000000..69e86ca74 --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.tsx @@ -0,0 +1,8 @@ +import { PropsWithChildren } from 'react'; +import * as S from './BottomSheetContainer.style'; + +export default function BottomSheetContainer(props: PropsWithChildren) { + const { children } = props; + + return
{children}
; +} diff --git a/frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.style.ts b/frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.style.ts new file mode 100644 index 000000000..b60cb7fb7 --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.style.ts @@ -0,0 +1,15 @@ +import { css } from '@emotion/react'; + +export const handleWrapper = css` + display: flex; + align-items: center; + justify-content: center; + height: 32px; +`; + +export const handleBar = css` + width: 50px; + height: 6px; + background-color: black; + border-radius: 12px; +`; diff --git a/frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.tsx b/frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.tsx new file mode 100644 index 000000000..54a94b4c6 --- /dev/null +++ b/frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.tsx @@ -0,0 +1,22 @@ +import * as S from './BottomSheetHandle.style'; + +interface BottomSheetHandleProps { + onTouchStart: (event: React.TouchEvent) => void; + onTouchMove: (event: React.TouchEvent) => void; + onTouchEnd: () => void; +} + +export default function BottomSheetHandle(props: BottomSheetHandleProps) { + const { onTouchStart, onTouchMove, onTouchEnd } = props; + + return ( +
+
+
+ ); +} diff --git a/frontend/src/components/BackArrowButton/BackArrowButton.style.ts b/frontend/src/components/Button/BackArrowButton/BackArrowButton.style.ts similarity index 100% rename from frontend/src/components/BackArrowButton/BackArrowButton.style.ts rename to frontend/src/components/Button/BackArrowButton/BackArrowButton.style.ts diff --git a/frontend/src/components/BackArrowButton/BackArrowButton.tsx b/frontend/src/components/Button/BackArrowButton/BackArrowButton.tsx similarity index 84% rename from frontend/src/components/BackArrowButton/BackArrowButton.tsx rename to frontend/src/components/Button/BackArrowButton/BackArrowButton.tsx index 5607116e0..01eae5844 100644 --- a/frontend/src/components/BackArrowButton/BackArrowButton.tsx +++ b/frontend/src/components/Button/BackArrowButton/BackArrowButton.tsx @@ -1,6 +1,7 @@ -import BackArrowIcon from '@_components/Icons/BackArrowIcon'; import * as S from './BackArrowButton.style'; +import BackArrowIcon from '@_components/Icons/BackArrowIcon'; + interface BackArrowButtonProps extends React.ButtonHTMLAttributes {} @@ -8,7 +9,7 @@ export default function BackArrowButton(props: BackArrowButtonProps) { const { ...rest } = props; return ( - ); diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx index 00135be0b..156a4d1a0 100644 --- a/frontend/src/components/Button/Button.tsx +++ b/frontend/src/components/Button/Button.tsx @@ -1,3 +1,4 @@ +import { HTMLAttributes, ReactNode } from 'react'; import { Interpolation, SerializedStyles, @@ -5,10 +6,9 @@ import { useTheme, } from '@emotion/react'; -import { ReactNode } from 'react'; import { shapes } from '@_components/Button/Button.style'; -export interface ButtonProps { +export interface ButtonProps extends HTMLAttributes { shape: 'circle' | 'bar'; onClick?: () => void; disabled?: boolean; @@ -22,13 +22,14 @@ export interface ButtonProps { } export default function Button(props: ButtonProps) { - const { onClick, disabled, children, font } = props; + const { onClick, disabled, children, font, ...restProps } = props; const theme = useTheme(); return ( diff --git a/frontend/src/components/ChatList/ChatList.tsx b/frontend/src/components/ChatList/ChatList.tsx deleted file mode 100644 index 39356210a..000000000 --- a/frontend/src/components/ChatList/ChatList.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useRef } from 'react'; - -import Chat from '@_components/Chat/Chat'; -import { ChatChildren } from '@_components/ChatBubble/ChatChildren/ChatChildren'; -import { Chat as ChatType } from '@_types/index'; -import { list } from './ChatList.style'; -import { useTheme } from '@emotion/react'; - -interface ChatListProps { - chats: ChatType[]; -} - -export default function ChatList(props: ChatListProps) { - const { chats } = props; - const endRef = useRef(null); - - const theme = useTheme(); - - useEffect(() => { - endRef.current?.scrollIntoView(); - }, [chats]); - - return ( -
- {chats.map((chat) => { - return ( - - - - ); - })} -
-
- ); -} diff --git a/frontend/src/components/ChattingPreview/ChattingPreview.stories.tsx b/frontend/src/components/ChattingPreview/ChattingPreview.stories.tsx deleted file mode 100644 index 65009858a..000000000 --- a/frontend/src/components/ChattingPreview/ChattingPreview.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import ChattingPreview from './ChattingPreview'; - -const meta: Meta = { - component: ChattingPreview, -}; - -export default meta; -type Story = StoryObj; - -export const NoContent: Story = { - args: { - chatPreview: { - moimId: 3, - title: '운동 모임', - currentPeople: 10, - isStarted: true, - lastContent: '', - unreadContentCount: 0, - }, - }, -}; - -export const HasContent: Story = { - args: { - chatPreview: { - moimId: 2, - title: '독서 클럽', - currentPeople: 8, - isStarted: true, - lastContent: '이번 주 독서 토론은 어떤 책으로 할까요?', - unreadContentCount: 1, - }, - }, -}; - -export const Over300UnreadMessage: Story = { - args: { - chatPreview: { - moimId: 2, - title: '독서 클럽', - currentPeople: 8, - isStarted: true, - lastContent: '이번 주 독서 토론은 어떤 책으로 할까요?', - unreadContentCount: 301, - }, - }, -}; - -export const AfterMoim: Story = { - args: { - chatPreview: { - moimId: 2, - title: '독서 클럽', - currentPeople: 8, - isStarted: false, - lastContent: '이번 주 독서 토론은 어떤 책으로 할까요?', - unreadContentCount: 1, - }, - }, -}; diff --git a/frontend/src/components/ChattingPreview/ChattingPreview.tsx b/frontend/src/components/ChattingPreview/ChattingPreview.tsx deleted file mode 100644 index d1b76a41d..000000000 --- a/frontend/src/components/ChattingPreview/ChattingPreview.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { - container, - messageContainer, - peopleContainer, - smallGrey400, - tag, - titleContainer, - unreadContentCountContainer, - unreadContentWrapper, -} from './ChattingPreview.style'; - -import ChatBubbleSvg from '@_components/Icons/ChatBubbleSvg'; -import { ChattingPreview as ChattingPreviewType } from '@_types/index'; -import POLICES from '@_constants/poclies'; -import UserPreviewList from '@_components/UserPreviewList/UserPreviewList'; -import { useMemo } from 'react'; -import { useTheme } from '@emotion/react'; - -interface ChattingPreviewProps { - chatPreview: ChattingPreviewType; - onClick: () => void; -} - -export default function ChattingPreview(props: ChattingPreviewProps) { - const { chatPreview, onClick } = props; - const { title, isStarted, lastContent, unreadContentCount, currentPeople } = - chatPreview; - const theme = useTheme(); - const imageUrls = useMemo( - () => new Array(chatPreview.currentPeople).fill(''), - // TODO:participation.profile 구현되면 아래 코드로 변경 - // () => moim.participants.map((participation) => participation.profile), - [chatPreview.currentPeople], - ); - - return ( -
-
-
-

{title}

-
- {isStarted ? '모임 후' : '모임 전'} -
-
- {lastContent && ( - - {lastContent} - - )} - {unreadContentCount > 0 && ( -
- - - {unreadContentCount > POLICES.maxUnreadMessageCount - ? POLICES.maxUnreadMessageCount + '+' - : unreadContentCount} - -
- )} -
- -
- {`${currentPeople}명`} - -
-
- ); -} diff --git a/frontend/src/components/CloseButton/CloseButton.tsx b/frontend/src/components/CloseButton/CloseButton.tsx new file mode 100644 index 000000000..81c7583f7 --- /dev/null +++ b/frontend/src/components/CloseButton/CloseButton.tsx @@ -0,0 +1,22 @@ +import CloseIcon from '@_components/Icons/CloseIcon'; +import { css } from '@emotion/react'; +import { ButtonHTMLAttributes } from 'react'; + +interface CloseButtonProps extends ButtonHTMLAttributes {} + +export default function CloseButton(props: CloseButtonProps) { + const { ...rest } = props; + + return ( + + ); +} diff --git a/frontend/src/components/CommentCard/CommentCard.tsx b/frontend/src/components/CommentCard/CommentCard.tsx deleted file mode 100644 index a1930e8f9..000000000 --- a/frontend/src/components/CommentCard/CommentCard.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as S from '@_components/CommentCard/CommentCard.style'; -import ProfileFrame from '@_components/Profile/ProfileFrame'; -import { Comment } from '@_types/index'; -import { useTheme } from '@emotion/react'; - -import { HTMLProps } from 'react'; - -export interface CommentCardProps extends HTMLProps { - comment: Comment; - onWriteClick?: () => void; - isChecked?: boolean; - isChild?: boolean; -} - -export default function CommentCard(props: CommentCardProps) { - const theme = useTheme(); - const { - comment: { profile, nickname, dateTime, content, children }, - onWriteClick, - isChecked = false, - isChild = false, - } = props; - - return ( -
-
- -
-
-
-
{nickname}
-
{dateTime}
-
- {!isChild && ( -
- -
- )} -
-
{content}
-
-
- {children && ( -
- {children.map((childComment) => ( - - ))} -
- )} -
- ); -} diff --git a/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.style.ts b/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.style.ts index 432b21452..80988501a 100644 --- a/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.style.ts +++ b/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.style.ts @@ -2,10 +2,11 @@ import { SerializedStyles, css } from '@emotion/react'; export const name = ({ font }: { font: string | SerializedStyles }) => css` ${font} + overflow-x: hidden; max-width: 40vw; - overflow-x: hidden; - text-overflow: ellipsis; + color: black; + text-overflow: ellipsis; white-space: nowrap; `; diff --git a/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.tsx b/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.tsx index eb1115acb..30e17ab79 100644 --- a/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.tsx +++ b/frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.tsx @@ -3,6 +3,7 @@ import * as S from './DarakbangNameWrapper.style'; import { SerializedStyles, useTheme } from '@emotion/react'; import { PropsWithChildren } from 'react'; +import { common } from '@_common/common.style'; interface DarakbangNameWrapperProps extends PropsWithChildren { font?: string | SerializedStyles; @@ -12,5 +13,5 @@ export default function DarakbangNameWrapper(props: DarakbangNameWrapperProps) { const theme = useTheme(); const { font = theme.typography.h5, children } = props; - return
{children}
; + return
{children}
; } diff --git a/frontend/src/components/Dimmer/Dimmer.style.ts b/frontend/src/components/Dimmer/Dimmer.style.ts new file mode 100644 index 000000000..9fc6d127a --- /dev/null +++ b/frontend/src/components/Dimmer/Dimmer.style.ts @@ -0,0 +1,16 @@ +import { css } from '@emotion/react'; + +export const dimmer = css` + position: fixed; + inset: 0; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + + width: 100%; + height: 100%; + + background-color: rgb(0 0 0 / 20%); +`; diff --git a/frontend/src/components/Dimmer/Dimmer.tsx b/frontend/src/components/Dimmer/Dimmer.tsx new file mode 100644 index 000000000..f74e677ec --- /dev/null +++ b/frontend/src/components/Dimmer/Dimmer.tsx @@ -0,0 +1,5 @@ +import * as S from './Dimmer.style'; + +export default function Dimmer({ onClick }: { onClick: () => void }) { + return
; +} diff --git a/frontend/src/components/Fallback/HappyFallback/HappyFallback.stories.tsx b/frontend/src/components/Fallback/HappyFallback/HappyFallback.stories.tsx new file mode 100644 index 000000000..39c831e08 --- /dev/null +++ b/frontend/src/components/Fallback/HappyFallback/HappyFallback.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import HappyFallback from './HappyFallback'; +import { css } from '@emotion/react'; + +const meta: Meta = { + component: HappyFallback, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { text: '없어요' }, + + decorators: (Story) => { + return ( +
+ +
+ ); + }, +}; diff --git a/frontend/src/components/Fallback/HappyFallback/HappyFallback.style.ts b/frontend/src/components/Fallback/HappyFallback/HappyFallback.style.ts new file mode 100644 index 000000000..9ff1a1235 --- /dev/null +++ b/frontend/src/components/Fallback/HappyFallback/HappyFallback.style.ts @@ -0,0 +1,27 @@ +import { SerializedStyles, css } from '@emotion/react'; + +export const container = ({ + backgroundColor, +}: { + backgroundColor?: string | SerializedStyles; +}) => css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + + ${backgroundColor ? `background-color: ${backgroundColor};` : ''} + + min-width: 30rem; + min-height: 30rem; + + & > * { + text-align: center; + white-space: pre-line; + } +`; + +export const image = css` + max-width: 40rem; + max-height: 40rem; +`; diff --git a/frontend/src/components/Fallback/HappyFallback/HappyFallback.tsx b/frontend/src/components/Fallback/HappyFallback/HappyFallback.tsx new file mode 100644 index 000000000..c455398ca --- /dev/null +++ b/frontend/src/components/Fallback/HappyFallback/HappyFallback.tsx @@ -0,0 +1,22 @@ +import * as S from './HappyFallback.style'; + +import { SerializedStyles, useTheme } from '@emotion/react'; + +import happyLogo from '@_common/assets/happy_logo.svg?url'; + +interface HappyFallbackProps { + text: string; + font?: SerializedStyles; + backgroundColor?: string | SerializedStyles; +} + +export default function HappyFallback(props: HappyFallbackProps) { + const { text, font, backgroundColor } = props; + const theme = useTheme(); + return ( +
+ 좋아용 + {text} +
+ ); +} diff --git a/frontend/src/components/MissingFallback/MissingFallback.stories.tsx b/frontend/src/components/Fallback/MissingFallback/MissingFallback.stories.tsx similarity index 100% rename from frontend/src/components/MissingFallback/MissingFallback.stories.tsx rename to frontend/src/components/Fallback/MissingFallback/MissingFallback.stories.tsx diff --git a/frontend/src/components/Fallback/MissingFallback/MissingFallback.style.ts b/frontend/src/components/Fallback/MissingFallback/MissingFallback.style.ts new file mode 100644 index 000000000..9ff1a1235 --- /dev/null +++ b/frontend/src/components/Fallback/MissingFallback/MissingFallback.style.ts @@ -0,0 +1,27 @@ +import { SerializedStyles, css } from '@emotion/react'; + +export const container = ({ + backgroundColor, +}: { + backgroundColor?: string | SerializedStyles; +}) => css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + + ${backgroundColor ? `background-color: ${backgroundColor};` : ''} + + min-width: 30rem; + min-height: 30rem; + + & > * { + text-align: center; + white-space: pre-line; + } +`; + +export const image = css` + max-width: 40rem; + max-height: 40rem; +`; diff --git a/frontend/src/components/MissingFallback/MissingFallback.tsx b/frontend/src/components/Fallback/MissingFallback/MissingFallback.tsx similarity index 57% rename from frontend/src/components/MissingFallback/MissingFallback.tsx rename to frontend/src/components/Fallback/MissingFallback/MissingFallback.tsx index 9f2bf83a8..1d7914a66 100644 --- a/frontend/src/components/MissingFallback/MissingFallback.tsx +++ b/frontend/src/components/Fallback/MissingFallback/MissingFallback.tsx @@ -2,19 +2,20 @@ import * as S from './MissingFallback.style'; import { SerializedStyles, useTheme } from '@emotion/react'; -import regretCat from '@_common/assets/regret_cat.png'; +import missingLogo from '@_common/assets/missing_logo.svg?url'; interface MissingFallbackProps { text: string; font?: SerializedStyles; + backgroundColor?: string | SerializedStyles; } export default function MissingFallback(props: MissingFallbackProps) { - const { text, font } = props; + const { text, font, backgroundColor } = props; const theme = useTheme(); return ( -
- 미안해용 +
+ 미안해용 {text}
); diff --git a/frontend/src/components/Funnel/FunnelErrorMessage/FunnelErrorMessage.style.ts b/frontend/src/components/Funnel/FunnelErrorMessage/FunnelErrorMessage.style.ts new file mode 100644 index 000000000..91314d853 --- /dev/null +++ b/frontend/src/components/Funnel/FunnelErrorMessage/FunnelErrorMessage.style.ts @@ -0,0 +1,7 @@ +import { css, Theme } from '@emotion/react'; + +export const errorMessage = ({ theme }: { theme: Theme }) => css` + font-size: 1.4rem; + color: ${theme.colorPalette.red[500]}; + text-align: center; +`; diff --git a/frontend/src/components/Funnel/FunnelErrorMessage/FunnelErrorMessage.tsx b/frontend/src/components/Funnel/FunnelErrorMessage/FunnelErrorMessage.tsx new file mode 100644 index 000000000..fc3d83598 --- /dev/null +++ b/frontend/src/components/Funnel/FunnelErrorMessage/FunnelErrorMessage.tsx @@ -0,0 +1,18 @@ +import { useTheme } from '@emotion/react'; +import * as S from './FunnelErrorMessage.style'; + +interface FunnelErrorMessageProps { + isError: boolean; + errorMessage: string; +} + +export default function FunnelErrorMessage(props: FunnelErrorMessageProps) { + const { isError, errorMessage } = props; + + const theme = useTheme(); + + if (!isError) { + return null; + } + return
{errorMessage}
; +} diff --git a/frontend/src/components/Funnel/FunnelInput/FunnelInput.style.ts b/frontend/src/components/Funnel/FunnelInput/FunnelInput.style.ts index d73d7f86c..0ce4fb966 100644 --- a/frontend/src/components/Funnel/FunnelInput/FunnelInput.style.ts +++ b/frontend/src/components/Funnel/FunnelInput/FunnelInput.style.ts @@ -1,7 +1,7 @@ import { Theme, css } from '@emotion/react'; export const input = (props: { theme: Theme }) => css` - ${props.theme.typography.b3} + ${props.theme.typography.b2} box-sizing: border-box; width: 100%; height: 4.4rem; diff --git a/frontend/src/components/Funnel/FunnelInputErrorMessage/FunnelInputErrorMessage.tsx b/frontend/src/components/Funnel/FunnelInputErrorMessage/FunnelInputErrorMessage.tsx new file mode 100644 index 000000000..c9d278869 --- /dev/null +++ b/frontend/src/components/Funnel/FunnelInputErrorMessage/FunnelInputErrorMessage.tsx @@ -0,0 +1,19 @@ +import { useTheme } from '@emotion/react'; +import { HTMLAttributes, PropsWithChildren } from 'react'; + +interface FunnelInputErrorMessageProps + extends HTMLAttributes {} + +export default function FunnelInputErrorMessage( + props: PropsWithChildren, +) { + const { children, ...rest } = props; + + const theme = useTheme(); + + return ( + + {children} + + ); +} diff --git a/frontend/src/components/Funnel/FunnelQuestion/FunnelQuestion.tsx b/frontend/src/components/Funnel/FunnelQuestion/FunnelQuestion.tsx index 4a1080c42..577b2b4e4 100644 --- a/frontend/src/components/Funnel/FunnelQuestion/FunnelQuestion.tsx +++ b/frontend/src/components/Funnel/FunnelQuestion/FunnelQuestion.tsx @@ -1,11 +1,13 @@ -import { PropsWithChildren } from 'react'; +import { LabelHTMLAttributes, PropsWithChildren } from 'react'; import FunnelQuestionHighlight from './FunnelQuestionHighlight/FunnelQuestionHighlight'; import FunnelTextQuestionText from './FunnelQuestionText/FunnelQuestionText'; -function FunnelQuestion(props: PropsWithChildren) { - const { children } = props; +interface FunnelQuestionProps extends LabelHTMLAttributes {} - return
{children}
; +function FunnelQuestion(props: PropsWithChildren) { + const { children, ...rest } = props; + + return ; } FunnelQuestion.Text = FunnelTextQuestionText; diff --git a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.style.ts b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.style.ts index b4dcb39a2..c20c28671 100644 --- a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.style.ts +++ b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.style.ts @@ -1,7 +1,14 @@ import { css } from '@emotion/react'; -export const container = css` +export const container = ({ columns }: { columns: number }) => css` + ${columns === 1 + ? ` display: flex; - flex-direction: column; + flex-direction: column; + ` + : ` + display: grid; + grid-template-columns: repeat(${columns}, 1fr); + `} gap: 24px; `; diff --git a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.tsx b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.tsx index d4adfd05c..a31068432 100644 --- a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.tsx +++ b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.tsx @@ -2,10 +2,15 @@ import { PropsWithChildren } from 'react'; import * as S from './FunnelRadioCardGroup.style'; import FunnelRadioCardGroupOption from './FunnelRadioCardGroupOption/FunnelRadioCardGroupOption'; -function FunnelRadioCardGroup(props: PropsWithChildren) { - const { children } = props; +interface FunnelRadioCardGroupProps { + columns?: number; +} +function FunnelRadioCardGroup( + props: PropsWithChildren, +) { + const { children, columns = 1 } = props; - return
{children}
; + return
{children}
; } FunnelRadioCardGroup.Option = FunnelRadioCardGroupOption; diff --git a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.style.ts b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.style.ts index c5cf1844c..578f3ab11 100644 --- a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.style.ts +++ b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.style.ts @@ -3,14 +3,16 @@ import { css, Theme } from '@emotion/react'; export const container = ({ theme, isSelected, + description, }: { theme: Theme; isSelected: boolean; + description?: string; }) => css` display: flex; justify-content: space-between; - padding: 30px 20px 22px; + padding: ${description ? `30px 20px 22px` : `12px`}; color: ${isSelected ? theme.colorPalette.white[100] @@ -19,7 +21,7 @@ export const container = ({ background-color: ${isSelected ? theme.semantic.primary : theme.colorPalette.orange[50]}; - border-radius: 24px; + border-radius: ${description ? `24px` : `8px`}; `; export const contentContainer = () => css` diff --git a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.tsx b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.tsx index 9d26e6cec..81bb69791 100644 --- a/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.tsx +++ b/frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.tsx @@ -4,7 +4,7 @@ import SelectionIcon from '@_components/Icons/SelectionIcon'; interface FunnelRadioCardGroupOptionProps { title: string; - description: string; + description?: string; isSelected: boolean; onSelect: () => void; } @@ -17,10 +17,13 @@ export default function FunnelRadioCardGroupOption( const theme = useTheme(); return ( -
+

{title}

-

{description}

+ {description &&

{description}

}
diff --git a/frontend/src/components/Funnel/FunnelTextArea/FunnelTextArea.style.ts b/frontend/src/components/Funnel/FunnelTextArea/FunnelTextArea.style.ts index 8de7baf1c..a8270eca5 100644 --- a/frontend/src/components/Funnel/FunnelTextArea/FunnelTextArea.style.ts +++ b/frontend/src/components/Funnel/FunnelTextArea/FunnelTextArea.style.ts @@ -7,9 +7,9 @@ export const textArea = (props: { theme: Theme }) => css` flex-shrink: 0; box-sizing: border-box; - padding: 0.6rem; width: 100%; height: 24rem; + padding: 0.6rem; font-size: 1.6rem; diff --git a/frontend/src/components/GoogleLoginButton/GoogleLoginButton.tsx b/frontend/src/components/GoogleLoginButton/GoogleLoginButton.tsx new file mode 100644 index 000000000..d9c7b7a65 --- /dev/null +++ b/frontend/src/components/GoogleLoginButton/GoogleLoginButton.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react'; + +import ROUTES from '@_constants/routes'; +import { useNavigate } from 'react-router-dom'; + +interface GoogleLoginButtonProps { + type: 'bar' | 'circle'; +} +function GoogleLoginButton({ type }: GoogleLoginButtonProps) { + const navigate = useNavigate(); + const g_sso = useRef(null); + + useEffect(() => { + if (g_sso.current) { + const renderOption = + type === 'bar' + ? { + theme: 'outline', + size: 'large', + width: 269, + } + : { + type: 'icon', + shape: 'circle', + theme: 'outline', + size: 'large', + }; + window.google.accounts.id.initialize({ + client_id: process.env.GOOGLE_O_AUTH_CLIENT_ID, + callback: handleGoogleSignIn, + ux_mode: 'popup', + }); + + window.google.accounts.id.renderButton(g_sso.current, renderOption); + } + }, [g_sso]); + + const handleGoogleSignIn = (response: { + credential: string; + error: string; + }) => { + if (response.error && response.error === 'AbortError') { + console.error('요청이 중단되었습니다. 다시 시도하세요.'); + return; + } + navigate(`${ROUTES.oAuthGoogle}/google?code=${response.credential}`); + }; + + return
; +} + +export default GoogleLoginButton; diff --git a/frontend/src/components/HighlightSpan/HighlightSpan.tsx b/frontend/src/components/HighlightSpan/HighlightSpan.tsx index 25aae6d3e..48b7f5a16 100644 --- a/frontend/src/components/HighlightSpan/HighlightSpan.tsx +++ b/frontend/src/components/HighlightSpan/HighlightSpan.tsx @@ -8,6 +8,7 @@ interface HighlightSpanProps extends PropsWithChildren { normalColor?: SerializedStyles | string; font?: SerializedStyles | string; isCenterAlign?: boolean; + ariaLabel?: string; } interface HighlightSpanContext { @@ -24,6 +25,7 @@ function HighlightSpan(props: HighlightSpanProps) { normalColor = theme.colorPalette.black[100], font = theme.typography.h5, isCenterAlign = false, + ariaLabel, children, } = props; @@ -31,7 +33,10 @@ function HighlightSpan(props: HighlightSpanProps) { - + {children} diff --git a/frontend/src/components/HomeHeaderContent/HomHeaderContent.tsx b/frontend/src/components/HomeHeaderContent/HomHeaderContent.tsx deleted file mode 100644 index 80902b3c3..000000000 --- a/frontend/src/components/HomeHeaderContent/HomHeaderContent.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useTheme } from '@emotion/react'; -import { PropsWithChildren } from 'react'; -import * as S from './HomeHeaderContent.style'; - -export default function HomeHeaderContent(props: PropsWithChildren) { - const { children } = props; - - const theme = useTheme(); - - return

{children}

; -} diff --git a/frontend/src/components/HomeHeaderContent/HomeHeaderContent.style.ts b/frontend/src/components/HomeHeaderContent/HomeHeaderContent.style.ts deleted file mode 100644 index 6d032eb23..000000000 --- a/frontend/src/components/HomeHeaderContent/HomeHeaderContent.style.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Theme, css } from '@emotion/react'; - -import { common } from '@_common/common.style'; - -export const logoStyle = (props: { theme: Theme }) => css` - ${props.theme.typography.h5} - ${common.nonScroll} -`; diff --git a/frontend/src/components/Icons/AppleOAuthIcon.tsx b/frontend/src/components/Icons/AppleOAuthIcon.tsx new file mode 100644 index 000000000..55b391d01 --- /dev/null +++ b/frontend/src/components/Icons/AppleOAuthIcon.tsx @@ -0,0 +1,41 @@ +import AppleCircle from '@_common/assets/apple_cirecle.svg'; +interface AppleOAuthIconProps { + type: 'bar' | 'circle'; +} +export default function AppleOAuthIcon({ type = 'bar' }: AppleOAuthIconProps) { + if (type === 'bar') { + return ( + + + + + + + + + + ); + } + if (type === 'circle') { + return ; + } +} diff --git a/frontend/src/components/Icons/CloseIcon.tsx b/frontend/src/components/Icons/CloseIcon.tsx new file mode 100644 index 000000000..f5ce1cec2 --- /dev/null +++ b/frontend/src/components/Icons/CloseIcon.tsx @@ -0,0 +1,16 @@ +export default function CloseIcon() { + return ( + + + + ); +} diff --git a/frontend/src/components/Icons/Hamburger.tsx b/frontend/src/components/Icons/Hamburger.tsx new file mode 100644 index 000000000..d11818069 --- /dev/null +++ b/frontend/src/components/Icons/Hamburger.tsx @@ -0,0 +1,31 @@ +import { SVGAttributes } from 'react'; + +interface HamburgerProps extends SVGAttributes { + color?: string; + strokeWidth?: number; + width?: number; + height?: number; +} + +export default function Hamburger(props: HamburgerProps) { + const { color, strokeWidth, width, height } = props; + + return ( + + + + ); +} diff --git a/frontend/src/components/Icons/HomeIcon.tsx b/frontend/src/components/Icons/HomeIcon.tsx index 9db8c1016..a87badcb1 100644 --- a/frontend/src/components/Icons/HomeIcon.tsx +++ b/frontend/src/components/Icons/HomeIcon.tsx @@ -11,14 +11,14 @@ export default function HomeIcon(props: HomeIconProps) { return ( - - - - - ); +import CircleKakaoLogin from '@_common/assets/kakaoCircleOuathLogin.svg'; +interface KakaoOAuthLoginIconProps { + type: 'bar' | 'circle'; +} + +export default function KakaoOAuthLoginIcon({ + type, +}: KakaoOAuthLoginIconProps) { + if (type === 'bar') { + return ( + + + + + + ); + } + if (type === 'circle') { + return ; + } } diff --git a/frontend/src/components/Icons/MainLogoIcon.tsx b/frontend/src/components/Icons/MainLogoIcon.tsx index 5d4c25daa..63c62bc7c 100644 --- a/frontend/src/components/Icons/MainLogoIcon.tsx +++ b/frontend/src/components/Icons/MainLogoIcon.tsx @@ -1,82 +1,4 @@ +import MainLogo from '@_common/assets/main_logo.svg'; export default function MainLogoIcon() { - return ( - - - - - - - - - - - - - - ); + return ; } diff --git a/frontend/src/components/Icons/PeopleIcon.tsx b/frontend/src/components/Icons/PeopleIcon.tsx new file mode 100644 index 000000000..a85924485 --- /dev/null +++ b/frontend/src/components/Icons/PeopleIcon.tsx @@ -0,0 +1,16 @@ +export default function PeopleIcon() { + return ( + + + + ); +} diff --git a/frontend/src/components/Icons/RouletteIcon.tsx b/frontend/src/components/Icons/RouletteIcon.tsx new file mode 100644 index 000000000..78fb77851 --- /dev/null +++ b/frontend/src/components/Icons/RouletteIcon.tsx @@ -0,0 +1,51 @@ +export default function RouletteIcon() { + return ( + + + + + + + + + + + + + ); +} diff --git a/frontend/src/components/Icons/RouletteItemIcon.tsx b/frontend/src/components/Icons/RouletteItemIcon.tsx new file mode 100644 index 000000000..5fbca0787 --- /dev/null +++ b/frontend/src/components/Icons/RouletteItemIcon.tsx @@ -0,0 +1,40 @@ +import { useTheme } from '@emotion/react'; + +interface ScissorsIconProps { + isActive: boolean; +} + +export default function RouletteItemIcon(props: ScissorsIconProps) { + const { isActive } = props; + + const theme = useTheme(); + + return ( + + + + + + ); +} diff --git a/frontend/src/components/Icons/SolidArrow.tsx b/frontend/src/components/Icons/SolidArrow.tsx index 5c658b449..490e55b97 100644 --- a/frontend/src/components/Icons/SolidArrow.tsx +++ b/frontend/src/components/Icons/SolidArrow.tsx @@ -22,16 +22,17 @@ export default function SolidArrow(props: SolidArrowProps) { viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg" - transform={`rotate(${directionMapper[direction]})`} {...otherProps} > - + + + ); } diff --git a/frontend/src/components/Icons/StarIcons/StarFourIcon.tsx b/frontend/src/components/Icons/StarIcons/StarFourIcon.tsx new file mode 100644 index 000000000..eb0257291 --- /dev/null +++ b/frontend/src/components/Icons/StarIcons/StarFourIcon.tsx @@ -0,0 +1,20 @@ +export default function StarFourIcon() { + return ( + + + + ); +} diff --git a/frontend/src/components/Icons/StarIcons/StarOneIcon.tsx b/frontend/src/components/Icons/StarIcons/StarOneIcon.tsx new file mode 100644 index 000000000..cebb49842 --- /dev/null +++ b/frontend/src/components/Icons/StarIcons/StarOneIcon.tsx @@ -0,0 +1,16 @@ +export default function StarOneIcon() { + return ( + + + + ); +} diff --git a/frontend/src/components/Icons/StarIcons/StarThreeIcon.tsx b/frontend/src/components/Icons/StarIcons/StarThreeIcon.tsx new file mode 100644 index 000000000..08a379fd5 --- /dev/null +++ b/frontend/src/components/Icons/StarIcons/StarThreeIcon.tsx @@ -0,0 +1,20 @@ +export default function StarThreeIcon() { + return ( + + + + ); +} diff --git a/frontend/src/components/Icons/StarIcons/StarTwoIcon.tsx b/frontend/src/components/Icons/StarIcons/StarTwoIcon.tsx new file mode 100644 index 000000000..fa30a0342 --- /dev/null +++ b/frontend/src/components/Icons/StarIcons/StarTwoIcon.tsx @@ -0,0 +1,16 @@ +export default function StarTwoIcon() { + return ( + + + + ); +} diff --git a/frontend/src/components/Input/MessagInput/MessageInput.tsx b/frontend/src/components/Input/MessagInput/MessageInput.tsx index 2aa20539a..67e926e12 100644 --- a/frontend/src/components/Input/MessagInput/MessageInput.tsx +++ b/frontend/src/components/Input/MessagInput/MessageInput.tsx @@ -1,8 +1,8 @@ import * as S from '@_components/Input/MessagInput/MessageInput.style'; -import { useTheme } from '@emotion/react'; -import { useState } from 'react'; import SubmitButton from '@_common/assets/submit_message_button.svg'; +import { useState } from 'react'; +import { useTheme } from '@emotion/react'; export interface MessageInputProps { placeHolder: string; @@ -36,6 +36,7 @@ export default function MessageInput(props: MessageInputProps) { css={S.button({ theme })} type="submit" disabled={!message.trim() || disabled} + aria-label={`댓글쓰기 버튼 ${!message.trim() || disabled ? '댓글을 작성하여 버튼을 활성화하세요' : ''}`} > diff --git a/frontend/src/components/Input/MoimInpit.stories.tsx b/frontend/src/components/Input/MoimInpit.stories.tsx new file mode 100644 index 000000000..af9ff218d --- /dev/null +++ b/frontend/src/components/Input/MoimInpit.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import MoimInput from './MoimInput'; + +const meta: Meta = { + component: MoimInput, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'sdfsdf', + placeholder: 'place-holder', + validateFun: () => false, + }, +}; diff --git a/frontend/src/components/Input/MoimInput.style.ts b/frontend/src/components/Input/MoimInput.style.ts index ba85b2fac..7558f80af 100644 --- a/frontend/src/components/Input/MoimInput.style.ts +++ b/frontend/src/components/Input/MoimInput.style.ts @@ -5,6 +5,9 @@ export const required = (props: { theme: Theme }) => css` `; export const labelWrapper = () => css` + display: flex; + flex-direction: column; + gap: 0.5rem; width: 100%; `; @@ -29,3 +32,8 @@ export const input = (props: { theme: Theme }) => css` border-radius: 0.8rem; `; + +export const errorMessage = (props: { theme: Theme }) => css` + ${props.theme.typography.c2} + color: ${props.theme.colorPalette.red[500]}; +`; diff --git a/frontend/src/components/Input/MoimInput.tsx b/frontend/src/components/Input/MoimInput.tsx index 1a07b8c27..dcf26050d 100644 --- a/frontend/src/components/Input/MoimInput.tsx +++ b/frontend/src/components/Input/MoimInput.tsx @@ -1,13 +1,18 @@ import * as S from '@_components/Input/MoimInput.style'; -import { useTheme } from '@emotion/react'; -import { HTMLProps } from 'react'; +import { ChangeEvent, HTMLProps, useState } from 'react'; + +import { useTheme } from '@emotion/react'; -export interface LabeledInputProps extends HTMLProps { - title: string; +export interface LabeledInputProps + extends HTMLProps { + title?: string; + validateFun?: (value: T) => boolean; } -export default function LabeledInput(props: LabeledInputProps) { +export default function LabeledInput( + props: LabeledInputProps, +) { const theme = useTheme(); const { name, @@ -16,15 +21,40 @@ export default function LabeledInput(props: LabeledInputProps) { placeholder, required, onChange, + validateFun, ...args } = props; + const [isError, setIsError] = useState(false); + + const handleInputChange = (e: ChangeEvent) => { + if (onChange) { + onChange(e); + } + + const value = e.currentTarget.value; + const validatedValue = type === 'number' ? Number(value) : value; + + // validateFun이 존재할 경우 + if (validateFun) { + if (typeof validatedValue === 'string') { + const isValid = validateFun(validatedValue as T); + setIsError(!isValid); + } else if (typeof validatedValue === 'number') { + const isValid = validateFun(validatedValue as T); // number에 대한 validate 처리 + setIsError(!isValid); + } + } + }; + return ( ); } diff --git a/frontend/src/components/KebabMenu/KebabMenu.style.ts b/frontend/src/components/KebabMenu/KebabMenu.style.ts index 7cb698f5f..28a8eddf6 100644 --- a/frontend/src/components/KebabMenu/KebabMenu.style.ts +++ b/frontend/src/components/KebabMenu/KebabMenu.style.ts @@ -12,9 +12,18 @@ export const kebabContainer = (props: { theme: Theme }) => css` border-bottom: 1px; } `; + +export const kebabItem = (props: { theme: Theme }) => css` + white-space: nowrap; + background: ${props.theme.colorPalette.white[100]}; + ${props.theme.typography.ButtonFont} + border: none; + border-bottom: 1px; +`; + export const optionBox = (props: { theme: Theme }) => css` position: absolute; - top: 100%; + top: 5rem; right: 0; display: flex; diff --git a/frontend/src/components/KebabMenu/KebabMenu.tsx b/frontend/src/components/KebabMenu/KebabMenu.tsx index 701a69f51..06382f4a2 100644 --- a/frontend/src/components/KebabMenu/KebabMenu.tsx +++ b/frontend/src/components/KebabMenu/KebabMenu.tsx @@ -1,7 +1,8 @@ import * as S from '@_components/KebabMenu/KebabMenu.style'; -import { useRef, useState, FocusEvent } from 'react'; +import { FocusEvent, useRef, useState } from 'react'; import KebabButton from '@_common/assets/kebab_menu.svg'; import { useTheme } from '@emotion/react'; +import { createPortal } from 'react-dom'; // createPortal 추가 type Option = { name: string; disabled: boolean; onClick: () => void }; export interface KebabMenuProps { @@ -33,26 +34,33 @@ export default function KebabMenu(props: KebabMenuProps) { onClick(); }; + const kebabMenu = isKebabOpen ? ( +
+ {options.map((option) => ( + + ))} +
+ ) : null; + return (
- - {isKebabOpen && ( -
- {options.map((option) => { - return ( - - ); - })} -
- )} + {createPortal(kebabMenu, document.body)} {/* createPortal 사용 */}
); } diff --git a/frontend/src/components/LoginForm/LoginForm.style.ts b/frontend/src/components/LoginForm/LoginForm.style.ts deleted file mode 100644 index 311a89870..000000000 --- a/frontend/src/components/LoginForm/LoginForm.style.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { css } from '@emotion/react'; - -export const formContainerStyle = css` - display: flex; - flex-direction: column; - gap: 20px; - width: 100%; -`; - -export const inputContainerStyle = css` - display: flex; - flex-direction: column; - gap: 12px; -`; diff --git a/frontend/src/components/LoginForm/LoginForm.tsx b/frontend/src/components/LoginForm/LoginForm.tsx deleted file mode 100644 index e8fc8a82c..000000000 --- a/frontend/src/components/LoginForm/LoginForm.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Button from '@_components/Button/Button'; -import LabeledInput from '@_components/Input/MoimInput'; -import * as S from './LoginForm.style'; -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import ROUTES from '@_constants/routes'; -import { setToken } from '@_utils/tokenManager'; -import { login } from '@_apis/auth'; - -// TODO: 로그인 기능 요구사항 변경 예정 -export default function LoginForm() { - const navigate = useNavigate(); - - const [nickname, setNickname] = useState(''); - const [isValid, setIsValid] = useState(false); - - const handleNicknameChange = (e: React.ChangeEvent) => { - setNickname(e.target.value); - }; - - const handleLoginFormSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - const response = await login({ nickname }); - setToken(response.data.accessToken); - navigate(ROUTES.main); - }; - - useEffect(() => { - setIsValid(nickname.length > 0); - }, [nickname]); - - return ( -
-
- -
- -
- ); -} diff --git a/frontend/src/components/MemberCard/MemberCard.tsx b/frontend/src/components/MemberCard/MemberCard.tsx index f5df8ae57..af43662d8 100644 --- a/frontend/src/components/MemberCard/MemberCard.tsx +++ b/frontend/src/components/MemberCard/MemberCard.tsx @@ -1,9 +1,13 @@ import * as S from './MemberCard.style'; import UserPreview from '@_components/UserPreview/UserPreview'; +import useDarakbangMember from '@_hooks/queries/useDarakbangMember'; +import useProfileBottomSheet from '@_hooks/useProfileBottomSheet'; import { useTheme } from '@emotion/react'; +import { Fragment } from 'react'; interface MemberCard { + memberId: number; name: string; // options?: { // description: string; @@ -14,18 +18,30 @@ interface MemberCard { } export default function MemberCard(props: MemberCard) { - const { imageUrl, name } = props; + const { memberId, imageUrl, name } = props; const theme = useTheme(); + const { member } = useDarakbangMember(memberId); + + const { profileBottomSheet, open } = useProfileBottomSheet({ + name: member?.name || '', + nickname: member?.nickname || '', + description: member?.description || '', + url: member?.profile || '', + }); + return ( -
-
- - {name} -
- {/* {options&&
+ +
open()}> +
+ + {name} +
+ {/* {options&&
-
} */} -
+
} */} +
+ {profileBottomSheet} + ); } diff --git a/frontend/src/components/MenuItemList/MenuItemList.stories.tsx b/frontend/src/components/MenuItemList/MenuItemList.stories.tsx deleted file mode 100644 index 25db4c04b..000000000 --- a/frontend/src/components/MenuItemList/MenuItemList.stories.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import MenuItemList from './MenuItemList'; - -const meta: Meta = { - component: MenuItemList, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - menus: [ - { - description: '멤버목록', - onClick: () => { - alert('멤버목록'); - }, - }, - { description: '수정하기', onClick: () => {} }, - ], - }, -}; diff --git a/frontend/src/components/MenuItemList/MenuItemList.tsx b/frontend/src/components/MenuItemList/MenuItemList.tsx deleted file mode 100644 index 1acf3a219..000000000 --- a/frontend/src/components/MenuItemList/MenuItemList.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import MenuItem from '@_components/MenuItem/MenuItem'; - -interface Menu { - description: string; - onClick: () => void; -} - -interface MenuItemListProps { - menus: Menu[]; -} - -export default function MenuItemList(props: MenuItemListProps) { - const { menus } = props; - return ( -
- {menus.map((menu, index) => ( - - ))} -
- ); -} diff --git a/frontend/src/components/MineInfoCard/MineInfoCard.style.ts b/frontend/src/components/MineInfoCard/MineInfoCard.style.ts deleted file mode 100644 index d9e394e10..000000000 --- a/frontend/src/components/MineInfoCard/MineInfoCard.style.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { css } from '@emotion/react'; - -export const MineInfoContainer = () => css` - display: flex; - gap: 1em; - align-items: center; - justify-content: space-between; - - width: 100%; -`; -export const MinetextWrapper = () => css` - display: flex; - flex-direction: column; - gap: 0.5rem; - align-items: flex-end; -`; diff --git a/frontend/src/components/MineInfoCard/MineInfoCard.tsx b/frontend/src/components/MineInfoCard/MineInfoCard.tsx deleted file mode 100644 index 62cf77ab6..000000000 --- a/frontend/src/components/MineInfoCard/MineInfoCard.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as S from '@_components/MineInfoCard/MineInfoCard.style'; - -import ProfileFrame from '@_components/Profile/ProfileFrame'; -import { common } from '@_common/common.style'; -import useNowDarakbangName from '@_hooks/queries/useNowDarakbangNameById'; -import { useTheme } from '@emotion/react'; - -interface MineInfoCardProps { - nickname: string; - profile: string; -} -export default function MineInfoCard(props: MineInfoCardProps) { - const { nickname, profile } = props; - const theme = useTheme(); - const { darakbangName } = useNowDarakbangName(); - return ( -
- -
- 안녕하세요 - {nickname} - - {darakbangName} - -
-
- ); -} diff --git a/frontend/src/components/MissingFallback/MissingFallback.style.ts b/frontend/src/components/MissingFallback/MissingFallback.style.ts deleted file mode 100644 index fdb467b2c..000000000 --- a/frontend/src/components/MissingFallback/MissingFallback.style.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { css } from '@emotion/react'; - -export const container = css` - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - - min-width: 30rem; - min-height: 30rem; - - & > * { - text-align: center; - white-space: pre-line; - } -`; - -export const image = css` - width: 50%; - max-width: 30rem; - height: 50%; - max-height: 35rem; -`; diff --git a/frontend/src/components/MoimSummary/MoimSummary.tsx b/frontend/src/components/MoimSummary/MoimSummary.tsx deleted file mode 100644 index 9204d8567..000000000 --- a/frontend/src/components/MoimSummary/MoimSummary.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { MoimInfo } from '@_types/index'; -import * as S from './MoimSummary.style'; -import { useTheme } from '@emotion/react'; -import Tag from '@_components/Tag/Tag'; - -interface MoimSummaryProps { - moimInfo: Pick; -} - -export default function MoimSummary(props: MoimSummaryProps) { - const theme = useTheme(); - const { - moimInfo: { title, status }, - } = props; - - return ( -
-
-

{title}

- -
-
- ); -} diff --git a/frontend/src/components/NavigationBar/NavigationBar.tsx b/frontend/src/components/NavigationBar/NavigationBar.tsx index d0ac36f74..dba13f097 100644 --- a/frontend/src/components/NavigationBar/NavigationBar.tsx +++ b/frontend/src/components/NavigationBar/NavigationBar.tsx @@ -3,20 +3,21 @@ import * as S from './NavigationBar.style'; import { useLocation, useNavigate } from 'react-router-dom'; import GET_ROUTES from '@_common/getRoutes'; -import NavigationBarItem from '@_components/NavigationBarItem/NavigationBarItem'; +import NavigationBarItem from '@_components/NavigationBar/NavigationBarItem/NavigationBarItem'; import { useState } from 'react'; import { useTheme } from '@emotion/react'; -export type Tab = '홈' | '채팅' | '해주세요' | '마이페이지'; +export type Tab = '홈' | '채팅' | '룰렛' | '마이페이지'; export default function NavigationBar() { const theme = useTheme(); const navigate = useNavigate(); const location = useLocation(); + const tabRoutes: Record = { 홈: GET_ROUTES.nowDarakbang.main(), 채팅: GET_ROUTES.nowDarakbang.chat(), - 해주세요: GET_ROUTES.nowDarakbang.please(), + 룰렛: GET_ROUTES.nowDarakbang.bet(), 마이페이지: GET_ROUTES.nowDarakbang.myPage(), }; @@ -32,8 +33,8 @@ export default function NavigationBar() { }; return ( -