diff --git a/frontend/package.json b/frontend/package.json
index 85311c09c..c176b2310 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -52,7 +52,8 @@
"html-webpack-plugin": "^5.6.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
- "msw": "^2.3.1",
+ "jest-fixed-jsdom": "^0.0.2",
+ "msw": "2.3.2",
"postcss-syntax": "^0.36.2",
"prettier": "^3.3.2",
"stylelint": "^16.7.0",
@@ -62,5 +63,10 @@
"webpack": "^5.92.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
+ },
+ "msw": {
+ "workerDirectory": [
+ "public"
+ ]
}
}
diff --git a/frontend/public/index.html b/frontend/public/index.html
index ee0fd573b..af6879ad3 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -8,7 +8,7 @@
-
Document
+ reveiw me
diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js
new file mode 100644
index 000000000..6185b3c32
--- /dev/null
+++ b/frontend/public/mockServiceWorker.js
@@ -0,0 +1,281 @@
+/* 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.4';
+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/apis/endpoints.ts b/frontend/src/apis/endpoints.ts
index 4bfb49bf6..2fbf1d531 100644
--- a/frontend/src/apis/endpoints.ts
+++ b/frontend/src/apis/endpoints.ts
@@ -1,8 +1,8 @@
const endPoint = {
- postingReview: `${process.env.API_BASE_URL}/reviews`,
- gettingDetailedReview: (reviewId: number) => `${process.env.API_BASE_URL}/reviews/${reviewId}`,
- gettingDataToWriteReview: (reviewerGroupId: number) => `/reviewer-groups/${reviewerGroupId}`,
- gettingKeyword: `${process.env.API_BASE_URL}/keywords`,
+ postingReview: `${process.env.API_BASE_URL}reviews`,
+ gettingDetailedReview: (reviewId: number) => `${process.env.API_BASE_URL}reviews/${reviewId}`,
+ gettingInfoToWriteReview: (reviewerGroupId: number) => `/reviewer-groups/${reviewerGroupId}`,
+ gettingKeyword: `${process.env.API_BASE_URL}keywords`,
};
export default endPoint;
diff --git a/frontend/src/components/common/MultilineTextViewer/index.tsx b/frontend/src/components/common/MultilineTextViewer/index.tsx
new file mode 100644
index 000000000..14fc0f6d7
--- /dev/null
+++ b/frontend/src/components/common/MultilineTextViewer/index.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+interface MultilineTextViewerProps {
+ text: string;
+}
+
+const MultilineTextViewer = ({ text }: MultilineTextViewerProps) => {
+ return (
+ <>
+ {text.split('\n').map((line, index) => (
+
+ {line}
+
+
+ ))}
+ >
+ );
+};
+
+export default MultilineTextViewer;
diff --git a/frontend/src/components/common/ProjectImg/index.tsx b/frontend/src/components/common/ProjectImg/index.tsx
new file mode 100644
index 000000000..9b6860c42
--- /dev/null
+++ b/frontend/src/components/common/ProjectImg/index.tsx
@@ -0,0 +1,16 @@
+import GithubLogoIcon from '@/assets/githubLogo.svg';
+
+import * as S from './styles';
+
+export interface ProjectImgProps {
+ thumbnailUrl?: string;
+ projectName: string;
+ $size: string;
+}
+const ProjectImg = ({ thumbnailUrl, projectName, $size }: ProjectImgProps) => {
+ const src = thumbnailUrl ?? GithubLogoIcon;
+ const alt = thumbnailUrl ? `${projectName} 저장소 이미지` : '깃허브 로고';
+ return ;
+};
+
+export default ProjectImg;
diff --git a/frontend/src/components/common/ProjectImg/styles.ts b/frontend/src/components/common/ProjectImg/styles.ts
new file mode 100644
index 000000000..e125a3316
--- /dev/null
+++ b/frontend/src/components/common/ProjectImg/styles.ts
@@ -0,0 +1,11 @@
+import styled from '@emotion/styled';
+
+interface ImgProps {
+ $size: string;
+}
+
+export const Img = styled.img`
+ width: ${(props) => props.$size};
+ height: ${(props) => props.$size};
+ border-radius: ${({ theme }) => theme.borderRadius.basic};
+`;
diff --git a/frontend/src/components/common/ReviewComment/index.tsx b/frontend/src/components/common/ReviewComment/index.tsx
new file mode 100644
index 000000000..64351c54f
--- /dev/null
+++ b/frontend/src/components/common/ReviewComment/index.tsx
@@ -0,0 +1,11 @@
+import * as S from './styles';
+
+interface ReviewCommentProps {
+ comment: string;
+}
+
+const ReviewComments = ({ comment }: ReviewCommentProps) => {
+ return {comment};
+};
+
+export default ReviewComments;
diff --git a/frontend/src/components/common/ReviewComment/styles.ts b/frontend/src/components/common/ReviewComment/styles.ts
new file mode 100644
index 000000000..3073cad59
--- /dev/null
+++ b/frontend/src/components/common/ReviewComment/styles.ts
@@ -0,0 +1,13 @@
+import styled from '@emotion/styled';
+
+export const ReviewComment = styled.p`
+ width: inherit;
+ height: 3rem;
+ margin-top: 1.6rem;
+ padding-left: 2.5rem;
+
+ font-size: ${({ theme }) => theme.fontSize.basic};
+ font-weight: ${({ theme }) => theme.fontWeight.bold};
+
+ border-left: 0.4rem solid ${({ theme }) => theme.colors.black};
+`;
diff --git a/frontend/src/components/common/ReviewDate/index.tsx b/frontend/src/components/common/ReviewDate/index.tsx
new file mode 100644
index 000000000..da552daec
--- /dev/null
+++ b/frontend/src/components/common/ReviewDate/index.tsx
@@ -0,0 +1,25 @@
+import ClockIcon from '@/assets/clock.svg';
+import { formatDate } from '@/utils';
+
+import * as S from './styles';
+
+export interface ReviewDateProps {
+ date: Date;
+ dateTitle: string;
+}
+
+const ReviewDate = ({ date, dateTitle }: ReviewDateProps) => {
+ const { year, month, day } = formatDate(date);
+ return (
+
+
+ {dateTitle}
+ :
+
+ {year}-{month}-{day}
+
+
+ );
+};
+
+export default ReviewDate;
diff --git a/frontend/src/components/common/ReviewDate/styles.ts b/frontend/src/components/common/ReviewDate/styles.ts
new file mode 100644
index 000000000..7b27f61e1
--- /dev/null
+++ b/frontend/src/components/common/ReviewDate/styles.ts
@@ -0,0 +1,17 @@
+import styled from '@emotion/styled';
+
+export const ClockImg = styled.img`
+ width: auto;
+ height: 1.6rem;
+ margin-right: 0.8rem;
+`;
+
+export const ReviewDate = styled.div`
+ display: flex;
+ align-items: center;
+ font-size: 1.6rem;
+`;
+
+export const Colon = styled.span`
+ margin: 0 1rem;
+`;
diff --git a/frontend/src/components/common/index.tsx b/frontend/src/components/common/index.tsx
index 949cf5f30..88e05dc64 100644
--- a/frontend/src/components/common/index.tsx
+++ b/frontend/src/components/common/index.tsx
@@ -1 +1,5 @@
export { default as SearchInput } from './SearchInput';
+export { default as ProjectImg } from './ProjectImg';
+export { default as ReviewDate } from './ReviewDate';
+export { default as ReviewComment } from './ReviewComment';
+export { default as MultilineTextViewer } from './MultilineTextViewer';
diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx
index deefb9532..a54853d52 100644
--- a/frontend/src/components/index.tsx
+++ b/frontend/src/components/index.tsx
@@ -1 +1,2 @@
export * from './layouts';
+export * from './common';
diff --git a/frontend/src/components/layouts/PageLayout/styles.ts b/frontend/src/components/layouts/PageLayout/styles.ts
index e9ebc1de1..e6e7c64dc 100644
--- a/frontend/src/components/layouts/PageLayout/styles.ts
+++ b/frontend/src/components/layouts/PageLayout/styles.ts
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
export const Layout = styled.div`
- width: 100vw;
+ width: 100%;
background-color: ${({ theme }) => theme.colors.sidebarBackground};
`;
@@ -12,7 +12,7 @@ export const Wrapper = styled.div`
display: flex;
flex-direction: column;
- width: inherit;
+ width: 100%;
background-color: ${({ theme }) => theme.colors.white};
`;
diff --git a/frontend/src/components/layouts/Sidebar/index.tsx b/frontend/src/components/layouts/Sidebar/index.tsx
index 327ff11ec..de745aeb0 100644
--- a/frontend/src/components/layouts/Sidebar/index.tsx
+++ b/frontend/src/components/layouts/Sidebar/index.tsx
@@ -10,7 +10,7 @@ const PATH = {
myPage: '/user/mypage',
reviewWriting: '/user/review-writing',
allReview: '/user/all-review',
- detailedReview: '/user/detailed-review',
+ detailedReview: '/user/detailed-review/0',
reviewGroupManagement: '/user/review-group-management',
};
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index cf98a429a..99c10d954 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -29,7 +29,7 @@ const router = createBrowserRouter([
element: ,
},
{
- path: 'user/detailed-review',
+ path: 'user/detailed-review/:id',
element: ,
},
],
@@ -37,11 +37,21 @@ const router = createBrowserRouter([
]);
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
-root.render(
-
-
-
-
-
- ,
-);
+
+async function enableMocking() {
+ if (process.env.NODE_ENV === 'development') {
+ const { worker } = await import('./mocks/browser');
+ return worker.start();
+ }
+}
+
+enableMocking().then(() => {
+ root.render(
+
+
+
+
+
+ ,
+ );
+});
diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts
new file mode 100644
index 000000000..93c00a0f1
--- /dev/null
+++ b/frontend/src/mocks/browser.ts
@@ -0,0 +1,8 @@
+/* eslint-disable */
+import { setupWorker } from 'msw/browser';
+
+import handlers from './handlers';
+
+export const worker = setupWorker(...handlers);
+
+export default worker;
diff --git a/frontend/src/mocks/handlers/index.ts b/frontend/src/mocks/handlers/index.ts
new file mode 100644
index 000000000..8bb8e8612
--- /dev/null
+++ b/frontend/src/mocks/handlers/index.ts
@@ -0,0 +1,5 @@
+import reviewHandler from './review';
+
+const handlers = [...reviewHandler];
+
+export default handlers;
diff --git a/frontend/src/mocks/handlers/review.ts b/frontend/src/mocks/handlers/review.ts
new file mode 100644
index 000000000..6feb7b3d3
--- /dev/null
+++ b/frontend/src/mocks/handlers/review.ts
@@ -0,0 +1,14 @@
+import { http, HttpResponse } from 'msw';
+
+import endPoint from '@/apis/endpoints';
+
+import { DETAILED_REVIEW_MOCK_DATA } from '../mockData/detailedReviewMockData';
+
+const getDetailedReview = () =>
+ http.get(endPoint.gettingDetailedReview(0), async ({ request }) => {
+ return HttpResponse.json(DETAILED_REVIEW_MOCK_DATA);
+ });
+
+const reviewHandler = [getDetailedReview()];
+
+export default reviewHandler;
diff --git a/frontend/src/mocks/mockData/detailedReviewMockData.ts b/frontend/src/mocks/mockData/detailedReviewMockData.ts
new file mode 100644
index 000000000..19311520f
--- /dev/null
+++ b/frontend/src/mocks/mockData/detailedReviewMockData.ts
@@ -0,0 +1,34 @@
+import { DetailReviewData } from '@/types';
+
+const ANSWER =
+ '림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. 림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. ';
+
+export const DETAILED_REVIEW_MOCK_DATA: DetailReviewData = {
+ id: 123456,
+ createdAt: new Date('2024-07-16'),
+ reviewerGroup: {
+ id: 123456,
+ name: 'review-me',
+ deadline: new Date('2024-07-01'),
+ reviewee: {
+ id: 78910,
+ name: '바다',
+ },
+ },
+ contents: [
+ {
+ id: 23456,
+ question: '[공개] 동료의 개발 역량 향상을 위해 피드백을 남겨 주세요.',
+ answer: ANSWER,
+ },
+ { id: 567810, question: '[공개] 동료의 소프트 스킬의 성장을 위해 피드백을 남겨 주세요.', answer: ANSWER },
+ { id: 98761, question: '[비공개] 팀 동료로 근무한다면 같이 일 하고 싶은 개발자인가요?', answer: ANSWER },
+ ],
+ keywords: [
+ { id: 1, detail: '친절해요' },
+ { id: 12, detail: '친절합니다!' },
+ { id: 11, detail: '친절해요요요요요' },
+ { id: 14, detail: '친절해해해해해' },
+ { id: 18, detail: '친절해요요용' },
+ ],
+};
diff --git a/frontend/src/mocks/server.ts b/frontend/src/mocks/server.ts
new file mode 100644
index 000000000..8f041004b
--- /dev/null
+++ b/frontend/src/mocks/server.ts
@@ -0,0 +1,7 @@
+import { setupServer } from 'msw/node';
+
+import handlers from './handlers';
+
+const server = setupServer(...handlers);
+
+export default server;
diff --git a/frontend/src/pages/DetailedReviewPage/components/KeywordSection/index.tsx b/frontend/src/pages/DetailedReviewPage/components/KeywordSection/index.tsx
new file mode 100644
index 000000000..b94b4de94
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/KeywordSection/index.tsx
@@ -0,0 +1,26 @@
+import { Keyword } from '@/types';
+
+import ReviewSectionHeader from '../ReviewSectionHeader';
+
+import * as S from './styles';
+
+interface KeywordSectionProps {
+ keywords: Keyword[];
+ index: number;
+}
+const KEY_WORD_HEADER = '키워드';
+
+const KeywordSection = ({ keywords, index }: KeywordSectionProps) => {
+ return (
+
+
+
+ {keywords.map(({ id, detail }) => (
+ {detail}
+ ))}
+
+
+ );
+};
+
+export default KeywordSection;
diff --git a/frontend/src/pages/DetailedReviewPage/components/KeywordSection/styles.ts b/frontend/src/pages/DetailedReviewPage/components/KeywordSection/styles.ts
new file mode 100644
index 000000000..b3b8900f8
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/KeywordSection/styles.ts
@@ -0,0 +1,31 @@
+import styled from '@emotion/styled';
+
+export const KeywordSection = styled.section`
+ width: 100%;
+ margin-top: 3.2rem;
+`;
+
+export const KeywordContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ row-gap: 3.2rem;
+ column-gap: 2.4rem;
+`;
+
+export const KeywordBox = styled.div`
+ display: flex;
+ align-items: center;
+
+ box-sizing: border-box;
+ height: 5rem;
+ height: fit-content;
+ padding: 0.8rem 2.5rem;
+
+ font-size: 1.6rem;
+ line-height: 2.4rem;
+ text-align: center;
+
+ background-color: ${({ theme }) => theme.colors.lightPurple};
+ border: 0.1rem solid ${({ theme }) => theme.colors.primary};
+ border-radius: ${({ theme }) => theme.borderRadius.basic};
+`;
diff --git a/frontend/src/pages/DetailedReviewPage/components/LockButton/index.tsx b/frontend/src/pages/DetailedReviewPage/components/LockButton/index.tsx
deleted file mode 100644
index 38c864cde..000000000
--- a/frontend/src/pages/DetailedReviewPage/components/LockButton/index.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import LockIcon from '../../../../assets/Lock.svg';
-import UnlockIcon from '../../../../assets/Unlock.svg';
-
-interface LockButtonProps {
- isLock: boolean;
- onClick: () => void;
-}
-
-const IMAGE = {
- lock: {
- src: LockIcon,
- alt: 'lock icon',
- },
- unlock: {
- src: UnlockIcon,
- alt: 'unlock icon',
- },
-};
-
-const LockButton = ({ isLock, onClick }: LockButtonProps) => {
- const { src, alt } = isLock ? IMAGE.lock : IMAGE.unlock;
- return (
-
- );
-};
-
-export default LockButton;
diff --git a/frontend/src/pages/DetailedReviewPage/components/LockToggle/index.tsx b/frontend/src/pages/DetailedReviewPage/components/LockToggle/index.tsx
new file mode 100644
index 000000000..50f44e5d0
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/LockToggle/index.tsx
@@ -0,0 +1,47 @@
+import LockIcon from '@/assets/lock.svg';
+import UnlockIcon from '@/assets/unLock.svg';
+
+import * as S from './styles';
+
+const IMAGE = {
+ lock: {
+ src: LockIcon,
+ alt: 'lock icon',
+ text: '비공개',
+ },
+ unlock: {
+ src: UnlockIcon,
+ alt: 'unlock icon',
+ text: '공개',
+ },
+};
+interface ToggleButtonProps {
+ name: keyof typeof IMAGE;
+ $isPublic: boolean;
+ handleClickToggleButton: () => void;
+}
+
+const ToggleButton = ({ name, $isPublic, handleClickToggleButton }: ToggleButtonProps) => {
+ const { src, alt, text } = IMAGE[name];
+ const $isActive = name === 'lock' ? $isPublic : !$isPublic;
+
+ return (
+
+
+ {text}
+
+ );
+};
+
+interface LockToggleProps extends Omit {}
+
+const LockToggle = (props: LockToggleProps) => {
+ return (
+
+
+
+
+ );
+};
+
+export default LockToggle;
diff --git a/frontend/src/pages/DetailedReviewPage/components/LockToggle/styles.ts b/frontend/src/pages/DetailedReviewPage/components/LockToggle/styles.ts
new file mode 100644
index 000000000..1dadbb62f
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/LockToggle/styles.ts
@@ -0,0 +1,40 @@
+import styled from '@emotion/styled';
+
+interface ButtonProps {
+ $isActive: boolean;
+}
+export const Button = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: 9rem;
+ height: 3rem;
+ padding: 0.8rem 0.8rem;
+
+ font-size: ${({ theme }) => theme.fontSize.small};
+ font-weight: ${({ theme }) => theme.fontWeight.bold};
+ color: ${(props) => (props.$isActive ? props.theme.colors.black : props.theme.colors.gray)};
+
+ background-color: ${(props) => (props.$isActive ? props.theme.colors.white : 'transparent')};
+ border-radius: ${({ theme }) => theme.borderRadius.basic};
+ img {
+ width: 1.4rem;
+ height: 1.4rem;
+ margin-right: 0.4rem;
+ fill: ${(props) => (props.$isActive ? props.theme.colors.black : props.theme.colors.gray)};
+ }
+`;
+
+export const LockToggle = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ width: 20rem;
+ height: 4rem;
+ padding: 0.5rem 0.6rem;
+
+ background-color: ${({ theme }) => theme.colors.lightGray};
+ border-radius: ${({ theme }) => theme.borderRadius.basic};
+`;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewAnswer/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewAnswer/index.tsx
deleted file mode 100644
index 176861cec..000000000
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewAnswer/index.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import * as S from './styles';
-
-interface ReviewAnswerProps {
- answer: string;
-}
-
-// NOTE: 개행 문자 처리하는 함수
-const applyNewLine = (text: string): React.ReactNode => {
- return text.split('\n').map((line, index) => (
-
- {line}
-
-
- ));
-};
-
-const ReviewAnswer = ({ answer }: ReviewAnswerProps) => {
- return {applyNewLine(answer)};
-};
-
-export default ReviewAnswer;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewAnswer/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewAnswer/styles.ts
deleted file mode 100644
index a3ce591a5..000000000
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewAnswer/styles.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import styled from '@emotion/styled';
-
-export const Answer = styled.article`
- margin-bottom: 1.5rem;
-`;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx
index 03ac14021..fe0f94355 100644
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx
+++ b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/index.tsx
@@ -1,44 +1,37 @@
-import LockButton from '../LockButton';
-import * as S from './styles';
+import { ProjectImg, ReviewDate } from '@/components';
+import { ProjectImgProps } from '@/components/common/ProjectImg';
+import { ReviewDateProps } from '@/components/common/ReviewDate';
-interface ReviewDescriptionItemProps {
- title: string;
- contents: string;
-}
-const ReviewDescriptionItem = ({ title, contents }: ReviewDescriptionItemProps) => {
- return (
-
- {title}
- :
- {contents}
-
- );
-};
+import LockToggle from '../LockToggle';
-interface ReviewDescriptionProps {
- projectName: string;
- createdAt: Date;
- isLock: boolean;
-}
+import * as S from './styles';
-const formatDate = (date: Date) => {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- //const minutes = String(date.getMinutes()).padStart(2, '0');
+const PROJECT_IMAGE_SIZE = '6rem';
- return `${year}/${month}/${day}`;
-};
+interface ReviewDescriptionProps extends Omit, Omit {
+ isPublic: boolean;
+ handleClickToggleButton: () => void;
+}
-const ReviewDescription = ({ projectName, createdAt, isLock }: ReviewDescriptionProps) => {
+const ReviewDescription = ({
+ thumbnailUrl,
+ projectName,
+ isPublic,
+ date,
+ handleClickToggleButton,
+}: ReviewDescriptionProps) => {
return (
-
-
- console.log('lock')} />
-
-
-
+
+
+
+ {projectName}
+
+
+
+
+
+
);
};
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts
index ce142dfe3..799be41d7 100644
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts
+++ b/frontend/src/pages/DetailedReviewPage/components/ReviewDescription/styles.ts
@@ -1,33 +1,27 @@
import styled from '@emotion/styled';
-export const Description = styled.ul`
+export const Description = styled.section`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
width: 100%;
+ height: 6rem;
margin: 0;
padding-left: 0;
`;
-
-export const ProjectAndLockButtonContainer = styled.div`
+export const DescriptionSide = styled.div`
display: flex;
- justify-content: space-between;
- width: 100%;
`;
-export const ListItem = styled.li`
- padding: 0;
- list-style: none;
-
- span {
- display: inline-block;
- }
-`;
-export const Title = styled.span`
- width: 6rem;
- font-size: 1.1rem;
- font-weight: bold;
-`;
-export const Clone = styled.span`
- margin: 0 0.5rem;
- font-size: 1rem;
+export const ProjectNameAndDateContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ margin-left: 1rem;
`;
-export const Contents = styled.span`
- font-size: 1rem;
+
+export const ProjectName = styled.p`
+ margin-top: 0;
+ font-size: ${({ theme }) => theme.fontSize.medium};
+ font-weight: ${({ theme }) => theme.fontWeight.bold};
`;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewQuestion/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewQuestion/index.tsx
deleted file mode 100644
index 7b9921932..000000000
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewQuestion/index.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import * as S from './styles';
-
-interface ReviewQuestionProps {
- question: string;
-}
-
-const ReviewQuestion = ({ question }: ReviewQuestionProps) => {
- return {question};
-};
-
-export default ReviewQuestion;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewQuestion/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewQuestion/styles.ts
deleted file mode 100644
index a230cccfd..000000000
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewQuestion/styles.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import styled from '@emotion/styled';
-
-export const Question = styled.p`
- margin-bottom: 0.5rem;
- font-size: 1rem;
- font-weight: bold;
-`;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSection/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewSection/index.tsx
new file mode 100644
index 000000000..c730feeaf
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/ReviewSection/index.tsx
@@ -0,0 +1,23 @@
+import { MultilineTextViewer } from '@/components';
+
+import ReviewSectionHeader from '../ReviewSectionHeader';
+
+import * as S from './styles';
+interface ReviewSectionProps {
+ question: string;
+ answer: string;
+ index: number;
+}
+
+const ReviewSection = ({ question, answer, index }: ReviewSectionProps) => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default ReviewSection;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSection/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewSection/styles.ts
new file mode 100644
index 000000000..eb8917aa8
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/ReviewSection/styles.ts
@@ -0,0 +1,20 @@
+import styled from '@emotion/styled';
+
+export const ReviewSection = styled.section`
+ width: 100%;
+ margin-top: 3.2rem;
+`;
+
+export const Answer = styled.div`
+ overflow-y: auto;
+
+ box-sizing: border-box;
+ width: 100%;
+ height: 23rem;
+ padding: 1rem 1.5rem;
+
+ font-size: 1.6rem;
+ line-height: 2.4rem;
+
+ background-color: ${({ theme }) => theme.colors.lightGray};
+`;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/index.tsx
new file mode 100644
index 000000000..ebc2b5a1f
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/index.tsx
@@ -0,0 +1,16 @@
+import * as S from './styles';
+
+interface ReviewSectionHeaderProps {
+ number: number;
+ text: string;
+}
+
+const ReviewSectionHeader = ({ number, text }: ReviewSectionHeaderProps) => {
+ return (
+
+ {number}. {text}
+
+ );
+};
+
+export default ReviewSectionHeader;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/styles.ts
new file mode 100644
index 000000000..517c62e27
--- /dev/null
+++ b/frontend/src/pages/DetailedReviewPage/components/ReviewSectionHeader/styles.ts
@@ -0,0 +1,7 @@
+import styled from '@emotion/styled';
+
+export const ReviewSectionHeader = styled.p`
+ margin-bottom: 1rem;
+ font-size: 1.6rem;
+ font-weight: bold;
+`;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewViewSection/index.tsx b/frontend/src/pages/DetailedReviewPage/components/ReviewViewSection/index.tsx
deleted file mode 100644
index fed1624b1..000000000
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewViewSection/index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import ReviewQuestion from '../ReviewQuestion';
-import ReviewAnswer from '../ReviewAnswer';
-import * as S from './styles';
-interface ReviewSectionProps {
- question: string;
- answer: string;
-}
-
-const ReviewViewSection = ({ question, answer }: ReviewSectionProps) => {
- return (
-
-
-
-
- );
-};
-
-export default ReviewViewSection;
diff --git a/frontend/src/pages/DetailedReviewPage/components/ReviewViewSection/styles.ts b/frontend/src/pages/DetailedReviewPage/components/ReviewViewSection/styles.ts
deleted file mode 100644
index 7d22bffdf..000000000
--- a/frontend/src/pages/DetailedReviewPage/components/ReviewViewSection/styles.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import styled from '@emotion/styled';
-
-export const ReviewSectionContainer = styled.section`
- width: 100%;
-`;
diff --git a/frontend/src/pages/DetailedReviewPage/index.tsx b/frontend/src/pages/DetailedReviewPage/index.tsx
index 45860b638..afc92cabf 100644
--- a/frontend/src/pages/DetailedReviewPage/index.tsx
+++ b/frontend/src/pages/DetailedReviewPage/index.tsx
@@ -1,47 +1,32 @@
+import { useState, useEffect } from 'react';
+import { useParams } from 'react-router';
+
+import { ReviewComment } from '@/components';
import { DetailReviewData } from '@/types';
-import ReviewViewSection from './components/ReviewViewSection';
-import ReviewDescription from './components/ReviewDescription';
-import { useState, useEffect } from 'react';
import { getDetailedReviewApi } from '../../apis/review';
-const ANSWER =
- '림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. \n 바람은 여전히 그를 감싸며, 그의 마음 속 깊은 곳에 있는 꿈과 희망을 불러일으켰습니다.\n 림순은 미소 지으며 앞으로 나아갔습니다.림순의 바람은 그윽한 산들바람처럼 잔잔하게 흘러갔습니다. \n 눈부신 햇살이 그의 어깨를 감싸며, 푸른 하늘 아래 펼쳐진 들판을 바라보았습니다.\n 그의 마음은 자연의 아름다움 속에서 평온을 찾았고, 그 순간마다 삶의 소중함을 느꼈습니다.\n 그는 늘 그러한 순간들을 기억하며, 미래의 나날들을 기대했습니다. ';
+import KeywordSection from './components/KeywordSection';
+import ReviewDescription from './components/ReviewDescription';
+import ReviewSection from './components/ReviewSection';
+import * as S from './styles';
-const MOCK_DATA: DetailReviewData = {
- id: 123456,
- reviewer: {
- memberId: 123456,
- name: '올리',
- },
- createdAt: new Date('2024-07-16'),
- reviewerGroup: {
- groupId: 123456,
- name: 'review-me',
- },
- contents: [
- {
- question: '1. [공개] 동료의 개발 역량 향상을 위해 피드백을 남겨 주세요.',
- answer: ANSWER,
- },
- { question: '2. [공개] 동료의 소프트 스킬의 성장을 위해 피드백을 남겨 주세요.', answer: ANSWER },
- { question: '3. [비공개] 팀 동료로 근무한다면 같이 일 하고 싶은 개발자인가요?', answer: ANSWER },
- ],
- keywords: [{ id: 1, detail: '친절해요' }],
-};
+const COMMENT = 'VITE 쓰고 싶다.';
-const DetailedReviewPage = ({}) => {
- const [detailReview, setDetailReview] = useState(MOCK_DATA);
+const DetailedReviewPage = () => {
+ const { id: reviewId } = useParams();
+ const [detailedReview, setDetailedReview] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const fetch = async () => {
+ if (!reviewId) return;
try {
setIsLoading(true);
- getDetailedReviewApi({ reviewId: 4 }).then((result) => {
- setDetailReview(result);
- setErrorMessage('');
- });
+ const result = await getDetailedReviewApi({ reviewId: Number(reviewId) });
+
+ setDetailedReview(result);
+ setErrorMessage('');
} catch (error) {
if (error instanceof Error) {
setErrorMessage(error.message);
@@ -58,21 +43,22 @@ const DetailedReviewPage = ({}) => {
if (isLoading) return Loading...
;
if (errorMessage) return Error: {errorMessage}
;
+ if (!detailedReview) return Error: '상세보기 리뷰 데이터를 가져올 수 없어요.'
;
return (
- <>
+
console.log('click toggle ')}
/>
- {detailReview.contents.map((item, index) => (
-
+
+ {detailedReview.contents.map((item, index) => (
+
))}
- >
+
+
);
};
diff --git a/frontend/src/pages/DetailedReviewPage/styles.ts b/frontend/src/pages/DetailedReviewPage/styles.ts
index 2dcecf0f4..c9b901bb3 100644
--- a/frontend/src/pages/DetailedReviewPage/styles.ts
+++ b/frontend/src/pages/DetailedReviewPage/styles.ts
@@ -1,11 +1,5 @@
import styled from '@emotion/styled';
-export const DetailedReview = styled.div`
- box-sizing: border-box;
- width: 40rem;
- min-height: calc(100vh - 3rem);
- margin-top: 3rem;
- padding: 1rem;
-
- border: 1px solid black;
+export const DetailedReviewPage = styled.div`
+ width: ${({ theme }) => theme.formWidth};
`;
diff --git a/frontend/src/pages/ReviewWriting/styles.ts b/frontend/src/pages/ReviewWriting/styles.ts
index 3cd3dd0b8..28901a833 100644
--- a/frontend/src/pages/ReviewWriting/styles.ts
+++ b/frontend/src/pages/ReviewWriting/styles.ts
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
export const ReviewWritingPage = styled.form`
display: flex;
flex-direction: column;
- width: 80rem;
+ width: ${({ theme }) => theme.formWidth};
height: fit-content;
`;
diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts
index f97c14e0d..c3e1cd3db 100644
--- a/frontend/src/styles/theme.ts
+++ b/frontend/src/styles/theme.ts
@@ -3,6 +3,7 @@ import { CSSProperties } from 'react';
import { ThemeProperty } from '../types';
+export const formWidth = '86.7rem';
export const sidebarWidth: ThemeProperty = {
desktop: '25rem',
mobile: '100vw',
@@ -15,6 +16,13 @@ export const breakpoints: ThemeProperty = {
// NOTE: 1rem = 10px
export const fontSize: ThemeProperty = {
basic: '1.6rem',
+ medium: '2.4rem',
+ large: '3.2rem',
+ h2: '4.8rem',
+};
+
+export const borderRadius: ThemeProperty = {
+ basic: '0.8rem',
};
export const fontWeight: ThemeProperty = {
@@ -33,7 +41,7 @@ export const colors: ThemeProperty = {
white: '#FFFFFF',
lightGray: '#F1F2F4',
placeholder: '#D3D3D3',
- gray: '#8D8C8C',
+ gray: '#7F7F7F',
sidebarBackground: '#F5F5F5',
disabled: '#D8D8D8',
disabledText: '#7F7F7F',
@@ -50,6 +58,8 @@ const theme: Theme = {
colors,
breakpoints,
sidebarWidth,
+ borderRadius,
+ formWidth,
};
export default theme;
diff --git a/frontend/src/types/emotion.ts b/frontend/src/types/emotion.ts
index 39daed803..dc3b0ccd4 100644
--- a/frontend/src/types/emotion.ts
+++ b/frontend/src/types/emotion.ts
@@ -1,13 +1,15 @@
import '@emotion/react';
-import { colors, fontSize, fontWeight, zIndex, breakpoints, sidebarWidth } from '../styles/theme';
+import { colors, fontSize, fontWeight, zIndex, breakpoints, sidebarWidth, borderRadius } from '../styles/theme';
+// TODO: export 해서 사용하지 않다면 리팩토링
export type colorType = typeof colors;
export type zIndexType = typeof zIndex;
export type fontSizeType = typeof fontSize;
export type fontWeightType = typeof fontWeight;
export type breakpoints = typeof breakpoints;
export type sidebarWidth = typeof sidebarWidth;
+export type borderRadius = typeof borderRadius;
type ThemeType = {
fontSize: fontSizeType;
@@ -16,6 +18,8 @@ type ThemeType = {
zIndex: zIndexType;
breakpoints: breakpoints;
sidebarWidth: sidebarWidth;
+ borderRadius: borderRadius;
+ formWidth: string;
};
declare module '@emotion/react' {
diff --git a/frontend/src/types/review.ts b/frontend/src/types/review.ts
index 97f531d1d..fc52eb147 100644
--- a/frontend/src/types/review.ts
+++ b/frontend/src/types/review.ts
@@ -2,20 +2,30 @@ export interface ReviewItem {
question: string;
answer: string;
}
+export interface Keyword {
+ id: number;
+ detail: string;
+}
+export interface DetailReviewContent {
+ id: number;
+ question: string;
+ answer: string;
+}
export interface DetailReviewData {
id: number;
createdAt: Date;
- reviewer: {
- memberId: number;
- name: string;
- };
reviewerGroup: {
- groupId: number;
+ id: number;
name: string;
+ deadline: Date;
+ reviewee: {
+ id: number;
+ name: string;
+ };
};
- contents: ReviewItem[];
- keywords: { id: number; detail: string }[];
+ contents: DetailReviewContent[];
+ keywords: Keyword[];
}
// api
diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts
new file mode 100644
index 000000000..1dbd129ee
--- /dev/null
+++ b/frontend/src/utils/date.ts
@@ -0,0 +1,11 @@
+export const formatDate = (date: Date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+
+ return {
+ year,
+ month,
+ day,
+ };
+};
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts
new file mode 100644
index 000000000..05b562fe2
--- /dev/null
+++ b/frontend/src/utils/index.ts
@@ -0,0 +1 @@
+export * from './date';
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 4ec24d839..4bb8482b4 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -16,7 +16,7 @@
"@/*": ["src/*"]
},
"jsxImportSource": "@emotion/react",
- "types": ["jest", "@testing-library/jest-dom"]
+ "types": ["jest", "@testing-library/jest-dom", "node"]
},
"include": ["src", "**/*.tsx", "types.d.ts"],
"exclude": ["node_modules", "dist"]
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 6a71c983b..ee6f4477b 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1371,20 +1371,20 @@
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@inquirer/confirm@^3.0.0":
- version "3.1.15"
- resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-3.1.15.tgz#50fad3e9e9af1ddc7b661ac044cc04a689904760"
- integrity sha512-CiLGi3JmKGEsia5kYJN62yG/njHydbYIkzSBril7tCaKbsnIqxa2h/QiON9NjfwiKck/2siosz4h7lVhLFocMQ==
+ version "3.1.17"
+ resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-3.1.17.tgz#adca3b0f35e2d2ace53f652a92f987aaccb8482a"
+ integrity sha512-qCpt/AABzPynz8tr69VDvhcjwmzAryipWXtW8Vi6m651da4H/d0Bdn55LkxXD7Rp2gfgxvxzTdb66AhIA8gzBA==
dependencies:
- "@inquirer/core" "^9.0.3"
- "@inquirer/type" "^1.5.0"
+ "@inquirer/core" "^9.0.5"
+ "@inquirer/type" "^1.5.1"
-"@inquirer/core@^9.0.3":
- version "9.0.3"
- resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-9.0.3.tgz#40564a501f77410752b0a5dda652d6340e30dfa1"
- integrity sha512-p2BRZv/vMmpwlU4ZR966vKQzGVCi4VhLjVofwnFLziTQia541T7i1Ar8/LPh+LzjkXzocme+g5Io6MRtzlCcNA==
+"@inquirer/core@^9.0.5":
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-9.0.5.tgz#b5e14d80e87419231981f48fa86f63d15cb8805b"
+ integrity sha512-QWG41I7vn62O9stYKg/juKXt1PEbr/4ZZCPb4KgXDQGwgA9M5NBTQ7FnOvT1ridbxkm/wTxLCNraUs7y47pIRQ==
dependencies:
- "@inquirer/figures" "^1.0.4"
- "@inquirer/type" "^1.5.0"
+ "@inquirer/figures" "^1.0.5"
+ "@inquirer/type" "^1.5.1"
"@types/mute-stream" "^0.0.4"
"@types/node" "^20.14.11"
"@types/wrap-ansi" "^3.0.0"
@@ -1397,15 +1397,15 @@
wrap-ansi "^6.2.0"
yoctocolors-cjs "^2.1.2"
-"@inquirer/figures@^1.0.4":
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.4.tgz#a54dab6e205636a881ece0f1017efff6d6174d6e"
- integrity sha512-R7Gsg6elpuqdn55fBH2y9oYzrU/yKrSmIsDX4ROT51vohrECFzTf2zw9BfUbOW8xjfmM2QbVoVYdTwhrtEKWSQ==
+"@inquirer/figures@^1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.5.tgz#57f9a996d64d3e3345d2a3ca04d36912e94f8790"
+ integrity sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==
-"@inquirer/type@^1.5.0":
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-1.5.0.tgz#0890e6286281b3f118632e6f7c47c0ccb9b29ee3"
- integrity sha512-L/UdayX9Z1lLN+itoTKqJ/X4DX5DaWu2Sruwt4XgZzMNv32x4qllbzMX4MbJlz0yxAQtU19UvABGOjmdq1u3qA==
+"@inquirer/type@^1.5.1":
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-1.5.1.tgz#cdd36732e38ea5d2b1a4336aada65ebe7d2765e0"
+ integrity sha512-m3YgGQlKNS0BM+8AFiJkCsTqHEFCWn6s/Rqye3mYwvqY6LdfUv12eSwbsgNzrYyrLXiy7IrrjDLPysaSBwEfhw==
dependencies:
mute-stream "^1.0.0"
@@ -2079,9 +2079,9 @@
undici-types "~5.26.4"
"@types/node@^20.14.11":
- version "20.14.11"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b"
- integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==
+ version "20.14.12"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.12.tgz#129d7c3a822cb49fc7ff661235f19cfefd422b49"
+ integrity sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==
dependencies:
undici-types "~5.26.4"
@@ -5326,6 +5326,11 @@ jest-environment-node@^29.7.0:
jest-mock "^29.7.0"
jest-util "^29.7.0"
+jest-fixed-jsdom@^0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/jest-fixed-jsdom/-/jest-fixed-jsdom-0.0.2.tgz#2a15d2db64cf9276afd162c451ae727b89a8811e"
+ integrity sha512-rQ7tcI7Sz3XGxEzk7Ic/4n3y83O0cGnSK3lFU3lqHhYaxp/00yoolBBOTnY6bLKkI1XzO4EMgyHYZ1IBRpEesw==
+
jest-get-type@^29.6.3:
version "29.6.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
@@ -5950,10 +5955,10 @@ ms@2.1.3, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-msw@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/msw/-/msw-2.3.1.tgz#bfc73e256ffc2c74ec4381b604abb258df35f32b"
- integrity sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==
+msw@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/msw/-/msw-2.3.2.tgz#ea4f45b51f833fa3b2215c4093bcda28dbe25a83"
+ integrity sha512-vDn6d6a50vxPE+HnaKQfpmZ4SVXlOjF97yD5FJcUT3v2/uZ65qvTYNL25yOmnrfCNWZ4wtAS7EbtXxygMug2Tw==
dependencies:
"@bundled-es-modules/cookie" "^2.0.0"
"@bundled-es-modules/statuses" "^1.0.1"
@@ -7546,9 +7551,9 @@ type-fest@^0.21.3:
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
type-fest@^4.9.0:
- version "4.22.0"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.22.0.tgz#da4fc735652e17ef693d2b8dc4f65d93f5fd4ef9"
- integrity sha512-hxMO1k4ip1uTVGgPbs1hVpYyhz2P91A6tQyH2H9POx3U6T3MdhIcfY8L2hRu/LRmzPFdfduOS0RIDjFlP2urPw==
+ version "4.23.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.23.0.tgz#8196561a6b835175473be744f3e41e2dece1496b"
+ integrity sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==
type-is@~1.6.18:
version "1.6.18"