Skip to content

Commit

Permalink
[FE] MSW를 mock server로 셋팅 (#95)
Browse files Browse the repository at this point in the history
* fix: stylelint를 통한 css 속성 정렬 기능 오류 수정

- stylelint 버전16과 충돌되는 플러그인 삭제 : stylelint-config-prettier, stylelint-prettier
- css 정렬에 필요하지 않은 플러그인 삭제 : stylelint-config-standard, stylelint-config-styled-componented, stylelint-webpack-plugin
- 추가로 설치한 플러그인: postcss-syntax, @stylelint/postcss-css-in-js
- stylelint 적용 script 추가
- .stylelintrc.json 수정 : css 관련 rule 설정

* refactor: stylelint 적용에 따른 css 속성 정렬

* fix : 절대 경로 사용 시 오류 수정

오류 : eslintimport/no-unresolved

* chore: eslintrc.cjs 에서 불필요한 코드 삭제

 node 환경 setting 삭제

* style: eslint 적용에 따른 리뷰 상세페이지 import 순서 정리

* refactor: formatDate를 utils/date 파일로 이동

* design: theme에 colors,, fontSize 변경 및 borderRadius 추가

* feat: MultilineTextViewer 컴포넌트 생성

- 개행이 포함된 string에 개행을 적용해서 보여주는 컴포넌트

* feat: 깃허브 저장소 이미지 컴포넌트 생성

* feat: 리뷰와 관련된 날짜 UI 컴포넌트 생성

* featr: LockButton 삭제 LockToggle 추가

* refactor: 피그마 디자인 변경에 따른 ReviewDescription 변경

* feat: ReviewComment 컴포넌트 생성

* refactor: ReviewViewSection -> ReviewSection 으로 변경 및 리팩토링

- 불필요한  컴포넌트 삭제 : RevieAnswer , ReviewQuestion

* refactor: DetailedReviewPage 리팩토링

- 목데이터 변경
-  추가 및 변경된 컴포넌트를 사용해 리뷰 상세페이지 컴포넌트(DetailedReviewPage) 리팩토링
- DetailedReviewPage 폴더의 styles.ts 삭제

* refactor: review에 대한 타입 변경

* design : ReviewDate의 클론 스타일 적용

* feat: KeywordSection 컴포넌트 생성

- 리뷰 상세 페이지 키워드 부분 컴포넌트 생성

* feat: ReviewSectionHeader 컴포넌트 생성 및 적용

- 리뷰 상세보기에서 반복되는 질문,키워드 헤더부분을 컴포넌트로 분리

* design : 리뷰 상세페이지에 width 변경

* refactor: DetailedReview의 목데이터 변경 및 리팩토링

- 타입 변경에 따른 목 데이터 변경
- KeywordSection 적용

* design : formWidth를 theme에 추가 및 리뷰 작성/리뷰 상세 페이지에 적용

* fix: Layout에서 가로 스크롤 생기는 오류 수정

- 100vw는 스크롤을 포함한 뷰포트 너비라서 100%으로 수정

* feat: 리뷰 상페이지 router에 라우터 파라미터 적용 및 관련 설정 변경

- 데모데이를 위해 현재 데이터베이스에 있는 리뷰 상세페이지 id를 sidebar의 리뷰 상세페이지 메뉴 link에 적용
- 리뷰 상세페이지(DetailedReviewPage)의 api 핸들러 수정

* docs: 변수명 변경 (isLock -> isPublic)

* refactor: 깃헙 저장소 로고 주소 변수명 변경

- projectImgSrc -> thumbnailUrl

* ci: msw 관련 패키지 설치

* ci: msw 관련 설정파일 추가

- 브라우저 환경, node 환경에서 msw로 목서버 사용할 수 있도록 관련 파일 추가

* feat: mock 핸들러 추가 및 상세 리뷰 페이지 목 데이터 추가

* feat:  root에서 목서버 사용할 수 있도록함

* refactor: endpoint 수정

- env 에서 서버 주소 끝에 슬래시 넣는 것으로 통일

* feat: 상세 리뷰 페이지(detailedReviewPage)에 목서버 연결 및 관련 코드 수정

- 상태명 변경: detailReview -> detailedReview
- detailedReview 타입에 null 추가 및 그에 따른 오류 핸들링 추가
- deadline에 string 타입으로 response로 전달되어서 new Date로 감싸서 props로 전달

* docs: indexhtml의 title 변경

* style: apis/review.ts 의 import 관련 eslint rule 적용에 따른 수정

* fix: ts에서 process 읽지 못하는 오류 수정

* fix: webpack dev server script 복원
  • Loading branch information
BadaHertz52 authored Jul 25, 2024
1 parent 4b2be22 commit e05c164
Show file tree
Hide file tree
Showing 48 changed files with 866 additions and 262 deletions.
8 changes: 7 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -62,5 +63,10 @@
"webpack": "^5.92.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
},
"msw": {
"workerDirectory": [
"public"
]
}
}
2 changes: 1 addition & 1 deletion frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link href="https://hangeul.pstatic.net/hangeul_static/css/nanum-gothic.css" rel="stylesheet">

<title>Document</title>
<title>reveiw me</title>
</head>
<body>
<div id="root"></div>
Expand Down
281 changes: 281 additions & 0 deletions frontend/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 4 additions & 4 deletions frontend/src/apis/endpoints.ts
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 20 additions & 0 deletions frontend/src/components/common/MultilineTextViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

interface MultilineTextViewerProps {
text: string;
}

const MultilineTextViewer = ({ text }: MultilineTextViewerProps) => {
return (
<>
{text.split('\n').map((line, index) => (
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
))}
</>
);
};

export default MultilineTextViewer;
16 changes: 16 additions & 0 deletions frontend/src/components/common/ProjectImg/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <S.Img src={src} alt={alt} $size={$size} />;
};

export default ProjectImg;
11 changes: 11 additions & 0 deletions frontend/src/components/common/ProjectImg/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from '@emotion/styled';

interface ImgProps {
$size: string;
}

export const Img = styled.img<ImgProps>`
width: ${(props) => props.$size};
height: ${(props) => props.$size};
border-radius: ${({ theme }) => theme.borderRadius.basic};
`;
11 changes: 11 additions & 0 deletions frontend/src/components/common/ReviewComment/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as S from './styles';

interface ReviewCommentProps {
comment: string;
}

const ReviewComments = ({ comment }: ReviewCommentProps) => {
return <S.ReviewComment>{comment}</S.ReviewComment>;
};

export default ReviewComments;
Loading

0 comments on commit e05c164

Please sign in to comment.