-
Notifications
You must be signed in to change notification settings - Fork 2
1201 (금) 팀회고 (4주차)
- 프로젝트 현황 공유는 **
팀 단위
**로 모여, 한 주 동안 팀이 기술적으로 도전한 내용이나 문제 해결 과정, 진행 현황, 앞으로 계획 등 한 주 간 팀의 현황을 발표하고 피드백하는 시간입니다. - 피어세션은 분야별 캠퍼와
**개인 단위**
로 만나, 한 주 동안 했던 시행착오나 기술적 도전, 학습한 것과 잘한 점 등을 공유하고 보다 깊게 얘기 나누는 시간입니다.
- **
개발 완료 기능과 구현 과정에서의 기술적 경험
**에 대한 발표를 진행합니다. - **
배포 링크 혹은 동작 데모
**를 반드시 포함해주세요.
프로젝트 진행 상황, 주요 기능, 협업 중 겪은 어려움, 해결 방안 등을 문서로 정리합니다. [프로젝트 현황 공유]에서 받은 질문과 피드백을 정리하여 추가해두길 권장합니다.
- 상시로 진행한 팀 회고 결과가 있다면 해당 기록으로 갈음할 수 있습니다.
-
Axios Interceptor 추가
- then 또는 catch로 처리되기 전에 요청 / 응답을 가로챌 수 있음
- 에러 핸들링, 로딩 처리, 헤더에 Authorization 미리 넣기 등에 유용함
import axios from 'axios'; import { useEffect } from 'react'; const instance = axios.create({ baseURL: 'https://www.별글.site/api/', }); interface Props { children: JSX.Element; } function AxiosInterceptor({ children }: Props) { useEffect(() => { const responseInterceptor = instance.interceptors.response.use( (response) => { return response; }, (error) => { console.error(error.response.data); return Promise.reject(error); }, ); const requestInterceptor = instance.interceptors.request.use( (config) => { return config; }, (error) => { console.error(error.response.data); return Promise.reject(error); }, ); return () => { instance.interceptors.request.eject(requestInterceptor); instance.interceptors.response.eject(responseInterceptor); }; }, []); return children; } export default instance; export { AxiosInterceptor };
-
홈화면 하단바, 상단바 UI 구현
-
글 조회 / 글 작성 모달 구현 및 API 연동
- 이미지 슬라이드 구현
- 글 삭제 구현
-
회원가입 구현 및 API 연동
- 회원가입 모달, 닉네임 모달 구현
- validation 처리
-
AlertDialog 컴포넌트 구현
-
로그아웃 구현
-
라우팅 방식 변경 (React Router Dom 6.4)
export const router = createBrowserRouter( createRoutesFromElements( <> <Route path="/" element={<Landing />}> <Route index element={<LogoAndStart />} /> <Route path="login" element={<LoginModal />} /> <Route path="signup" element={<SignUpModal />} /> <Route path="nickname" element={<NickNameSetModal />} /> </Route> <Route path="/home" element={<Home />}> <Route path=":postId"> <Route path="detail" element={<PostModal />} /> </Route> <Route path="writing" element={<WritingModal />} /> </Route> </>, ), );
export default function Landing() { return ( <div> <Outlet /> <LandingScreen mousePosition={mouse} /> </div> ); }
-
은하 성능 최적화
- instance mesh를 통해 draw call을 줄임
-files-secure.s3.us-west-2.amazonaws.com/e750c264-eee6-4c52-bc91-67ad175143f2/fbdb90e3-2437-4cbe-b199-70117e36e9f8/Untitled.png)
-
페이지 이동 화면 (Warp Screen) 화면 적용
-
로그 스파이럴을 적용한 은하 생성 - 아직 미적용
- 좀 더 직관적인 은하 형태 커스텀을 위함
- https://github.com/boostcampwm2023/web16-B1G1/pull/186
-
CORS 시러
- 로그
- [Deploy] sharp package 버전 명시로 리눅스 환경에서 실행 안되는 문제 해결 BE 🐛 BugFix 🚀 Deploy #205 by SongJSeop was merged 1 hour ago
- [BE] Log 컬러 지정 및 catchError 추가 BE ⚡️ Enhancement ♻️ Refactor #199 by SongJSeop was merged 8 hours ago
- [BE] LogInterceptor, TransactionInterceptor 구현 BE ✨ Feature #198 by SongJSeop was merged 9 hours ago
- [BE] PATCH /post/:id 트랜잭션 개선, POST /post 트랜잭션 적용 BE ⚡️ Enhancement #195 by qkrwogk was merged 9 hours ago
- [BE] 사용자 공유 상태 변경 API 및 공유 링크 조회 API 추가 BE ✨ Feature #194 by SongJSeop was merged 14 hours ago
- [BE] PATCH & DELETE /post/:id 개선 및 트랜잭션 적용, PATCH /star/:id 구현 BE ⚡️ Enhancement ✨ Feature #190 by qkrwogk was merged yesterday
- [BE] SignIn 에러 상태 코드 수정 BE ♻️ Refactor #189 by SongJSeop was merged yesterday
- [BE] 닉네임 검색 API 구현 BE ✨ Feature #188 by SongJSeop was merged yesterday
- [BE] CORS 트러블 슈팅, Star/Board Swagger 파일 분리, Board/Post 용어 통일 BE 🐛 BugFix ♻️ Refactor #184 by qkrwogk was merged 2 days ago
- [BE] GET /star, GET /post/:id 쿼리 최적화 (+ docker 컨테이너 재실행 설정) BE 🐛 BugFix ⚡️ Enhancement #183 by qkrwogk was merged 2 days ago
- [BE] Auth E2E 테스트 코드 수정 BE ✅ Test #182 by SongJSeop was merged 2 days ago
- [BE] Sharp 플랫폼 종속성 해결, GET /stars, GET /post/:id 프론트 요청에 맞게 새로 구현 BE 🐛 BugFix ✨ Feature #180 by qkrwogk was merged 3 days ago
- [BE] Auth Controller의 Swagger 부분 리팩토링 BE ✨ Feature ♻️ Refactor #179 by SongJSeop was merged 3 days ago
- [BE] 글 생성 시 별 위치 및 스타일 정보 MongoDB에 저장 BE ✨ Feature #170 by qkrwogk was merged 4 days ago
- [BE] 이미지 리사이징하여 업로드 BE ⚡️ Enhancement ✨ Feature #168 by qkrwogk was merged 5 days ago
- [BE] 구글 로그인 구현 BE ✨ Feature #165 by SongJSeop was merged 5 days ago
- [BE] 네이버 로그인 구현 BE ✨ Feature ♻️ Refactor #164 by SongJSeop was merged 5 days ago
-
OAuth2.0 로그인 구현 : 깃헙, 네이버, 구글로 로그인 구현
-
아직 클라이언트에서 개발이 안됐음
-
서비스별로 switch-case문으로 리팩토링
-
예시
function getOAuthAccessTokenRequestData( service: string, authorizedCode: string, state?: string, ) { let urlForAccessToken: string; let requestData: any; switch (service) { case 'github': urlForAccessToken = 'https://github.com/login/oauth/access_token'; requestData = { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ client_id: process.env.OAUTH_GITHUB_CLIENT_ID, client_secret: process.env.OAUTH_GITHUB_CLIENT_SECRETS, code: authorizedCode, }), }; break; case 'naver': urlForAccessToken = 'https://nid.naver.com/oauth2.0/token'; requestData = { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: process.env.OAUTH_NAVER_CLIENT_ID, client_secret: process.env.OAUTH_NAVER_CLIENT_SECRETS, code: authorizedCode, state, }), }; break; case 'google': urlForAccessToken = 'https://oauth2.googleapis.com/token'; requestData = { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: process.env.OAUTH_GOOGLE_CLIENT_ID, client_secret: process.env.OAUTH_GOOGLE_CLIENT_SECRETS, code: authorizedCode, redirect_uri: process.env.OAUTH_GOOGLE_REDIRECT_URI, }), }; break; default: throw new NotFoundException('존재하지 않는 서비스입니다.'); } return [urlForAccessToken, requestData]; }
-
-
이미지 업로드 : Sharp 리사이징 & 컨버팅, NCP 업로드 & 링크
-
리소스를 절약하기 위해 리사이징 & 통일된 파일포맷으로 컨버팅 후 업로드
-
예시
const resized_buffer = await sharp(buffer) .resize(500, 500, { fit: 'cover' }) .toFormat('png', { quality: 100 }) .toBuffer();
before (image/jpg, 377KB, 2730x1636)
after (image/png, 69KB, 500x500)
-
-
전문 검색 : 닉네임 검색 API 구현
-
MySQL의 닉네임 컬럼에 FULLTEXT 인덱싱
-
MySQL MATCH, AGAINST 연산자로 전문검색
-
예시
코드
async searchUser(nickname: string): Promise<User[]> { const users: User[] = await this.userRepository .createQueryBuilder('user') .select(['user.id', 'user.nickname']) // MATCH (조회 컬럼) AGAINST (닉네임으로 시작하는 IN BOOLEAN MODE) .where(`MATCH (user.nickname) AGAINST (:nickname IN BOOLEAN MODE)`, { nickname: nickname + '*', }) .getMany(); return users; }
테스트용으로 데이터베이스에 저장한 유저 데이터들
Postman 테스트 (”테스트”라는 이름으로 조회)
localhost:3000/auth/search?nickname=테스트
결과가 잘 출력된 모습
-
-
공유 링크 : 사용자 공유 상태 변경 API 및 공유 링크 조회 API 추가
-
예시
일단 로그인하여 JWT를 발급받아야 진행됨
PATCH http://localhost:3000/auth/status
body에 들어갈 정보들
- 미리 지정된 status값이 아닌 경우 ('public', 'only_link', 'private'이 아닌 경우)
{ "status": "test" }
400 BadRequest 에러 발생
- 현재 상태인 값으로 요청을 보냈을 경우
{ "status": "private" }
400 BadRequest 에러 발생
- 잘 변경되는 경우
{ "status": "public" }
유저 정보 반환
GET http://localhost:3000/auth/sharelink
- 만약 private 사용자의 공유링크를 조회했을 경우
400 BadRequest 에러 발생
- 잘 조회되는 경우
링크 정보 반환
-
-
Swagger 리팩토링 : 커스텀 데코레이터 구현
-
스웨거 데코레이터들이 너무 더러워서 묶어줌
-
예시
before
... @UseGuards(CookieAuthGuard) @UseInterceptors(FilesInterceptor('file', 3)) @UsePipes(ValidationPipe) @ApiOperation({ summary: '게시글 작성', description: '게시글을 작성합니다.' }) @ApiCreatedResponse({ status: 201, description: '게시글 작성 성공' }) @ApiBadRequestResponse({ status: 400, description: '잘못된 요청으로 게시글 작성 실패', }) @CreateBoardSwaggerDecorator() createBoard( @Body() createBoardDto: CreateBoardDto, @GetUser() userData: UserDataDto, ...
after
// sign-in-swagger.decorator.ts import { applyDecorators } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiUnauthorizedResponse, } from '@nestjs/swagger'; const apiOperation = { summary: '로그인', description: 'username, password를 받아 로그인을 진행합니다.', }; const apiOkResponse = { status: 200, description: '로그인이 성공해 쿠키에 토큰이 저장됨', }; const apiUnauthorizedResponse = { status: 401, description: '잘못된 유저 정보로 로그인 실패', }; export const SignInSwaggerDecorator = () => { return applyDecorators( ApiOperation(apiOperation), ApiOkResponse(apiOkResponse), ApiUnauthorizedResponse(apiUnauthorizedResponse), ); };
@Post('signin') @HttpCode(200) @SignInSwaggerDecorator() async signIn( @Body() signInUserDto: SignInUserDto, @Res({ passthrough: true }) res: Response, ) { ... }
-
-
쿼리 최적화 : GET /star, GET /post/:id
-
예시
import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { configDotenv } from 'dotenv'; configDotenv(); export const typeOrmConfig: TypeOrmModuleOptions = { ... logging: true, };
SELECT `Board`.`id` AS `Board_id`, `Board`.`title` AS `Board_title`, `Board`.`content` AS `Board_content`, `Board`.`created_at` AS `Board_created_at`, `Board`.`updated_at` AS `Board_updated_at`, `Board`.`like_cnt` AS `Board_like_cnt`, `Board`.`star` AS `Board_star`, `Board`.`userId` AS `Board_userId`, `Board__Board_user`.`id` AS `Board__Board_user_id`, `Board__Board_user`.`username` AS `Board__Board_user_username`, `Board__Board_user`.`password` AS `Board__Board_user_password`, `Board__Board_user`.`nickname` AS `Board__Board_user_nickname`, `Board__Board_user`.`created_at` AS `Board__Board_user_created_at`, `Board__likes`.`id` AS `Board__likes_id`, `Board__likes`.`username` AS `Board__likes_username`, `Board__likes`.`password` AS `Board__likes_password`, `Board__likes`.`nickname` AS `Board__likes_nickname`, `Board__likes`.`created_at` AS `Board__likes_created_at`, `Board__images`.`id` AS `Board__images_id`, `Board__images`.`mimetype` AS `Board__images_mimetype`, `Board__images`.`filename` AS `Board__images_filename`, `Board__images`.`size` AS `Board__images_size`, `Board__images`.`created_at` AS `Board__images_created_at`, `Board__images`.`boardId` AS `Board__images_boardId` FROM `board` `Board` LEFT JOIN `user` `Board__Board_user` ON `Board__Board_user`.`id`=`Board`.`userId` LEFT JOIN `board_likes_user` `Board_Board__likes` ON `Board_Board__likes`.`boardId`=`Board`.`id` LEFT JOIN `user` `Board__likes` ON `Board__likes`.`id`=`Board_Board__likes`.`userId` LEFT JOIN `image` `Board__images` ON `Board__images`.`boardId`=`Board`.`id` WHERE (`Board__Board_user`.`nickname` = ?); -- PARAMETERS: ["test2"]
SELECT `board`.`id` AS `board_id`, `board`.`title` AS `board_title`, `board`.`star` AS `board_star` FROM `board` `Board`, `board` `board` LEFT JOIN `user` `user` ON `user`.`id`=`board`.`userId` WHERE `user`.`nickname` = ?; -- PARAMETERS: ["test2"]
const boards = await this.boardRepository .createQueryBuilder() .select('board.id as id') .addSelect('board.title as title') .addSelect('board.star as star') .from(Board, 'board') .leftJoinAndSelect('board.user', 'user') .where('user.nickname = :nickname', { nickname: author }) .getMany();
-
-
트랜잭션 제어 : POST /post, PATCH & DELETE /post/:id, TransactionInterceptor 구현
-
예시
async createBoard( createBoardDto: CreateBoardDto, userData: UserDataDto, files: Express.Multer.File[], ): Promise<Board> { const { title, content, star } = createBoardDto; // transaction 생성하여 board, image, star, like 레코드 동시에 생성 const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); // const user = await this.userRepository.findOneBy({ id: userData.userId }); const user = await queryRunner.manager.findOneBy(User, { id: userData.userId, }); // transaction 시작 await queryRunner.startTransaction(); try { const images: Image[] = []; for (const file of files) { // Object Storage에 업로드 const imageInfo = await this.uploadFile(file); // 이미지 리포지토리에 저장 const image = queryRunner.manager.create(Image, { ...imageInfo, }); const createdImage = await queryRunner.manager.save(image); images.push(createdImage); } // 별 스타일이 존재하면 MongoDB에 저장 let star_id: string; if (star) { const starDoc = new this.starModel({ ...JSON.parse(star), }); await starDoc.save(); star_id = starDoc._id.toString(); } const board = queryRunner.manager.create(Board, { title, content: encryptAes(content), // AES 암호화하여 저장 user, images, star: star_id, }); // const createdBoard: Board = await this.boardRepository.save(board); const createdBoard: Board = await queryRunner.manager.save(board); // commit transacton await queryRunner.commitTransaction(); createdBoard.user.password = undefined; // password 제거하여 반환 return createdBoard; } catch (error) { Logger.error(error); await queryRunner.rollbackTransaction(); throw new InternalServerErrorException('Failed to update board'); } finally { await queryRunner.release(); } }
-
-
로거 구현 : LogInterceptor, Log 컬러 지정 및 catchError 추가, TransactionInterceptor
-
인터셉터는 요청과 응답 둘 다 건들 수 있음
-
요청할 때 로그를 찍고, 응답시 로그를 찍음 → 그 사이 시간도 표시
-
TransactionInterceptor: 트랜잭션이 필요한 로직에서는 일단 항상 쿼리러너를 생성하고, 마지막에 커밋 또는 롤백 후 릴리즈
- 요청이 들어오면 쿼리러너를 생성하고 로그를 찍음
- 응답이 나갈 때 중간에 에러가 발생해서 에러 응답이면 트랜잭션을 롤백하고 릴리즈 후 로그, 올바른 응답이면 트랜잭션을 커밋하고 릴리즈 후 로그
-
예시
요청은 REQ 라는 이름으로 들어오고 응답은 RES라는 이름으로 들어옴 → RES에 응답에 걸린 시간 표시
AuthGuard가 필요한 요청이면 유저 정보도 표시
Transaction start, commit, rollback에 대한 로그도 출력
try catch 없앰
before
// board.service.ts constructor( ... @InjectDataSource() private readonly dataSource: DataSource, ) {} ... async deleteBoard(id: number, userData: UserDataDto): Promise<void> { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); const board: Board = await queryRunner.manager.findOneBy(Board, { id }); if (!board) { throw new NotFoundException(`Not found board with id: ${id}`); } if (board.user.id !== userData.userId) { throw new BadRequestException('You are not the author of this post'); } await queryRunner.startTransaction(); try { for (const image of board.images) { await queryRunner.manager.delete(Image, { id: image.id }); await this.deleteFile(image.filename); } if (board.star) { await this.starModel.deleteOne({ _id: board.star }); } const result = await queryRunner.manager.delete(Board, { id }); await queryRunner.commitTransaction(); } catch (err) { Logger.error(err); await queryRunner.rollbackTransaction(); } finally { await queryRunner.release(); } }
after → 쿼리러너 생성 및 커밋, 롤백, 릴리즈 단계 없앰 (try catch 없앰), 생성자 DataSouce도 Interceptor에 빼서 없앴음 → 컨트롤러에서 쿼리러너를 서비스에 넘겨줌
async deleteBoard( id: number, userData: UserDataDto, queryRunner: QueryRunner, ): Promise<DeleteResult> { const board: Board = await queryRunner.manager.findOneBy(Board, { id }); if (!board) { throw new NotFoundException(`Not found board with id: ${id}`); } if (board.user.id !== userData.userId) { throw new BadRequestException('You are not the author of this post'); } for (const image of board.images) { await queryRunner.manager.delete(Image, { id: image.id }); await this.deleteFile(image.filename); } if (board.star) { await this.starModel.deleteOne({ _id: board.star }); } const result = await queryRunner.manager.delete(Board, { id }); return result; }
-
-
배포 트러블 슈팅 : Sharp 플랫폼 종속성 문제 해결, CORS 트러블 슈팅
- 내용
-
Sharp 플랫폼 종속성 문제
# 플랫폼을 명시한 yarn 패키지 설치 방법 SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm_config_arch=x64 npm_config_platform=linux yarn workspace server add sharp@0.32.1
-
CORS 트러블 슈팅
- 배포된 테스트용 BE서버 : https://www.별글.site
- 로컬 테스트용 FE서버 : http://localhost:5173/
- origin이 같지 않아 브라우저에서 cookie 세팅을 차단하는 문제
- BE에서 https를 제공해야 함
- origin은 *이 아닌 정확한 도메인명 명시해야 함
- credentials를 포함하도록 FE, BE 모두 설정해야 함
- set cookie시
sameSite:none
,secure:true
옵션을 설정해야 함
// cors 허용 app.enableCors({ // Access-Control-Allow-Origin : 도메인 명시 (* 안됨) origin: ['https://www.xn--bj0b03z.site', 'http://localhost:5173'], // Access-Control-Allow-Credentials : true credentials: true, methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], }); ... // 쿠키 세팅 시 res.clearCookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME, { path: '/', httpOnly: true, sameSite: 'none', secure: true, }); res.clearCookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME, { path: '/', httpOnly: true, sameSite: 'none', secure: true, }); ... // FE에서 AJAX 요청 시 axios.defaults.withCredentials = true;
-
- 내용
-
API 인터페이스 개선 : SignIn 에러 상태 코드 수정 BE, GET /stars & PATCH /star/:id & GET /post/:id 재구현, PATCH & DELETE /post/:id 개선
- 계획을 실현하는 능력
- 실현을 못한 팀원도 있습니다 안타깝지만
- 생각해보니 하나도 못함 흐흑
- 히히잉이잉이잉
- 멘토링 일지를 열심히 작성함
- 프론트와 백엔드의 원활한 소통
- 우린 진짜 개쩔어
- 에러가 발생하면 모두 해결에 열심히 참여함
- 결과물이 예쁘다⭐️
- 독감에 걸린 사람이 나왔다. 건강관리를 잘하자
- 내 코딩활동에 자꾸 훈수두는 팀원이 있다 기죽는다 난열심히햇는데 흐힉잉
- 금쪽이 팀원이 있다 걱정이 너무 많고 완벽에 너무 집착함
- 프론트 문서화를 너무 못함 ㅜ
- 건강관리를 잘하도록 하십쇼. 아시겠냐구요 아시겟냐구ㅜ요 네 알겠습니다
- 모이지 말자
- 기능 쳐낼껀 쳐내자
- 남은 기간 기능/코드의 완성도를 높이고 문서화를 조금이라도 해보장구리
© 2023 debussysanjang
- 🐙 [가은] Three.js와의 설레는 첫만남
- 🐙 [가은] JS로 자전과 공전을 구현할 수 있다고?
- ⚽️ [준섭] NestJS 강의 정리본
- 🐧 [동민] R3F Material 간단 정리
- 👾 [재하] 만들면서 배우는 NestJS 기초
- 👾 [재하] GitHub Actions을 이용한 자동 배포
- ⚽️ [준섭] 테스트 코드 작성 이유
- ⚽️ [준섭] TypeScript의 type? interface?
- 🐙 [가은] 우리 팀이 Zustand를 쓰는 이유
- 👾 [재하] NestJS, TDD로 개발하기
- 👾 [재하] AWS와 NCP의 주요 서비스
- 🐰 [백범] Emotion 선택시 고려사항
- 🐧 [동민] Yarn berry로 모노레포 구성하기
- 🐧 [동민] Vite, 왜 쓰는거지?
- ⚽️ [준섭] 동시성 제어
- 👾 [재하] NestJS에 Swagger 적용하기
- 🐙 [가은] 너와의 추억을 우주의 별로 띄울게
- 🐧 [동민] React로 멋진 3D 은하 만들기(feat. R3F)
- ⚽️ [준섭] NGINX 설정
- 👾 [재하] Transaction (트랜잭션)
- 👾 [재하] SSH 보안: Key Forwarding, Tunneling, 포트 변경
- ⚽️ [준섭] MySQL의 검색 - LIKE, FULLTEXT SEARCH(전문검색)
- 👾 [재하] Kubernetes 기초(minikube), docker image 최적화(멀티스테이징)
- 👾 [재하] NestJS, 유닛 테스트 각종 mocking, e2e 테스트 폼데이터 및 파일첨부
- 2주차(화) - git, monorepo, yarn berry, TDD
- 2주차(수) - TDD, e2e 테스트
- 2주차(목) - git merge, TDD
- 2주차(일) - NCP 배포환경 구성, MySQL, nginx, docker, docker-compose
- 3주차(화) - Redis, Multer 파일 업로드, Validation
- 3주차(수) - AES 암복호화, TypeORM Entity Relation
- 3주차(목) - NCP Object Storage, HTTPS, GitHub Actions
- 3주차(토) - Sharp(이미지 최적화)
- 3주차(일) - MongoDB
- 4주차(화) - 플랫폼 종속성 문제 해결(Sharp), 쿼리 최적화
- 4주차(수) - 코드 개선, 트랜잭션 제어
- 4주차(목) - 트랜잭션 제어
- 4주차(일) - docker 이미지 최적화
- 5주차(화) - 어드민 페이지(전체 글, 시스템 정보)
- 5주차(목) - 감정분석 API, e2e 테스트
- 5주차(토) - 유닛 테스트(+ mocking), e2e 테스트(+ 파일 첨부)
- 6주차(화) - ERD
- 2주차(화) - auth, board 모듈 생성 및 테스트 코드 환경 설정
- 2주차(목) - Board, Auth 테스트 코드 작성 및 API 완성
- 3주차(월) - Redis 연결 후 RedisRepository 작성
- 3주차(화) - SignUpUserDto에 ClassValidator 적용
- 3주차(화) - SignIn시 RefreshToken 발급 및 Redis에 저장
- 3주차(화) - 커스텀 AuthGuard 작성
- 3주차(수) - SignOut시 토큰 제거
- 3주차(수) - 깃헙 로그인 구현
- 3주차(토) - OAuth 코드 통합 및 재사용
- 4주차(수) - NestJS + TypeORM으로 MySQL 전문검색 구현
- 4주차(목) - NestJS Interceptor와 로거
- [전체] 10/12(목)
- [전체] 10/15(일)
- [전체] 10/30(월)
- [FE] 11/01(수)~11/03(금)
- [전체] 11/06(월)
- [전체] 11/07(화)
- [전체] 11/09(목)
- [전체] 11/11(토)
- [전체] 11/13(월)
- [BE] 11/14(화)
- [BE] 11/15(수)
- [FE] 11/16(목)
- [FE] 11/19(일)
- [BE] 11/19(일)
- [FE] 11/20(월)
- [BE] 11/20(월)
- [BE] 11/27(월)
- [FE] 12/04(월)
- [BE] 12/04(월)
- [FE] 12/09(금)
- [전체] 12/10(일)
- [FE] 12/11(월)
- [전체] 12/11(월)
- [전체] 12/12(화)