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} + + + + + 바로가기 + + + + +