diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index 6c833b356..7c3bc4bae 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -1,7 +1,7 @@ import type { StorybookConfig } from "@storybook/react-webpack5"; const config: StorybookConfig = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: [ "@storybook/addon-webpack5-compiler-swc", "@storybook/addon-onboarding", @@ -14,5 +14,8 @@ const config: StorybookConfig = { name: "@storybook/react-webpack5", options: {}, }, + docs: { + autodocs: true, + }, }; export default config; diff --git a/frontend/.storybook/preview.ts b/frontend/.storybook/preview.ts index 37914b18f..48c6553bd 100644 --- a/frontend/.storybook/preview.ts +++ b/frontend/.storybook/preview.ts @@ -1,4 +1,5 @@ import type { Preview } from "@storybook/react"; +import React from "react"; const preview: Preview = { parameters: { diff --git a/frontend/jest.config.mjs b/frontend/jest.config.js similarity index 100% rename from frontend/jest.config.mjs rename to frontend/jest.config.js diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js new file mode 100644 index 000000000..8a41e78f4 --- /dev/null +++ b/frontend/jest.setup.js @@ -0,0 +1,10 @@ +// const { server } = require("@/mocks/server"); + +// // 모든 테스트 전에 MSW 서버를 시작합니다. +// beforeAll(() => server.listen()); + +// // 각 테스트 후 MSW 핸들러를 리셋합니다. +// afterEach(() => server.resetHandlers()); + +// // 모든 테스트 후 MSW 서버를 닫습니다. +// afterAll(() => server.close()); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6dc37f048..6b149b9e8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@react-icons/all-files": "^4.1.0", "@tanstack/react-query": "^5.51.1", + "axios": "^1.7.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.1", @@ -6867,8 +6868,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -6885,6 +6885,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -7798,7 +7808,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -8559,7 +8568,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -10177,7 +10185,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -10362,7 +10369,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -14616,7 +14622,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -14625,7 +14630,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -16177,6 +16181,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index c11451fa4..32dd1fe0a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "name": "corea", "version": "1.0.0", "description": "Code Review Area", + "type": "module", "main": "index.js", "scripts": { "dev": "webpack-dev-server --mode=development --open --hot --progress", @@ -25,6 +26,7 @@ "dependencies": { "@react-icons/all-files": "^4.1.0", "@tanstack/react-query": "^5.51.1", + "axios": "^1.7.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.1", @@ -73,5 +75,10 @@ "webpack": "^5.92.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js new file mode 100644 index 000000000..24fe3a25f --- /dev/null +++ b/frontend/public/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.3.1' +const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/frontend/src/@types/icon.ts b/frontend/src/@types/icon.ts new file mode 100644 index 000000000..7d63006b8 --- /dev/null +++ b/frontend/src/@types/icon.ts @@ -0,0 +1,3 @@ +type IconKind = "person" | "link" | "calendar"; + +export default IconKind; diff --git a/frontend/src/@types/reviewer.ts b/frontend/src/@types/reviewer.ts new file mode 100644 index 000000000..7842c2901 --- /dev/null +++ b/frontend/src/@types/reviewer.ts @@ -0,0 +1,29 @@ +export interface ReviewerInfo { + userId: number; + username: string; + link: string; + isReviewed: boolean; +} + +// { +// "reviewInfo": [ +// { +// "userId" : 1, +// "username" : "youngsu5582", +// "link" : "https://github.com/youngsu5582/java-racing/pull/7", +// "isReviewed" : true +// }, +// { +// "userId" : 2, +// "username" : "youngsu5583", +// "link" : "https://github.com/youngsu5583/java-racing/pull/12", +// "isReviewed" : false +// }, +// { +// "userId" : 3, +// "username" : "youngsu5584", +// "link" : "https://github.com/youngsu5584/java-racing/pull/23", +// "isReviewed" : false +// } +// ] +// } diff --git a/frontend/src/@types/roomInfo.ts b/frontend/src/@types/roomInfo.ts new file mode 100644 index 000000000..d991418be --- /dev/null +++ b/frontend/src/@types/roomInfo.ts @@ -0,0 +1,16 @@ +export interface RoomInfo { + id: number; + title: string; + content: string; + matchingSize: number; + repositoryLink: string; + thumbnailLink: string; + keywords: string[]; + currentParticipantSize: number; + maximumParticipantSize: number; + manager: string; + recruitmentDeadline: string; + reviewDeadline: string; + isParticipated: boolean; + isClosed: boolean; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index abb67dbcc..000000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import styled from "styled-components"; - -const App = () => { - console.log("qwe"); - - return hello world; -}; - -export default App; - -const Container = styled.div` - background: orange; -`; diff --git a/frontend/src/apis/apiClient.ts b/frontend/src/apis/apiClient.ts new file mode 100644 index 000000000..b3502149f --- /dev/null +++ b/frontend/src/apis/apiClient.ts @@ -0,0 +1,15 @@ +import axios from "axios"; +import { serverUrl } from "@/config/serverUrl"; + +const apiClient = axios.create({ + baseURL: serverUrl, +}); + +// api 요청하기 전 수행 +apiClient.interceptors.request.use((config) => { + config.headers["Authorization"] = "choco@gmail.com"; + + return config; +}); + +export default apiClient; diff --git a/frontend/src/apis/endpoints.ts b/frontend/src/apis/endpoints.ts new file mode 100644 index 000000000..09c0f887b --- /dev/null +++ b/frontend/src/apis/endpoints.ts @@ -0,0 +1,6 @@ +export const API_ENDPOINTS = { + ROOMS: `/rooms`, + MY: `/my`, + REVIEWER: `/my/reviewer`, + REVIEWEE: `/my/reviewee`, +}; diff --git a/frontend/src/apis/my.api.ts b/frontend/src/apis/my.api.ts new file mode 100644 index 000000000..5ddf0e808 --- /dev/null +++ b/frontend/src/apis/my.api.ts @@ -0,0 +1,21 @@ +import { ReviewerInfo } from "./../@types/reviewer"; +import apiClient from "./apiClient"; +import { API_ENDPOINTS } from "./endpoints"; + +export const getMyReviewers = async (): Promise => { + const res = await apiClient({ + method: "get", + url: API_ENDPOINTS.REVIEWER, + }); + + return res.data; +}; + +export const getMyReviewees = async (): Promise => { + const res = await apiClient({ + method: "get", + url: API_ENDPOINTS.REVIEWEE, + }); + + return res.data; +}; diff --git a/frontend/src/apis/queryKeys.ts b/frontend/src/apis/queryKeys.ts new file mode 100644 index 000000000..6a7268a33 --- /dev/null +++ b/frontend/src/apis/queryKeys.ts @@ -0,0 +1,4 @@ +export const QUERY_KEYS = { + PRODUCTS: "products", + CART: "cart", +}; diff --git a/frontend/src/apis/rooms.api.ts b/frontend/src/apis/rooms.api.ts new file mode 100644 index 000000000..d70970373 --- /dev/null +++ b/frontend/src/apis/rooms.api.ts @@ -0,0 +1,21 @@ +import apiClient from "./apiClient"; +import { API_ENDPOINTS } from "./endpoints"; +import { RoomInfo } from "@/@types/roomInfo"; + +export const getRoomList = async (): Promise<{ rooms: RoomInfo[] }> => { + const res = await apiClient<{ rooms: RoomInfo[] }>({ + method: "get", + url: `${API_ENDPOINTS.ROOMS}`, + }); + + return res.data; +}; + +export const getRoomDetailInfo = async (id: number): Promise => { + const res = await apiClient({ + method: "get", + url: `${API_ENDPOINTS.ROOMS}/${id}`, + }); + + return res.data; +}; diff --git a/frontend/src/assets/fonts/bmHanna.otf b/frontend/src/assets/fonts/bmHanna.otf new file mode 100644 index 000000000..29020e81f Binary files /dev/null and b/frontend/src/assets/fonts/bmHanna.otf differ diff --git a/frontend/src/components/common/button/Button.style.ts b/frontend/src/components/common/button/Button.style.ts new file mode 100644 index 000000000..0725d4be1 --- /dev/null +++ b/frontend/src/components/common/button/Button.style.ts @@ -0,0 +1,24 @@ +import styled from "styled-components"; + +export const Button = styled.button<{ color: "primary2" | "secondary" | "grey4" }>` + cursor: pointer; + + display: flex; + align-items: center; + justify-content: center; + + width: 6rem; + height: 2rem; + padding: 0.2rem; + + font: ${({ theme }) => theme.TEXT.semiSmall}; + color: white; + text-align: center; + + background: ${({ color, theme }) => theme.COLOR[color]}; + border-radius: 0.5rem; + + &:hover { + opacity: 0.6; + } +`; diff --git a/frontend/src/components/common/button/Button.tsx b/frontend/src/components/common/button/Button.tsx new file mode 100644 index 000000000..737ed1ad6 --- /dev/null +++ b/frontend/src/components/common/button/Button.tsx @@ -0,0 +1,18 @@ +import * as S from "@/components/common/button/Button.style"; + +interface ButtonProps + extends React.PropsWithChildren> { + onClick: React.MouseEventHandler; + text: string; + color?: "primary2" | "secondary" | "grey4"; +} + +const Button = ({ text, onClick, color = "primary2", ...props }: ButtonProps) => { + return ( + + {text} + + ); +}; + +export default Button; diff --git a/frontend/src/components/common/contentForm/ContentForm.style.ts b/frontend/src/components/common/contentForm/ContentForm.style.ts new file mode 100644 index 000000000..4dc145afc --- /dev/null +++ b/frontend/src/components/common/contentForm/ContentForm.style.ts @@ -0,0 +1,11 @@ +import styled from "styled-components"; + +export const ContentFormContainer = styled.div` + display: flex; + flex-direction: column; +`; + +export const ContentFormTitle = styled.h2` + padding-bottom: 1rem; + font: ${({ theme }) => theme.TEXT.large}; +`; diff --git a/frontend/src/components/common/contentForm/ContentForm.tsx b/frontend/src/components/common/contentForm/ContentForm.tsx new file mode 100644 index 000000000..ebb3ed31c --- /dev/null +++ b/frontend/src/components/common/contentForm/ContentForm.tsx @@ -0,0 +1,17 @@ +import * as S from "@/components/common/contentForm/ContentForm.style"; + +interface ContentFormProps { + title: string; + children: React.ReactNode; +} + +const ContentForm = ({ title, children }: ContentFormProps) => { + return ( + + {title} + {children} + + ); +}; + +export default ContentForm; diff --git a/frontend/src/components/common/header/Header.style.ts b/frontend/src/components/common/header/Header.style.ts new file mode 100644 index 000000000..76666fa14 --- /dev/null +++ b/frontend/src/components/common/header/Header.style.ts @@ -0,0 +1,27 @@ +import styled from "styled-components"; + +export const HeaderContainer = styled.header` + display: flex; + align-items: center; + justify-content: space-between; + + width: 100vw; + height: 6.5rem; + padding: 0 calc((100vw - 1200px) / 2); + + box-shadow: 0 4px 4px rgb(0 0 0 / 10%); + + div { + font: ${({ theme }) => theme.TEXT.large}; + } + + li { + cursor: pointer; + font: ${({ theme }) => theme.TEXT.semiSmall}; + } + + ul { + display: flex; + gap: 1rem; + } +`; diff --git a/frontend/src/components/common/header/Header.tsx b/frontend/src/components/common/header/Header.tsx new file mode 100644 index 000000000..157276c6b --- /dev/null +++ b/frontend/src/components/common/header/Header.tsx @@ -0,0 +1,16 @@ +import * as S from "@/components/common/header/Header.style"; + +const Header = () => { + return ( + + CoReA + + 가이드 + 랭킹 + 마이페이지 + + + ); +}; + +export default Header; diff --git a/frontend/src/components/common/icon/Icon.stories.tsx b/frontend/src/components/common/icon/Icon.stories.tsx new file mode 100644 index 000000000..dd871e95a --- /dev/null +++ b/frontend/src/components/common/icon/Icon.stories.tsx @@ -0,0 +1,41 @@ +import Icon from "./Icon"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "common/Icon", + component: Icon, + parameters: { + docs: { + description: { + component: "아이콘 컴포넌트", + }, + }, + }, + argTypes: { + kind: { + description: "아이콘 종류", + }, + onClick: { + description: "아이콘 클릭 이벤트", + }, + color: { + description: "아이콘 색상", + control: { type: "radio" }, + options: ["black", "red", "green"], + }, + size: { + description: "아이콘 크기", + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + kind: "person", + color: "black", + }, +}; diff --git a/frontend/src/components/common/icon/Icon.tsx b/frontend/src/components/common/icon/Icon.tsx new file mode 100644 index 000000000..6594abf34 --- /dev/null +++ b/frontend/src/components/common/icon/Icon.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { MouseEventHandler } from "react"; +import { IconType } from "react-icons/lib"; +import { MdCalendarMonth, MdInsertLink, MdPerson } from "react-icons/md"; +import IconKind from "@/@types/icon"; + +const ICON: { [key in IconKind]: IconType } = { + person: MdPerson, + link: MdInsertLink, + calendar: MdCalendarMonth, +}; + +interface IconProps { + kind: IconKind; + onClick?: MouseEventHandler; + color?: string; + size?: string | number; +} + +const Icon = ({ kind, ...props }: IconProps) => { + const TargetIcon = ICON[kind]; + return ; +}; + +export default Icon; diff --git a/frontend/src/components/layout/Layout.style.ts b/frontend/src/components/layout/Layout.style.ts new file mode 100644 index 000000000..da01ae315 --- /dev/null +++ b/frontend/src/components/layout/Layout.style.ts @@ -0,0 +1,9 @@ +import styled from "styled-components"; + +export const ContentContainer = styled.section` + width: 100vw; + min-width: 436px; + max-width: 1200px; + margin: 2rem auto; + padding: 1rem; +`; diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx new file mode 100644 index 000000000..b8f479b0d --- /dev/null +++ b/frontend/src/components/layout/Layout.tsx @@ -0,0 +1,16 @@ +import { Outlet } from "react-router-dom"; +import Header from "@/components/common/header/Header"; +import * as S from "@/components/layout/Layout.style"; + +const Layout = () => { + return ( + <> + + + + + > + ); +}; + +export default Layout; diff --git a/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.style.ts b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.style.ts new file mode 100644 index 000000000..85734e3ca --- /dev/null +++ b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.style.ts @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +export const MyRevieweeContainer = styled.div` + border: 1px solid ${({ theme }) => theme.COLOR.grey1}; + border-radius: 1rem; +`; + +export const MyRevieweeTitle = styled.span` + width: 10rem; + padding: 1rem 2rem; +`; + +export const MyRevieweeContent = styled.span` + display: flex; + flex-direction: column; + gap: 0.2rem; + + width: 10rem; + padding: 1rem 2rem; +`; + +export const MyRevieweeWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + &:not(:last-child) { + border-bottom: 1px solid ${({ theme }) => theme.COLOR.grey1}; + } +`; + +export const PRLink = styled.a` + cursor: pointer; + text-decoration: underline !important; + text-underline-offset: 0.3rem; + + &:hover { + color: ${({ theme }) => theme.COLOR.primary2}; + text-decoration: underline !important; + } +`; diff --git a/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.tsx b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.tsx new file mode 100644 index 000000000..d681561c3 --- /dev/null +++ b/frontend/src/components/roomDetailPage/myReviewee/MyReviewee.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import Button from "@/components/common/button/Button"; +import Icon from "@/components/common/icon/Icon"; +import * as S from "@/components/roomDetailPage/myReviewee/MyReviewee.style"; +import { ReviewerInfo } from "@/@types/reviewer"; +import { getMyReviewees } from "@/apis/my.api"; + +const MyReviewee = () => { + const [revieweeData, setRevieweeData] = useState([]); + + const fetchRevieweeData = async () => { + const res = await getMyReviewees(); + setRevieweeData(res); + }; + + useEffect(() => { + fetchRevieweeData(); + }, []); + + if (revieweeData.length === 0) { + return <>아직 리뷰이가 매칭되지 않았습니다! 조금만 기다려주세요🤗>; + } + + return ( + + + 아이디 + PR 링크 + 제출 여부 + + + {revieweeData?.map((reviewee) => ( + + {reviewee.username} + + + + + 바로가기 + + + + + alert("버튼 클릭 완료!")} color="secondary" /> + alert("버튼 클릭 완료!")} /> + + + ))} + + ); +}; + +export default MyReviewee; diff --git a/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.style.ts b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.style.ts new file mode 100644 index 000000000..df4b7fecb --- /dev/null +++ b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.style.ts @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +export const MyReviewerContainer = styled.div` + border: 1px solid ${({ theme }) => theme.COLOR.grey1}; + border-radius: 1rem; +`; + +export const MyReviewerTitle = styled.span` + width: 10rem; + padding: 1rem 2rem; +`; + +export const MyReviewerContent = styled.span` + display: flex; + flex-direction: column; + gap: 0.2rem; + + width: 10rem; + padding: 1rem 2rem; +`; + +export const MyReviewerWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + &:not(:last-child) { + border-bottom: 1px solid ${({ theme }) => theme.COLOR.grey1}; + } +`; + +export const PRLink = styled.a` + cursor: pointer; + text-decoration: underline !important; + text-underline-offset: 0.3rem; + + &:hover { + color: ${({ theme }) => theme.COLOR.primary2}; + text-decoration: underline !important; + } +`; diff --git a/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.tsx b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.tsx new file mode 100644 index 000000000..e20481eb0 --- /dev/null +++ b/frontend/src/components/roomDetailPage/myReviewer/MyReviewer.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import Button from "@/components/common/button/Button"; +import Icon from "@/components/common/icon/Icon"; +import * as S from "@/components/roomDetailPage/myReviewer/MyReviewer.style"; +import { ReviewerInfo } from "@/@types/reviewer"; +import { getMyReviewers } from "@/apis/my.api"; + +const MyReviewer = () => { + const [reviewerData, setReviewerData] = useState([]); + + const fetchReviewerData = async () => { + const res = await getMyReviewers(); + setReviewerData(res); + }; + + useEffect(() => { + fetchReviewerData(); + }, []); + + if (reviewerData.length === 0) { + return <>아직 리뷰어가 매칭되지 않았습니다! 조금만 기다려주세요🤗>; + } + + return ( + + + 아이디 + PR 링크 + 제출 여부 + + + {reviewerData.map((reviewer) => ( + + {reviewer.username} + + + + + 바로가기 + + + + + alert("버튼 클릭 완료!")} /> + + + ))} + + ); +}; + +export default MyReviewer; diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts new file mode 100644 index 000000000..bd053a61d --- /dev/null +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts @@ -0,0 +1,96 @@ +import styled from "styled-components"; + +export const RoomInfoCardContainer = styled.div` + display: flex; + + width: 100%; + padding-left: 2rem; + + border: 1px solid ${({ theme }) => theme.COLOR.grey1}; + border-radius: 1rem; + box-shadow: 0 4px 4px rgb(0 0 0 / 10%); +`; + +export const RoomInfoCardImg = styled.img` + overflow: hidden; + align-self: center; + + width: 15rem; + height: 100%; + + object-fit: scale-down; +`; + +export const RoomInfoCardContent = styled.div` + display: flex; + flex-direction: column; + + width: calc(100% - 15rem); + height: 100%; + padding: 2rem; +`; + +export const RoomHeaderWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + margin-bottom: 1rem; + padding-bottom: 1rem; + + border-bottom: 1px solid ${({ theme }) => theme.COLOR.grey1}; +`; + +export const RoomTitle = styled.span` + font: ${({ theme }) => theme.TEXT.large}; + color: ${({ theme }) => theme.COLOR.black}; +`; + +export const RepositoryLink = styled.a` + cursor: pointer; + + font: ${({ theme }) => theme.TEXT.small}; + color: ${({ theme }) => theme.COLOR.black}; + text-decoration: underline !important; + text-underline-offset: 0.3rem; + + &:hover { + color: ${({ theme }) => theme.COLOR.primary2}; + text-decoration: underline !important; + } +`; + +export const RoomContentBox = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 2rem; + + &:last-child { + margin-bottom: 0; + } +`; + +export const RoomContentSmall = styled.span` + font: ${({ theme }) => theme.TEXT.small}; + color: ${({ theme }) => theme.COLOR.black}; +`; + +export const RoomTagBox = styled.div` + display: flex; + flex-direction: row; + gap: 1rem; +`; + +export const RoomKeyword = styled.div` + display: flex; + align-items: center; + justify-content: center; + + padding: 0 1rem; + + background-color: ${({ theme }) => theme.COLOR.primary1}; + border: none; + border-radius: 5px; +`; diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx new file mode 100644 index 000000000..9158c8190 --- /dev/null +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx @@ -0,0 +1,73 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import Icon from "@/components/common/icon/Icon"; +import * as S from "@/components/roomDetailPage/roomInfoCard/RoomInfoCard.style"; +import { RoomInfo } from "@/@types/roomInfo"; +import { getRoomDetailInfo } from "@/apis/rooms.api"; + +const RoomInfoCard = () => { + const [roomInfo, setRoomInfo] = useState(); + const params = useParams(); + const missionId = params.id ? Number(params.id) : 0; + + const fetchRoomDetailInfoData = async () => { + const res = await getRoomDetailInfo(missionId); + setRoomInfo(res); + }; + + useEffect(() => { + fetchRoomDetailInfoData(); + }, []); + + if (!roomInfo) return <>>; + + return ( + + + + + {roomInfo.title} + + + 저장소 바로가기 + + + + + + {roomInfo.keywords.map((keyword, index) => ( + + #{keyword} + + ))} + + {roomInfo.content} + + + + + 방 생성자 : {roomInfo.manager} + + + + 현재 참여 인원 : {roomInfo.currentParticipantSize} / {roomInfo.maximumParticipantSize}명 + + + + 상호 리뷰 인원 : {roomInfo.matchingSize}명 + + + + 모집 마감일: {roomInfo.recruitmentDeadline} + + + + 리뷰 마감일: {roomInfo.reviewDeadline} + + + + + ); +}; + +export default RoomInfoCard; diff --git a/frontend/src/components/shared/roomCard/RoomCard.style.ts b/frontend/src/components/shared/roomCard/RoomCard.style.ts new file mode 100644 index 000000000..5f4156135 --- /dev/null +++ b/frontend/src/components/shared/roomCard/RoomCard.style.ts @@ -0,0 +1,82 @@ +import styled from "styled-components"; + +export const RoomCardContainer = styled.div` + overflow: hidden; + display: flex; + flex-direction: column; + + width: 14rem; + height: 18rem; + + border: 0.1rem solid ${({ theme }) => theme.COLOR.grey1}; + border-radius: 1.5rem; + box-shadow: 0 4px 4px rgb(0 0 0 / 10%); + + &:hover { + transform: scale(1.05); + transition: 0.3s; + transition-property: transform; + } + + &:active { + position: relative; + top: 3px; + box-shadow: 0 1px 1px rgb(0 0 0 / 10%); + } +`; + +export const RoomInfoThumbnail = styled.img` + width: 100%; + height: 10rem; + object-fit: scale-down; +`; + +export const RoomInformation = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + + width: 100%; + height: 8rem; + padding: 1rem; +`; + +export const MainInfo = styled.div` + display: flex; + flex-direction: column; +`; + +export const RoomTitle = styled.h2` + overflow: hidden; + font: ${({ theme }) => theme.TEXT.small}; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const RecruitmentDeadline = styled.p` + font: ${({ theme }) => theme.TEXT.semiSmall}; +`; + +export const EtcInfo = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + font: ${({ theme }) => theme.TEXT.xSmall}; +`; + +export const KeywordsContainer = styled.div` + display: flex; + gap: 0.2rem; +`; + +export const RoomKeyword = styled.span` + display: flex; + align-items: center; + justify-content: center; + + padding: 0 0.2rem; + + background-color: ${({ theme }) => theme.COLOR.primary1}; + border: none; + border-radius: 5px; +`; diff --git a/frontend/src/components/shared/roomCard/RoomCard.tsx b/frontend/src/components/shared/roomCard/RoomCard.tsx new file mode 100644 index 000000000..b3ba13cff --- /dev/null +++ b/frontend/src/components/shared/roomCard/RoomCard.tsx @@ -0,0 +1,33 @@ +import Icon from "@/components/common/icon/Icon"; +import * as S from "@/components/shared/roomCard/RoomCard.style"; +import { RoomInfo } from "@/@types/roomInfo"; + +const RoomCard = ({ roomInfo }: { roomInfo: RoomInfo }) => { + return ( + + + + + {roomInfo.title} + + + {roomInfo.recruitmentDeadline} + + + + + {roomInfo.keywords.map((keyword) => ( + #{keyword} + ))} + + + + {roomInfo.currentParticipantSize}/{roomInfo.maximumParticipantSize} + + + + + ); +}; + +export default RoomCard; diff --git a/frontend/src/components/shared/roomList/RoomList.style.ts b/frontend/src/components/shared/roomList/RoomList.style.ts new file mode 100644 index 000000000..82e53effc --- /dev/null +++ b/frontend/src/components/shared/roomList/RoomList.style.ts @@ -0,0 +1,8 @@ +import styled from "styled-components"; + +export const RoomListContainer = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + gap: 5rem; + justify-content: space-between; +`; diff --git a/frontend/src/components/shared/roomList/RoomList.tsx b/frontend/src/components/shared/roomList/RoomList.tsx new file mode 100644 index 000000000..b6daf5cb1 --- /dev/null +++ b/frontend/src/components/shared/roomList/RoomList.tsx @@ -0,0 +1,20 @@ +import { Link } from "react-router-dom"; +import RoomCard from "@/components/shared/roomCard/RoomCard"; +import * as S from "@/components/shared/roomList/RoomList.style"; +import { RoomInfo } from "@/@types/roomInfo"; + +const RoomList = ({ roomList }: { roomList: RoomInfo[] }) => { + if (!roomList) return <>>; + + return ( + + {roomList.map((roomInfo) => ( + + + + ))} + + ); +}; + +export default RoomList; diff --git a/frontend/src/components/test.style.ts b/frontend/src/components/test.style.ts deleted file mode 100644 index cb4194bcc..000000000 --- a/frontend/src/components/test.style.ts +++ /dev/null @@ -1,12 +0,0 @@ -import styled from "styled-components"; - -const Container = styled.div` - z-index: 99; - - display: none; - - width: 10px; - height: 10px; - margin: 10px; - padding: 10px; -`; diff --git a/frontend/src/components/test.test.ts b/frontend/src/components/test.test.ts deleted file mode 100644 index 9002990b1..000000000 --- a/frontend/src/components/test.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -describe("테스트임", () => { - it("두번째 테스트임", () => { - expect(1 + 1).toBe(2); - }); - - it("ㅌㅌ2", () => { - expect(3 + 3).toBe(5); - }); -}); diff --git a/frontend/src/components/test2.tsx b/frontend/src/components/test2.tsx deleted file mode 100644 index 275069961..000000000 --- a/frontend/src/components/test2.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Test = () => { - return test; -}; - -export default Test; diff --git a/frontend/src/config/serverUrl.ts b/frontend/src/config/serverUrl.ts new file mode 100644 index 000000000..f5d2125af --- /dev/null +++ b/frontend/src/config/serverUrl.ts @@ -0,0 +1 @@ +export const serverUrl = "http://192.168.1.24:8080"; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index dda6e616a..54929c60d 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,10 +1,26 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider } from "react-router-dom"; +import { ThemeProvider } from "styled-components"; import router from "@/router"; +import GlobalStyles from "@/styles/globalStyles"; +import { theme } from "@/styles/theme"; -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +const enableMocking = async () => { + if (process.env.NODE_ENV !== "development") { + return; + } + const { worker } = await import("./mocks/browser"); + // return worker.start(); +}; + +enableMocking().then(() => { + ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + , + ); +}); diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts new file mode 100644 index 000000000..d10720f78 --- /dev/null +++ b/frontend/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from "msw/browser"; +import missionHandler from "@/mocks/handler/missionHandler"; + +export const worker = setupWorker(...missionHandler); diff --git a/frontend/src/mocks/handler/missionHandler.ts b/frontend/src/mocks/handler/missionHandler.ts new file mode 100644 index 000000000..d30476199 --- /dev/null +++ b/frontend/src/mocks/handler/missionHandler.ts @@ -0,0 +1,22 @@ +import { HttpResponse, http } from "msw"; +import { API_ENDPOINTS } from "@/apis/endpoints"; + +const reviewerInfo = [ + { + email: "reviewer1@email.com", + }, + { + email: "reviewer2@email.com", + }, + { + email: "reviewer3@email.com", + }, +]; + +const missionHandler = [ + http.get(API_ENDPOINTS.REVIEWER, () => { + return HttpResponse.json({ reviewerInfo }, { status: 200 }); + }), +]; + +export default missionHandler; diff --git a/frontend/src/mocks/server.ts b/frontend/src/mocks/server.ts new file mode 100644 index 000000000..423310750 --- /dev/null +++ b/frontend/src/mocks/server.ts @@ -0,0 +1,4 @@ +import { setupServer } from "msw/node"; +import missionHandler from "@/mocks/handler/missionHandler"; + +export const server = setupServer(...missionHandler); diff --git a/frontend/src/pages/main/MainPage.style.ts b/frontend/src/pages/main/MainPage.style.ts new file mode 100644 index 000000000..50781335d --- /dev/null +++ b/frontend/src/pages/main/MainPage.style.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 5rem; +`; diff --git a/frontend/src/pages/main/MainPage.tsx b/frontend/src/pages/main/MainPage.tsx new file mode 100644 index 000000000..897d19e5b --- /dev/null +++ b/frontend/src/pages/main/MainPage.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import ContentForm from "@/components/common/contentForm/ContentForm"; +import RoomList from "@/components/shared/roomList/RoomList"; +import * as S from "@/pages/main/MainPage.style"; +import { RoomInfo } from "@/@types/roomInfo"; +import { getRoomList } from "@/apis/rooms.api"; + +const MainPage = () => { + const [roomList, setRoomList] = useState<{ rooms: RoomInfo[] }>(); + + const fetchRoomListData = async () => { + const res = await getRoomList(); + setRoomList(res); + }; + + useEffect(() => { + fetchRoomListData(); + }, []); + + return ( + + + {roomList && } + + + ); +}; + +export default MainPage; diff --git a/frontend/src/pages/roomDetail/RoomDetailPage.style.ts b/frontend/src/pages/roomDetail/RoomDetailPage.style.ts new file mode 100644 index 000000000..50781335d --- /dev/null +++ b/frontend/src/pages/roomDetail/RoomDetailPage.style.ts @@ -0,0 +1,7 @@ +import styled from "styled-components"; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 5rem; +`; diff --git a/frontend/src/pages/roomDetail/RoomDetailPage.tsx b/frontend/src/pages/roomDetail/RoomDetailPage.tsx new file mode 100644 index 000000000..1e9da6631 --- /dev/null +++ b/frontend/src/pages/roomDetail/RoomDetailPage.tsx @@ -0,0 +1,24 @@ +import ContentForm from "@/components/common/contentForm/ContentForm"; +import MyReviewee from "@/components/roomDetailPage/myReviewee/MyReviewee"; +import MyReviewer from "@/components/roomDetailPage/myReviewer/MyReviewer"; +import RoomInfoCard from "@/components/roomDetailPage/roomInfoCard/RoomInfoCard"; +import * as S from "@/pages/roomDetail/RoomDetailPage.style"; + +const RoomDetailPage = () => { + return ( + + + + + + + + + + + + + ); +}; + +export default RoomDetailPage; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 51121ca47..61f23d3c5 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,13 +1,20 @@ import { createBrowserRouter } from "react-router-dom"; -import App from "@/App"; +import Layout from "@/components/layout/Layout"; +import MainPage from "@/pages/main/MainPage"; +import RoomDetailPage from "@/pages/roomDetail/RoomDetailPage"; const router = createBrowserRouter([ { path: "/", + element: , children: [ { index: true, - element: , + element: , + }, + { + path: "room/:id", + element: , }, ], }, diff --git a/frontend/src/styles/globalStyles.ts b/frontend/src/styles/globalStyles.ts new file mode 100644 index 000000000..d19c2d6fe --- /dev/null +++ b/frontend/src/styles/globalStyles.ts @@ -0,0 +1,211 @@ +import { createGlobalStyle } from "styled-components"; + +const globalStyles = createGlobalStyle` + scrollbar-width: none; + + :root { + font-size: 62.5%; + } + + *, + *::before, + *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + html, + body, + div, + span, + applet, + object, + iframe, + h1, + h2, + h3, + h4, + h5, + h6, + p, + blockquote, + pre, + a, + abbr, + acronym, + address, + big, + cite, + code, + del, + dfn, + em, + img, + ins, + kbd, + q, + s, + samp, + small, + strike, + strong, + sub, + sup, + tt, + var, + b, + u, + i, + center, + dl, + dt, + dd, + ol, + ul, + li, + fieldset, + form, + label, + legend, + table, + caption, + tbody, + tfoot, + thead, + tr, + th, + td, + article, + aside, + canvas, + details, + embed, + figure, + figcaption, + footer, + header, + hgroup, + menu, + nav, + output, + ruby, + section, + summary, + time, + mark, + audio, + video { + margin: 0; + padding: 0; + + font: inherit; + font-size: 100%; + vertical-align: baseline; + + border: 0; + } + + /* HTML5 display-role reset for older browsers */ + article, + aside, + details, + figcaption, + figure, + footer, + header, + hgroup, + menu, + nav, + section { + display: block; + } + + body { + line-height: 1; + } + + ol, + ul { + list-style: none; + } + + blockquote, + q { + quotes: none; + } + + blockquote::before, + blockquote::after, + q::before, + q::after { + content: ''; + content: none; + } + + table { + border-spacing: 0; + border-collapse: collapse; + } + + button { + border: none; + } + + a { + color: inherit; + text-decoration: none !important; + } + + a:hover { + text-decoration: none !important; + } + + a:visited { + color: inherit; + text-decoration: none; + } + + ul, + li { + list-style: none; + } + + /* h1 { + font-size: 2.5em; + } + + h2 { + font-size: 2.2em; + } + + h3 { + font-size: 2em; + } + + h4 { + font-size: 1.6em; + } + + h5 { + font-size: 1.3em; + } */ + + ::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +::-webkit-scrollbar-thumb { + height: 30%; + background: rgb(132 174 225 / 70%); + border-radius: 10px; +} + +::-webkit-scrollbar-track { + background: rgb(132 174 225 / 20%); +} + +`; + +export default globalStyles; diff --git a/frontend/src/styles/style.d.ts b/frontend/src/styles/style.d.ts new file mode 100644 index 000000000..bad8c37e5 --- /dev/null +++ b/frontend/src/styles/style.d.ts @@ -0,0 +1,6 @@ +import "styled-components"; +import { ThemeType } from "@/styles/theme"; + +declare module "styled-components" { + export interface DefaultTheme extends ThemeType {} +} diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts new file mode 100644 index 000000000..df8cbb815 --- /dev/null +++ b/frontend/src/styles/theme.ts @@ -0,0 +1,28 @@ +const COLOR = { + white: "#ffffff", + grey1: "#c6c6c6", + grey2: "#919191", + grey3: "#5e5e5e", + grey4: "#303030", + black: "#000000", + primary1: "#D9F3FF", + primary2: "#84AEE1", + primary3: "#607999", + secondary: "#FF3D45", +}; + +const TEXT = { + xLarge: "800 1.8rem/1.4rem hanna", + large: "700 1.4rem/1.4rem hanna", + medium: "500 1.2rem/1.4rem hanna", + small: "400 1.0rem/1.4rem hanna", + semiSmall: "400 0.8rem/1.4rem hanna", + xSmall: "400 0.6rem/1.4rem hanna", +}; + +export const theme = { + COLOR, + TEXT, +}; + +export type ThemeType = typeof theme; diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..580cea780 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,7 @@ +interface ImportMetaEnv { + readonly SERVER_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a4319d3c6..c413e4dec 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -114,5 +114,5 @@ "@/*": ["src/*"] } }, - "include": ["src/**/*"] + "include": ["src/**/*", "jest.setup.js"] } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index dc047569b..48059333a 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -1,9 +1,9 @@ -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const { CleanWebpackPlugin } = require("clean-webpack-plugin"); -const path = require("path"); -const webpack = require("webpack"); +import { CleanWebpackPlugin } from "clean-webpack-plugin"; +import HtmlWebpackPlugin from "html-webpack-plugin"; +import path from "path"; +import webpack from "webpack"; -module.exports = (env, argv) => { +export default (env, argv) => { const prod = argv.mode === "production"; return { @@ -11,20 +11,21 @@ module.exports = (env, argv) => { devtool: prod ? "hidden-source-map" : "eval", entry: "./src/index.tsx", output: { - path: path.join(__dirname, "/dist"), + path: path.join(path.resolve(), "/dist"), filename: "index.js", + publicPath: "/", }, devServer: { + historyApiFallback: true, port: 3000, hot: true, }, resolve: { - extensions: [".js", ".jsx", ".ts", ".tsx"], + extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".mjs"], alias: { - "@": path.resolve(__dirname, "src/"), + "@": path.resolve(path.resolve(), "src/"), }, }, - devtool: prod ? false : "source-map", module: { rules: [ {