From 8b19c94399f6bc449566b1a98012d20b326500c1 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 11:10:39 +0900 Subject: [PATCH 001/139] =?UTF-8?q?feat:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=EC=9D=B4=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EC=97=90=20@Transactional=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9=20#170?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/auth.service.ts | 19 ++++++++------ backend/src/course/course.service.ts | 6 +++++ backend/src/map/entity/map.entity.ts | 4 +-- backend/src/map/map.service.ts | 37 +++++++++++++++++----------- backend/src/user/user.service.ts | 2 ++ 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 97213069..f7cf4630 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -1,16 +1,17 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { JWTHelper } from './JWTHelper'; -import { CreateUserRequest } from '../user/dto/CreateUserRequest'; -import { UserService } from '../user/user.service'; -import { RefreshTokenRepository } from './refresh-token.repository'; -import { OAuthProvider } from './oauthProvider/OAuthProvider'; -import { AuthenticationException } from './exception/AuthenticationException'; -import { UserRole } from '../user/user.role'; +import { JWTHelper } from '@src/auth/JWTHelper'; +import { CreateUserRequest } from '@src/user/dto/CreateUserRequest'; +import { UserService } from '@src/user/user.service'; +import { RefreshTokenRepository } from '@src/auth/refresh-token.repository'; +import { OAuthProvider } from '@src/auth/oauthProvider/OAuthProvider'; +import { AuthenticationException } from '@src/auth/exception/AuthenticationException'; +import { UserRole } from '@src/user/user.role'; import { OAuthProviderName, getOAuthProviders, -} from './oauthProvider/OAuthProviders'; +} from '@src/auth/oauthProvider/OAuthProviders'; +import { Transactional } from 'typeorm-transactional'; @Injectable() export class AuthService { @@ -40,6 +41,7 @@ export class AuthService { return provider.getAuthUrl(origin); } + @Transactional() async signInWith( providerName: OAuthProviderName, origin: string, @@ -87,6 +89,7 @@ export class AuthService { return provider; } + @Transactional() private async generateTokens(userId: number, role: string) { const accessToken = this.jwtHelper.generateToken( this.accessTokenExpiration, diff --git a/backend/src/course/course.service.ts b/backend/src/course/course.service.ts index df224e12..a77345e8 100644 --- a/backend/src/course/course.service.ts +++ b/backend/src/course/course.service.ts @@ -13,6 +13,7 @@ import { PlaceRepository } from '../place/place.repository'; import { InvalidPlaceToCourseException } from './exception/InvalidPlaceToCourseException'; import { PagedCourseResponse } from './dto/PagedCourseResponse'; import { User } from '../user/entity/user.entity'; +import { Transactional } from 'typeorm-transactional'; @Injectable() export class CourseService { @@ -68,6 +69,7 @@ export class CourseService { return course.user.id; } + @Transactional() async createCourse(userId: number, createCourseForm: CreateCourseRequest) { const user = { id: userId } as User; const course = createCourseForm.toEntity(user); @@ -75,6 +77,7 @@ export class CourseService { return { id: (await this.courseRepository.save(course)).id }; } + @Transactional() async deleteCourse(id: number) { await this.validateCourseExistsById(id); @@ -82,6 +85,7 @@ export class CourseService { return { id }; } + @Transactional() async updateCourseInfo( id: number, updateCourseForm: UpdateCourseInfoRequest, @@ -96,6 +100,7 @@ export class CourseService { ); } + @Transactional() async updateCourseVisibility(id: number, isPublic: boolean) { await this.validateCourseExistsById(id); return this.courseRepository.updateIsPublicById(id, isPublic); @@ -106,6 +111,7 @@ export class CourseService { throw new CourseNotFoundException(id); } + @Transactional() async setPlacesOfCourse( id: number, setPlacesOfCourseRequest: SetPlacesOfCourseRequest, diff --git a/backend/src/map/entity/map.entity.ts b/backend/src/map/entity/map.entity.ts index 6593ff81..32290d48 100644 --- a/backend/src/map/entity/map.entity.ts +++ b/backend/src/map/entity/map.entity.ts @@ -54,11 +54,11 @@ export class Map extends BaseEntity { this.mapPlaces.push(MapPlace.of(placeId, this, color, description)); } - async deletePlace(placeId: number) { + deletePlace(placeId: number) { this.mapPlaces = this.mapPlaces.filter((p) => p.placeId !== placeId); } - async hasPlace(placeId: number) { + hasPlace(placeId: number) { return this.mapPlaces.some((p) => p.placeId === placeId); } diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index fbd491ba..7f924c75 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -1,18 +1,19 @@ import { Injectable } from '@nestjs/common'; -import { MapRepository } from './map.repository'; -import { User } from '../user/entity/user.entity'; -import { MapListResponse } from './dto/MapListResponse'; -import { MapDetailResponse } from './dto/MapDetailResponse'; -import { UserRepository } from '../user/user.repository'; -import { UpdateMapInfoRequest } from './dto/UpdateMapInfoRequest'; -import { CreateMapRequest } from './dto/CreateMapRequest'; -import { MapNotFoundException } from './exception/MapNotFoundException'; -import { DuplicatePlaceToMapException } from './exception/DuplicatePlaceToMapException'; -import { PlaceRepository } from '../place/place.repository'; -import { InvalidPlaceToMapException } from './exception/InvalidPlaceToMapException'; -import { Map } from './entity/map.entity'; -import { Color } from '../place/place.color.enum'; -import { UserRole } from '../user/user.role'; +import { MapRepository } from '@src/map/map.repository'; +import { User } from '@src/user/entity/user.entity'; +import { MapListResponse } from '@src/map/dto/MapListResponse'; +import { MapDetailResponse } from '@src/map/dto/MapDetailResponse'; +import { UserRepository } from '@src/user/user.repository'; +import { UpdateMapInfoRequest } from '@src/map/dto/UpdateMapInfoRequest'; +import { CreateMapRequest } from '@src/map/dto/CreateMapRequest'; +import { MapNotFoundException } from '@src/map/exception/MapNotFoundException'; +import { DuplicatePlaceToMapException } from '@src/map/exception/DuplicatePlaceToMapException'; +import { PlaceRepository } from '@src/place/place.repository'; +import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMapException'; +import { Map } from '@src/map/entity/map.entity'; +import { Color } from '@src/place/place.color.enum'; +import { UserRole } from '@src/user/user.role'; +import { Transactional } from 'typeorm-transactional'; @Injectable() export class MapService { @@ -73,6 +74,7 @@ export class MapService { return await MapDetailResponse.from(map); } + @Transactional() async createMap(userId: number, createMapForm: CreateMapRequest) { const user = { id: userId } as User; const map = createMapForm.toEntity(user); @@ -80,6 +82,7 @@ export class MapService { return { id: (await this.mapRepository.save(map)).id }; } + @Transactional() async deleteMap(id: number) { await this.checkExists(id); @@ -87,6 +90,7 @@ export class MapService { return { id }; } + @Transactional() async updateMapInfo(id: number, updateMapForm: UpdateMapInfoRequest) { await this.checkExists(id); @@ -94,6 +98,7 @@ export class MapService { return this.mapRepository.update(id, { title, description }); } + @Transactional() async updateMapVisibility(id: number, isPublic: boolean) { await this.checkExists(id); @@ -105,6 +110,7 @@ export class MapService { throw new MapNotFoundException(id); } + @Transactional() async addPlace( id: number, placeId: number, @@ -130,11 +136,12 @@ export class MapService { throw new InvalidPlaceToMapException(placeId); } - if (await map.hasPlace(placeId)) { + if (map.hasPlace(placeId)) { throw new DuplicatePlaceToMapException(placeId); } } + @Transactional() async deletePlace(id: number, placeId: number) { const map = await this.mapRepository.findById(id); if (!map) throw new MapNotFoundException(id); diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 61acc2d1..a43e191c 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -3,11 +3,13 @@ import { UserRepository } from './user.repository'; import { CreateUserRequest } from './dto/CreateUserRequest'; import { UserIconResponse } from '@src/user/dto/UserIconResponse'; import { UserNotFoundException } from '@src/user/exception/UserNotFoundException'; +import { Transactional } from 'typeorm-transactional'; @Injectable() export class UserService { constructor(private readonly userRepository: UserRepository) {} + @Transactional() async addUser(userInfo: CreateUserRequest) { const { provider, oauthId } = userInfo; const existingUser = await this.userRepository.findByProviderAndOauthId( From 8f2acef0027af4716533e39abeca495283f10b55 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 13:45:48 +0900 Subject: [PATCH 002/139] =?UTF-8?q?fix:=20=EC=A0=9C=EB=AA=A9=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=BD=94=EC=8A=A4=20=EA=B0=AF=EC=88=98=EB=A5=BC=20=EC=84=B8?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20#170?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/course.repository.ts | 4 +++- backend/src/course/dto/CreateCourseRequest.ts | 2 +- backend/src/course/dto/PagedCourseResponse.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/course/course.repository.ts b/backend/src/course/course.repository.ts index c43ff306..61262bdc 100644 --- a/backend/src/course/course.repository.ts +++ b/backend/src/course/course.repository.ts @@ -40,7 +40,9 @@ export class CourseRepository extends SoftDeleteRepository { } countByTitleAndIsPublic(title: string) { - return this.count({ where: { title, isPublic: true } }); + return this.count({ + where: { title: ILike(`%${title}%`), isPublic: true }, + }); } countAllPublic() { diff --git a/backend/src/course/dto/CreateCourseRequest.ts b/backend/src/course/dto/CreateCourseRequest.ts index 091c5782..075f0bc7 100644 --- a/backend/src/course/dto/CreateCourseRequest.ts +++ b/backend/src/course/dto/CreateCourseRequest.ts @@ -1,4 +1,4 @@ -import { User } from '../../user/entity/user.entity'; +import { User } from '@src/user/entity/user.entity'; import { IsBoolean, IsNotEmpty, diff --git a/backend/src/course/dto/PagedCourseResponse.ts b/backend/src/course/dto/PagedCourseResponse.ts index ee24d9c5..8643c608 100644 --- a/backend/src/course/dto/PagedCourseResponse.ts +++ b/backend/src/course/dto/PagedCourseResponse.ts @@ -1,5 +1,5 @@ import { CourseListResponse } from './CourseListResponse'; -import { PaginationResponse } from '../../common/dto/PaginationResponse'; +import { PaginationResponse } from '@src/common/dto/PaginationResponse'; export class PagedCourseResponse extends PaginationResponse { constructor( From ebf6d7769d840d3db4720dcbb3dbda0d92887762 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 13:46:28 +0900 Subject: [PATCH 003/139] =?UTF-8?q?test:=20service=EC=97=90=EC=84=9C=20moc?= =?UTF-8?q?king=20=EB=8C=80=EC=8B=A0=20=ED=86=B5=ED=95=A9=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20#170?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/course/course.service.test.ts | 255 ++++++++++----------- 1 file changed, 117 insertions(+), 138 deletions(-) diff --git a/backend/test/course/course.service.test.ts b/backend/test/course/course.service.test.ts index 8419d849..2f831422 100644 --- a/backend/test/course/course.service.test.ts +++ b/backend/test/course/course.service.test.ts @@ -1,7 +1,6 @@ import { CourseRepository } from '@src/course/course.repository'; import { CourseService } from '@src/course/course.service'; -import { Test, TestingModule } from '@nestjs/testing'; -import { CourseFixture } from './fixture/course.fixture'; +import { CourseFixture } from '@test/course/fixture/course.fixture'; import { User } from '@src/user/entity/user.entity'; import { CourseListResponse } from '@src/course/dto/CourseListResponse'; import { PagedCourseResponse } from '@src/course/dto/PagedCourseResponse'; @@ -15,74 +14,57 @@ import { SetPlacesOfCourseRequestItem, } from '@src/course/dto/AddPlaceToCourseRequest'; import { InvalidPlaceToCourseException } from '@src/course/exception/InvalidPlaceToCourseException'; +import { initializeTransactionalContext } from 'typeorm-transactional'; +import { MySqlContainer, StartedMySqlContainer } from '@testcontainers/mysql'; +import { initDataSource } from '@test/config/datasource.config'; +import { CoursePlace } from '@src/course/entity/course-place.entity'; +import { DataSource, Repository } from 'typeorm'; +import { UserFixture } from '@test/user/fixture/user.fixture'; +import { PlaceFixture } from '@test/place/fixture/place.fixture'; async function createPagedResponse( courses: Course[], totalCount: number, - page: number, + currentPage: number, pageSize: number, ) { const courseList = await Promise.all(courses.map(CourseListResponse.from)); - return new PagedCourseResponse(courseList, totalCount, page, pageSize); + return new PagedCourseResponse(courseList, totalCount, currentPage, pageSize); } describe('CourseService', () => { let courseService: CourseService; - let courseRepository: Partial>; - let placeRepository: Partial>; + let placeRepository: PlaceRepository; + let container: StartedMySqlContainer; + let datasource: DataSource; + let courseRepository: CourseRepository; + let coursePlaceRepository: Repository; let fakeUser1: User; - let page: number; + let currentPage: number; let pageSize: number; let foodQuery: string; beforeAll(async () => { - fakeUser1 = { id: 1 } as User; - page = 1; + initializeTransactionalContext(); + container = await new MySqlContainer().withReuse().start(); + datasource = await initDataSource(container); + coursePlaceRepository = datasource.getRepository(CoursePlace); + placeRepository = new PlaceRepository(datasource); + courseRepository = new CourseRepository(datasource, coursePlaceRepository); + courseService = new CourseService(courseRepository, placeRepository); + fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); + await datasource.getRepository(User).save(fakeUser1); + currentPage = 1; pageSize = 10; foodQuery = 'Food'; }); - afterAll(() => { - jest.clearAllMocks(); + afterAll(async () => { + await datasource.destroy(); }); beforeEach(async () => { - courseRepository = { - save: jest.fn(), - findAll: jest.fn(), - findById: jest.fn(), - findByUserId: jest.fn(), - searchByTitleQuery: jest.fn(), - countAllPublic: jest.fn(), - countByTitleAndIsPublic: jest.fn(), - countByUserId: jest.fn(), - updateIsPublicById: jest.fn(), - updateInfoById: jest.fn(), - updateCoursePlaceById: jest.fn(), - existById: jest.fn(), - softDelete: jest.fn(), - }; - placeRepository = { - existById: jest.fn(), - }; - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CourseService, - { - provide: CourseRepository, - useValue: courseRepository, - }, - { - provide: PlaceRepository, - useValue: placeRepository, - }, - ], - }).compile(); - courseService = module.get(CourseService); - }); - - afterEach(() => { - jest.clearAllMocks(); + await courseRepository.delete({}); }); describe('코스 목록을 조회할 때', () => { @@ -97,19 +79,18 @@ describe('CourseService', () => { isPublic, }), ); - courseRepository.findAll.mockResolvedValue(publicCourses); - courseRepository.countAllPublic.mockResolvedValue(publicCourses.length); + await courseRepository.save(publicCourses); const result = await courseService.searchPublicCourses( null, - page, + currentPage, pageSize, ); const expectedResponse = await createPagedResponse( publicCourses, publicCourses.length, - page, + currentPage, pageSize, ); expect(result).toEqual(expectedResponse); @@ -126,23 +107,18 @@ describe('CourseService', () => { isPublic, }), ); - courseRepository.searchByTitleQuery.mockResolvedValue( - publicCoursesWithFood, - ); - courseRepository.countByTitleAndIsPublic.mockResolvedValue( - publicCoursesWithFood.length, - ); + await courseRepository.save(publicCoursesWithFood); const result = await courseService.searchPublicCourses( foodQuery, - page, + currentPage, pageSize, ); const expectedResponse = await createPagedResponse( publicCoursesWithFood, publicCoursesWithFood.length, - page, + currentPage, pageSize, ); expect(result).toEqual(expectedResponse); @@ -159,23 +135,18 @@ describe('CourseService', () => { isPublic, }), ); - courseRepository.searchByTitleQuery.mockResolvedValue( - publicCoursesWithFood, - ); - courseRepository.countByTitleAndIsPublic.mockResolvedValue( - publicCoursesWithFood.length, - ); + await courseRepository.save(publicCoursesWithFood); const result = await courseService.searchPublicCourses( foodQuery, - page, + currentPage, pageSize, ); const expectedResponse = await createPagedResponse( publicCoursesWithFood, publicCoursesWithFood.length, - page, + currentPage, pageSize, ); expect(result).toEqual(expectedResponse); @@ -196,19 +167,18 @@ describe('CourseService', () => { isPublic, }), ); - courseRepository.findByUserId.mockResolvedValue(ownCourses); - courseRepository.countByUserId.mockResolvedValue(ownCourses.length); + await courseRepository.save(ownCourses); const result = await courseService.getOwnCourses( fakeUser1.id, - page, + currentPage, pageSize, ); const expectedResponse = await createPagedResponse( ownCourses, ownCourses.length, - page, + currentPage, pageSize, ); expect(result).toEqual(expectedResponse); @@ -221,20 +191,17 @@ describe('CourseService', () => { title: 'Course 1', isPublic: true, }); - const courseWithId = { ...course, id: 1 } as Course; - courseRepository.findById.mockResolvedValue(courseWithId); - courseWithId.getPlacesWithComment = jest.fn().mockResolvedValue([]); + const savedCourse = await courseRepository.save(course); - const result = await courseService.getCourseById(courseWithId.id); + const result = await courseService.getCourseById(savedCourse.id); - expect(result.id).toEqual(courseWithId.id); - expect(result.title).toEqual(courseWithId.title); - expect(result.isPublic).toEqual(courseWithId.isPublic); + expect(result.id).toEqual(savedCourse.id); + expect(result.title).toEqual(savedCourse.title); + expect(result.isPublic).toEqual(savedCourse.isPublic); }); it('실패하면 코스를 찾을 수 없다는 예외를 던진다', async () => { const courseId = 1; - courseRepository.findById.mockResolvedValue(null); const result = courseService.getCourseById(courseId); @@ -252,17 +219,15 @@ describe('CourseService', () => { title: 'Course 1', isPublic: true, }); - const courseWithId = { ...course, id: 1 } as Course; - courseRepository.findById.mockResolvedValue(courseWithId); + const savedCourse = await courseRepository.save(course); - const result = await courseService.getCourseOwnerId(courseWithId.id); + const result = await courseService.getCourseOwnerId(savedCourse.id); expect(result).toEqual(fakeUser1.id); }); it('실패하면 코스를 찾을 수 없다는 예외를 던진다', async () => { const courseId = 1; - courseRepository.findById.mockResolvedValue(null); const result = courseService.getCourseOwnerId(courseId); @@ -280,33 +245,32 @@ describe('CourseService', () => { thumbnailUrl: 'https://example.com/course_thumbnail.jpg', description: 'A sample course with popular places', }; - const course = CourseFixture.createCourse({ - user: fakeUser1, - ...createCourseForm, - }); - course.id = 1; - courseRepository.save.mockResolvedValue(course); const result = await courseService.createCourse( fakeUser1.id, CreateCourseRequest.from(createCourseForm), ); - expect(result.id).toEqual(course.id); + const savedCourse = await courseRepository.findById(result.id); + expect(result.id).toEqual(savedCourse.id); }); describe('코스를 삭제할 때', () => { it('코스가 존재하면 삭제할 수 있다', async () => { - const courseId = 1; - courseRepository.existById.mockResolvedValue(true); + const course = CourseFixture.createCourse({ + user: fakeUser1, + title: 'Course 1', + isPublic: true, + }); + const savedCourse = await courseRepository.save(course); - const result = await courseService.deleteCourse(courseId); + const result = await courseService.deleteCourse(savedCourse.id); - expect(result.id).toEqual(courseId); + expect(result.id).toEqual(savedCourse.id); }); + it('코스가 존재하지 않으면 예외를 던진다', async () => { const courseId = 1; - courseRepository.existById.mockResolvedValue(false); const result = courseService.deleteCourse(courseId); @@ -317,31 +281,35 @@ describe('CourseService', () => { }); }); - it('코스 정보를 수정할 때 코스가 존재하지 않으면 예외를 던진다', async () => { - const courseId = 1; - const updateCourseForm = { - title: 'My Course', - description: 'A sample course with popular places', - thumbnailUrl: 'https://example.com/course_thumbnail.jpg', - } as UpdateCourseInfoRequest; - courseRepository.existById.mockResolvedValue(false); + describe('코스를 수정할 때', () => { + it('코스 정보를 수정할 때 코스가 존재하지 않으면 예외를 던진다', async () => { + const courseId = 1; + const updateCourseForm = { + title: 'My Course', + description: 'A sample course with popular places', + thumbnailUrl: 'https://example.com/course_thumbnail.jpg', + } as UpdateCourseInfoRequest; - const result = courseService.updateCourseInfo(courseId, updateCourseForm); + const result = courseService.updateCourseInfo(courseId, updateCourseForm); - await expect(result).rejects.toThrow(CourseNotFoundException); - await expect(result).rejects.toThrow(new CourseNotFoundException(courseId)); - }); + await expect(result).rejects.toThrow(CourseNotFoundException); + await expect(result).rejects.toThrow( + new CourseNotFoundException(courseId), + ); + }); - it('코스 공개/비공개 여부를 수정할 때 코스가 존재하지 않으면 예외를 던진다', async () => { - const courseId = 1; - const isPublic = true; - courseRepository.existById.mockResolvedValue(false); + it('코스 공개/비공개 여부를 수정할 때 코스가 존재하지 않으면 예외를 던진다', async () => { + const courseId = 1; - const result = courseService.updateCourseVisibility(courseId, isPublic); + const result = courseService.updateCourseVisibility(courseId, true); - await expect(result).rejects.toThrow(CourseNotFoundException); - await expect(result).rejects.toThrow(new CourseNotFoundException(courseId)); + await expect(result).rejects.toThrow(CourseNotFoundException); + await expect(result).rejects.toThrow( + new CourseNotFoundException(courseId), + ); + }); }); + describe('코스에 장소를 추가할 때', () => { const setPlacesOfCourseRequest = { places: [ @@ -351,10 +319,9 @@ describe('CourseService', () => { }, ] as SetPlacesOfCourseRequestItem[], } as SetPlacesOfCourseRequest; + it('코스가 존재하지 않으면 예외를 던진다', async () => { const courseId = 1; - courseRepository.findById.mockResolvedValue(null); - placeRepository.existById.mockResolvedValue(true); const result = courseService.setPlacesOfCourse( courseId, @@ -368,12 +335,15 @@ describe('CourseService', () => { }); it('장소가 존재하지 않으면 예외를 던진다', async () => { - const courseId = 1; - courseRepository.findById.mockResolvedValue({} as Course); - placeRepository.existById.mockResolvedValue(false); + const course = CourseFixture.createCourse({ + user: fakeUser1, + title: 'Course 1', + isPublic: true, + }); + const savedCourse = await courseRepository.save(course); const result = courseService.setPlacesOfCourse( - courseId, + savedCourse.id, setPlacesOfCourseRequest, ); @@ -384,34 +354,43 @@ describe('CourseService', () => { }); it('성공적으로 코스에 장소를 추가할 수 있다', async () => { - const courseId = 1; const course = CourseFixture.createCourse({ user: fakeUser1, title: 'Course 1', isPublic: true, }); - const courseWithId = { ...course, id: courseId } as Course; - courseRepository.findById.mockResolvedValue(courseWithId); - placeRepository.existById.mockResolvedValue(true); - const mockPlaces = [ - { place: { id: 1, name: 'Place 1' }, comment: 'A popular place' }, - { place: { id: 2, name: 'Place 2' }, comment: 'A good place' }, - ]; - courseWithId.setPlaces = jest.fn(); - courseWithId.getPlacesWithComment = jest - .fn() - .mockResolvedValue(mockPlaces); + const savedCourse = await courseRepository.save(course); + const places = await Promise.all( + Array.from({ length: 3 }, (_, i) => + placeRepository.save( + PlaceFixture.createPlace({ + googlePlaceId: `googlePlaceId_${i + 1}`, + name: `Place ${i + 1}`, + formattedAddress: `Address ${i + 1}`, + }), + ), + ), + ); + const setPlacesOfCourseRequest = { + places: places.map((place, index) => ({ + placeId: place.id, + comment: `Comment ${index + 1}`, + })), + }; const result = await courseService.setPlacesOfCourse( - courseId, + savedCourse.id, setPlacesOfCourseRequest, ); - mockPlaces.forEach((place, index) => { + + expect(result.places).toHaveLength( + setPlacesOfCourseRequest.places.length, + ); + + setPlacesOfCourseRequest.places.forEach((expectedPlace, index) => { expect(result.places[index]).toMatchObject({ - id: place.place.id, - name: place.place.name, - comment: place.comment, - order: index + 1, + id: expectedPlace.placeId, + comment: expectedPlace.comment, }); }); }); From 88c5e7090bd28564291b2af355dd32450e7484e0 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 13:52:06 +0900 Subject: [PATCH 004/139] =?UTF-8?q?fix:=20=EC=BD=94=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20transaction?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=20#170?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/course.service.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/course/course.service.ts b/backend/src/course/course.service.ts index a77345e8..df224e12 100644 --- a/backend/src/course/course.service.ts +++ b/backend/src/course/course.service.ts @@ -13,7 +13,6 @@ import { PlaceRepository } from '../place/place.repository'; import { InvalidPlaceToCourseException } from './exception/InvalidPlaceToCourseException'; import { PagedCourseResponse } from './dto/PagedCourseResponse'; import { User } from '../user/entity/user.entity'; -import { Transactional } from 'typeorm-transactional'; @Injectable() export class CourseService { @@ -69,7 +68,6 @@ export class CourseService { return course.user.id; } - @Transactional() async createCourse(userId: number, createCourseForm: CreateCourseRequest) { const user = { id: userId } as User; const course = createCourseForm.toEntity(user); @@ -77,7 +75,6 @@ export class CourseService { return { id: (await this.courseRepository.save(course)).id }; } - @Transactional() async deleteCourse(id: number) { await this.validateCourseExistsById(id); @@ -85,7 +82,6 @@ export class CourseService { return { id }; } - @Transactional() async updateCourseInfo( id: number, updateCourseForm: UpdateCourseInfoRequest, @@ -100,7 +96,6 @@ export class CourseService { ); } - @Transactional() async updateCourseVisibility(id: number, isPublic: boolean) { await this.validateCourseExistsById(id); return this.courseRepository.updateIsPublicById(id, isPublic); @@ -111,7 +106,6 @@ export class CourseService { throw new CourseNotFoundException(id); } - @Transactional() async setPlacesOfCourse( id: number, setPlacesOfCourseRequest: SetPlacesOfCourseRequest, From 5033a57a81e9bb1e6f93684dc500481b553a5eeb Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 13:58:01 +0900 Subject: [PATCH 005/139] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20transactional=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0=20#170?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/auth.service.ts | 5 +---- backend/src/course/course.service.ts | 2 ++ backend/src/map/map.service.ts | 6 ------ backend/src/place/place.service.ts | 1 - backend/src/user/user.service.ts | 2 -- 5 files changed, 3 insertions(+), 13 deletions(-) diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index f7cf4630..f813415a 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -8,10 +8,9 @@ import { OAuthProvider } from '@src/auth/oauthProvider/OAuthProvider'; import { AuthenticationException } from '@src/auth/exception/AuthenticationException'; import { UserRole } from '@src/user/user.role'; import { - OAuthProviderName, getOAuthProviders, + OAuthProviderName, } from '@src/auth/oauthProvider/OAuthProviders'; -import { Transactional } from 'typeorm-transactional'; @Injectable() export class AuthService { @@ -41,7 +40,6 @@ export class AuthService { return provider.getAuthUrl(origin); } - @Transactional() async signInWith( providerName: OAuthProviderName, origin: string, @@ -89,7 +87,6 @@ export class AuthService { return provider; } - @Transactional() private async generateTokens(userId: number, role: string) { const accessToken = this.jwtHelper.generateToken( this.accessTokenExpiration, diff --git a/backend/src/course/course.service.ts b/backend/src/course/course.service.ts index df224e12..2504c38a 100644 --- a/backend/src/course/course.service.ts +++ b/backend/src/course/course.service.ts @@ -13,6 +13,7 @@ import { PlaceRepository } from '../place/place.repository'; import { InvalidPlaceToCourseException } from './exception/InvalidPlaceToCourseException'; import { PagedCourseResponse } from './dto/PagedCourseResponse'; import { User } from '../user/entity/user.entity'; +import { Transactional } from 'typeorm-transactional'; @Injectable() export class CourseService { @@ -106,6 +107,7 @@ export class CourseService { throw new CourseNotFoundException(id); } + @Transactional() async setPlacesOfCourse( id: number, setPlacesOfCourseRequest: SetPlacesOfCourseRequest, diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 7f924c75..32470c68 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -74,7 +74,6 @@ export class MapService { return await MapDetailResponse.from(map); } - @Transactional() async createMap(userId: number, createMapForm: CreateMapRequest) { const user = { id: userId } as User; const map = createMapForm.toEntity(user); @@ -82,7 +81,6 @@ export class MapService { return { id: (await this.mapRepository.save(map)).id }; } - @Transactional() async deleteMap(id: number) { await this.checkExists(id); @@ -90,7 +88,6 @@ export class MapService { return { id }; } - @Transactional() async updateMapInfo(id: number, updateMapForm: UpdateMapInfoRequest) { await this.checkExists(id); @@ -98,7 +95,6 @@ export class MapService { return this.mapRepository.update(id, { title, description }); } - @Transactional() async updateMapVisibility(id: number, isPublic: boolean) { await this.checkExists(id); @@ -110,7 +106,6 @@ export class MapService { throw new MapNotFoundException(id); } - @Transactional() async addPlace( id: number, placeId: number, @@ -141,7 +136,6 @@ export class MapService { } } - @Transactional() async deletePlace(id: number, placeId: number) { const map = await this.mapRepository.findById(id); if (!map) throw new MapNotFoundException(id); diff --git a/backend/src/place/place.service.ts b/backend/src/place/place.service.ts index 8d8777a4..44b0895d 100644 --- a/backend/src/place/place.service.ts +++ b/backend/src/place/place.service.ts @@ -26,7 +26,6 @@ export class PlaceService { this.GOOGLE_API_KEY = this.configService.get('GOOGLE_MAPS_API_KEY'); } - @Transactional() async addPlace(createPlaceRequest: CreatePlaceRequest) { const { googlePlaceId } = createPlaceRequest; if (await this.placeRepository.findByGooglePlaceId(googlePlaceId)) { diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index a43e191c..61acc2d1 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -3,13 +3,11 @@ import { UserRepository } from './user.repository'; import { CreateUserRequest } from './dto/CreateUserRequest'; import { UserIconResponse } from '@src/user/dto/UserIconResponse'; import { UserNotFoundException } from '@src/user/exception/UserNotFoundException'; -import { Transactional } from 'typeorm-transactional'; @Injectable() export class UserService { constructor(private readonly userRepository: UserRepository) {} - @Transactional() async addUser(userInfo: CreateUserRequest) { const { provider, oauthId } = userInfo; const existingUser = await this.userRepository.findByProviderAndOauthId( From 8d53c176e5684b5f406b1d76171af332ef293ad3 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 13:59:57 +0900 Subject: [PATCH 006/139] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=20=EC=A0=9C=EA=B1=B0=20#170?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.service.ts | 1 - backend/src/place/place.service.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 32470c68..8584d74e 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -13,7 +13,6 @@ import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMap import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { UserRole } from '@src/user/user.role'; -import { Transactional } from 'typeorm-transactional'; @Injectable() export class MapService { diff --git a/backend/src/place/place.service.ts b/backend/src/place/place.service.ts index 44b0895d..a32fff12 100644 --- a/backend/src/place/place.service.ts +++ b/backend/src/place/place.service.ts @@ -5,7 +5,6 @@ import { PlaceNotFoundException } from '@src/place/exception/PlaceNotFoundExcept import { PlaceAlreadyExistsException } from '@src/place/exception/PlaceAlreadyExistsException'; import { PlaceSearchResponse } from '@src/place/dto/PlaceSearchResponse'; import { ConfigService } from '@nestjs/config'; -import { Transactional } from 'typeorm-transactional'; import { SearchService } from '@src/search/search.service'; import { PinoLogger } from 'nestjs-pino'; From 120ada1e38f7832e13ea519f9ea48d2967ac6044 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 14:18:02 +0900 Subject: [PATCH 007/139] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=94=BD=EC=8A=A4=EC=B3=90=EC=97=90=EC=84=9C=20unique=20?= =?UTF-8?q?=ED=95=9C=20=EA=B0=92=EB=93=A4=EC=97=90=20=EB=8C=80=ED=95=B4=20?= =?UTF-8?q?=EB=9E=9C=EB=8D=A4=20=EA=B0=92=20=EB=B6=80=EC=97=AC=20#170?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/course/course.repository.test.ts | 4 ++-- backend/test/course/course.service.test.ts | 2 +- backend/test/place/fixture/place.fixture.ts | 2 +- backend/test/place/place.repository.test.ts | 17 +++++------------ backend/test/user/fixture/user.fixture.ts | 2 +- backend/test/user/fixture/user.fixture.type.ts | 2 +- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/backend/test/course/course.repository.test.ts b/backend/test/course/course.repository.test.ts index 33caa8bc..b9b58668 100644 --- a/backend/test/course/course.repository.test.ts +++ b/backend/test/course/course.repository.test.ts @@ -26,8 +26,8 @@ describe('CourseRepository', () => { coursePlaceRepository = datasource.getRepository(CoursePlace); courseRepository = new CourseRepository(datasource, coursePlaceRepository); - fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); - fakeUser2 = UserFixture.createUser({ oauthId: 'def' }); + fakeUser1 = UserFixture.createUser({}); + fakeUser2 = UserFixture.createUser({}); await datasource.getRepository(User).save([fakeUser1, fakeUser2]); }); diff --git a/backend/test/course/course.service.test.ts b/backend/test/course/course.service.test.ts index 2f831422..9be5256a 100644 --- a/backend/test/course/course.service.test.ts +++ b/backend/test/course/course.service.test.ts @@ -52,7 +52,7 @@ describe('CourseService', () => { placeRepository = new PlaceRepository(datasource); courseRepository = new CourseRepository(datasource, coursePlaceRepository); courseService = new CourseService(courseRepository, placeRepository); - fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); + fakeUser1 = UserFixture.createUser({}); await datasource.getRepository(User).save(fakeUser1); currentPage = 1; pageSize = 10; diff --git a/backend/test/place/fixture/place.fixture.ts b/backend/test/place/fixture/place.fixture.ts index 52b7d1cb..00d88c76 100644 --- a/backend/test/place/fixture/place.fixture.ts +++ b/backend/test/place/fixture/place.fixture.ts @@ -3,7 +3,7 @@ import { PlaceFixtureType } from '@test/place/fixture/place.fixture.type'; export class PlaceFixture { static createPlace = ({ - googlePlaceId = 'googlePlaceId_1', + googlePlaceId = `googlePlaceId${Date.now()}${Math.random()}`, name = 'Central Park', imageUrl = 'https://example.com/central_park.jpg', rating = 4.5, diff --git a/backend/test/place/place.repository.test.ts b/backend/test/place/place.repository.test.ts index 6276f6f4..86100694 100644 --- a/backend/test/place/place.repository.test.ts +++ b/backend/test/place/place.repository.test.ts @@ -52,9 +52,9 @@ describe('PlaceRepository', () => { it('페이지 번호와 페이지 크기를 기준으로 모든 장소를 반환한다', async () => { const places = [ - PlaceFixture.createPlace({ googlePlaceId: 'googlePlaceId_1' }), - PlaceFixture.createPlace({ googlePlaceId: 'googlePlaceId_2' }), - PlaceFixture.createPlace({ googlePlaceId: 'googlePlaceId_3' }), + PlaceFixture.createPlace({}), + PlaceFixture.createPlace({}), + PlaceFixture.createPlace({}), ]; await placeRepository.save(places); @@ -101,7 +101,6 @@ describe('PlaceRepository', () => { it('페이지 크기가 전체 개수를 초과할 경우에도 정상적으로 작동한다', async () => { const places = Array.from({ length: 50 }, (_, i) => PlaceFixture.createPlace({ - googlePlaceId: `googlePlaceId_${i + 1}`, name: `Place ${i + 1}`, formattedAddress: `Address ${i + 1}`, }), @@ -166,26 +165,22 @@ describe('PlaceRepository', () => { it('장소 이름이나 주소에 포함된 키워드를 찾아 해당하는 장소를 반환한다', async () => { const placesWithParkName = [ { - googlePlaceId: 'googlePlaceId_1', name: 'Central Park', formattedAddress: 'New York, NY, USA', }, { - googlePlaceId: 'googlePlaceId_2', name: 'Tower Park', formattedAddress: 'London, UK', }, ]; const placesWithParkAddress = [ { - googlePlaceId: 'googlePlaceId_3', name: 'Eiffel Tower', formattedAddress: 'Park Avenue, New York, NY, USA', }, ]; const placesEtc = [ { - googlePlaceId: 'googlePlaceId_4', name: 'Seoul Forest', formattedAddress: 'Seoul, South Korea', }, @@ -195,8 +190,8 @@ describe('PlaceRepository', () => { ...placesWithParkName, ...placesWithParkAddress, ...placesEtc, - ].map(({ googlePlaceId, name, formattedAddress }) => - PlaceFixture.createPlace({ googlePlaceId, name, formattedAddress }), + ].map(({ name, formattedAddress }) => + PlaceFixture.createPlace({ name, formattedAddress }), ); await placeRepository.save(places); @@ -228,7 +223,6 @@ describe('PlaceRepository', () => { it('검색 키워드는 대소문자를 구분하지 않는다', async () => { const place = PlaceFixture.createPlace({ - googlePlaceId: 'googlePlaceId_1', name: 'Park View', formattedAddress: 'New York, NY, USA', }); @@ -259,7 +253,6 @@ describe('PlaceRepository', () => { it('결과 개수가 페이지 크기를 초과할 경우 페이지 크기만큼만 반환한다', async () => { const places = Array.from({ length: 20 }, (_, i) => PlaceFixture.createPlace({ - googlePlaceId: `googlePlaceId_${i + 1}`, name: `Place ${i + 1}`, formattedAddress: `Address ${i + 1}`, }), diff --git a/backend/test/user/fixture/user.fixture.ts b/backend/test/user/fixture/user.fixture.ts index cabfb24c..21caf7be 100644 --- a/backend/test/user/fixture/user.fixture.ts +++ b/backend/test/user/fixture/user.fixture.ts @@ -6,7 +6,7 @@ export class UserFixture { static createUser({ provider = 'google', nickname = 'test', - oauthId = 'abcd1234', + oauthId = `${Date.now()}${Math.random()}`, role = UserRole.MEMBER, profileImageUrl = 'https://test.com/test.jpg', }: UserFixtureType) { diff --git a/backend/test/user/fixture/user.fixture.type.ts b/backend/test/user/fixture/user.fixture.type.ts index 11ec6b0e..2406a2e3 100644 --- a/backend/test/user/fixture/user.fixture.type.ts +++ b/backend/test/user/fixture/user.fixture.type.ts @@ -1,4 +1,4 @@ -import { UserRole } from '../../../src/user/user.role'; +import { UserRole } from '@src/user/user.role'; export type UserFixtureType = { provider?: string; From 46be96c992a603a7c0c66b1a9c6895de828f30f3 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 15:30:10 +0900 Subject: [PATCH 008/139] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20api=20=EA=B5=AC=ED=98=84=20#159?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/auth.controller.ts | 26 +++++++++++++++++++++++++- backend/src/auth/auth.service.ts | 4 ++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 96c33355..0a1c2e4e 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -1,8 +1,20 @@ import { Request, Response } from 'express'; -import { Body, Controller, Post, Get, Param, Res, Req } from '@nestjs/common'; +import { + Body, + Controller, + Post, + Get, + Param, + Res, + Req, + Delete, + UseGuards, +} from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthenticationException } from './exception/AuthenticationException'; import { getOAuthProviderNameByValue } from './oauthProvider/OAuthProviders'; +import { JwtAuthGuard } from '@src/auth/JwtAuthGuard'; +import { AuthUser } from '@src/auth/AuthUser.decorator'; const REFRESH_TOKEN = 'refreshToken'; @@ -49,6 +61,18 @@ export class AuthController { }); } + @Delete('signOut') + @UseGuards(JwtAuthGuard) + async signOut(@Res() response: Response, @AuthUser() user: AuthUser) { + await this.authService.signOut(user.userId); + response.clearCookie(REFRESH_TOKEN, { + httpOnly: true, + secure: true, + sameSite: 'none', + }); + response.status(204).send(); + } + @Post('refresh') async refreshAccessToken(@Req() request: Request) { const refreshToken = request.cookies[REFRESH_TOKEN]; diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index f813415a..020a5df3 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -110,4 +110,8 @@ export class AuthService { return { accessToken, refreshToken }; } + + async signOut(userId: number) { + await this.refreshTokenRepository.deleteByUserId(userId); + } } From 8168efa6b6d4bb67ecc9313b17ee3d1d2b984a04 Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 25 Nov 2024 15:30:27 +0900 Subject: [PATCH 009/139] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20mutation=EC=97=90=20url=20=EC=A0=81=EC=9A=A9=20#159?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/auth/index.ts | 2 +- frontend/src/constants/api.ts | 1 + frontend/src/hooks/api/useLogoutMutation.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts index 606dfef3..c46aa500 100644 --- a/frontend/src/api/auth/index.ts +++ b/frontend/src/api/auth/index.ts @@ -21,6 +21,6 @@ export const getUserInfo = async () => { }; export const deleteLogOut = async () => { - const { data } = await axiosInstance.delete(''); + const { data } = await axiosInstance.delete(END_POINTS.LOGOUT); return data; }; diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index 7818f399..dc3ab64e 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -28,6 +28,7 @@ export const END_POINTS = { `/courses/${courseId}/places/${placeId}`, GOOGLE_LOGIN: '/oauth/google/signIn', + LOGOUT: 'oauth/signOut', MY_MAP: '/maps/my', MY_COURSE: '/courses/my', PLACE: '/places', diff --git a/frontend/src/hooks/api/useLogoutMutation.ts b/frontend/src/hooks/api/useLogoutMutation.ts index b4ff671c..039aa67c 100644 --- a/frontend/src/hooks/api/useLogoutMutation.ts +++ b/frontend/src/hooks/api/useLogoutMutation.ts @@ -2,6 +2,7 @@ import { useStore } from '@/store/useStore'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { deleteLogOut } from '@/api/auth'; + export const useLogOutMutation = () => { const navigate = useNavigate(); const queryClient = useQueryClient(); From 4ecf81afa5379db066ace73089ca7683195c2c6e Mon Sep 17 00:00:00 2001 From: miensoap Date: Mon, 25 Nov 2024 13:29:54 +0900 Subject: [PATCH 010/139] =?UTF-8?q?test:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.controller.spec.ts | 73 -------------------------- 1 file changed, 73 deletions(-) delete mode 100644 backend/src/map/map.controller.spec.ts diff --git a/backend/src/map/map.controller.spec.ts b/backend/src/map/map.controller.spec.ts deleted file mode 100644 index 25040856..00000000 --- a/backend/src/map/map.controller.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { MapController } from './map.controller'; -import { MapService } from './map.service'; -import * as request from 'supertest'; -import { INestApplication, ValidationPipe } from '@nestjs/common'; - -describe('MapController', () => { - let app: INestApplication; - const mapService = { - searchMap: jest.fn(), - getOwnMaps: jest.fn(), - createMap: jest.fn(), - }; - - beforeAll(async () => { - const moduleRef: TestingModule = await Test.createTestingModule({ - controllers: [MapController], - providers: [ - { - provide: MapService, - useValue: mapService, - }, - ], - }).compile(); - - app = moduleRef.createNestApplication(); - app.useGlobalPipes(new ValidationPipe()); - await app.init(); - }); - - afterAll(async () => { - await app.close(); - }); - - it('/GET maps', () => { - mapService.searchMap.mockResolvedValue([]); - return request(app.getHttpServer()).get('/maps').expect(200).expect([]); - }); - - it('/GET maps/my', () => { - mapService.getOwnMaps.mockResolvedValue([]); - return request(app.getHttpServer()).get('/maps/my').expect(200).expect([]); - }); - - it('/POST maps - success', () => { - const createMapFormData = { - title: 'Test Map', - isPublic: true, - description: 'Test Description', - thumbnailUrl: 'http://example.com/thumbnail.jpg', - }; - - mapService.createMap.mockResolvedValue({ id: 1 }); - - return request(app.getHttpServer()) - .post('/maps') - .send(createMapFormData) - .expect(201) - .expect({ id: 1 }); - }); - - it('/POST maps - validation error', () => { - const invalidCreateMapForm = { - title: '', - isPublic: 'not-a-boolean', - }; - - return request(app.getHttpServer()) - .post('/maps') - .send(invalidCreateMapForm) - .expect(400); - }); -}); From b0b27005c0dbd717220fa9d2265627b516810883 Mon Sep 17 00:00:00 2001 From: miensoap Date: Mon, 25 Nov 2024 13:30:26 +0900 Subject: [PATCH 011/139] =?UTF-8?q?fix:=20=EC=A7=80=EB=8F=84/=EC=BD=94?= =?UTF-8?q?=EC=8A=A4=20=EA=B3=B5=EA=B0=9C=20=EC=97=AC=EB=B6=80=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/course.controller.ts | 5 +++++ backend/src/map/map.controller.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/backend/src/course/course.controller.ts b/backend/src/course/course.controller.ts index a80d7432..7c978d1d 100644 --- a/backend/src/course/course.controller.ts +++ b/backend/src/course/course.controller.ts @@ -9,6 +9,7 @@ import { Put, Query, UseGuards, + BadRequestException, } from '@nestjs/common'; import { CreateCourseRequest } from './dto/CreateCourseRequest'; import { UpdateCourseInfoRequest } from './dto/UpdateCourseInfoRequest'; @@ -83,6 +84,10 @@ export class CourseController { @Param('id') id: number, @Body('isPublic') isPublic: boolean, ) { + if (typeof isPublic !== 'boolean') { + throw new BadRequestException('공개 여부는 boolean 타입이어야 합니다.'); + } + await this.courseService.updateCourseVisibility(id, isPublic); return { id, isPublic }; } diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 4308dad8..1b0e7756 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -7,6 +7,7 @@ import { Delete, Param, Patch, + BadRequestException, } from '@nestjs/common'; import { MapService } from './map.service'; import { CreateMapRequest } from './dto/CreateMapRequest'; @@ -75,6 +76,10 @@ export class MapController { @Param('id') id: number, @Body('isPublic') isPublic: boolean, ) { + if (typeof isPublic !== 'boolean') { + throw new BadRequestException('공개 여부는 boolean 타입이어야 합니다.'); + } + await this.mapService.updateMapVisibility(id, isPublic); return { id, isPublic }; } From cbfb1b64715c1f123fc94aac8185883588035fed Mon Sep 17 00:00:00 2001 From: miensoap Date: Mon, 25 Nov 2024 13:31:30 +0900 Subject: [PATCH 012/139] =?UTF-8?q?feat:=20=EC=A7=80=EB=8F=84=EC=97=90=20?= =?UTF-8?q?=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=88=98=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80,=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EA=B0=80=20=EC=97=86=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=9D=91=EB=8B=B5=20#177?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/course.controller.ts | 4 ++++ backend/src/course/dto/UpdateCourseInfoRequest.ts | 14 ++++++++++---- backend/src/map/dto/UpdateMapInfoRequest.ts | 15 ++++++++++++--- backend/src/map/map.controller.ts | 4 ++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/backend/src/course/course.controller.ts b/backend/src/course/course.controller.ts index 7c978d1d..f322c8ea 100644 --- a/backend/src/course/course.controller.ts +++ b/backend/src/course/course.controller.ts @@ -74,6 +74,10 @@ export class CourseController { @Param('id') id: number, @Body() updateCourseInfoRequest: UpdateCourseInfoRequest, ) { + if (updateCourseInfoRequest.isEmpty()) { + throw new BadRequestException('수정할 정보가 없습니다.'); + } + await this.courseService.updateCourseInfo(id, updateCourseInfoRequest); return { id, ...updateCourseInfoRequest }; } diff --git a/backend/src/course/dto/UpdateCourseInfoRequest.ts b/backend/src/course/dto/UpdateCourseInfoRequest.ts index 695d7472..575c0ec4 100644 --- a/backend/src/course/dto/UpdateCourseInfoRequest.ts +++ b/backend/src/course/dto/UpdateCourseInfoRequest.ts @@ -1,13 +1,19 @@ -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsString, IsUrl, IsOptional } from 'class-validator'; export class UpdateCourseInfoRequest { + @IsOptional() @IsString() - @IsNotEmpty() - title: string; + title?: string; + @IsOptional() @IsString() description?: string; - @IsString() + @IsOptional() + @IsUrl() thumbnailUrl?: string; + + isEmpty(): boolean { + return !this.title && !this.description && !this.thumbnailUrl; + } } diff --git a/backend/src/map/dto/UpdateMapInfoRequest.ts b/backend/src/map/dto/UpdateMapInfoRequest.ts index 852264c8..7917554d 100644 --- a/backend/src/map/dto/UpdateMapInfoRequest.ts +++ b/backend/src/map/dto/UpdateMapInfoRequest.ts @@ -1,10 +1,19 @@ -import { IsString, IsNotEmpty } from 'class-validator'; +import { IsString, IsUrl, IsOptional } from 'class-validator'; export class UpdateMapInfoRequest { + @IsOptional() @IsString() - @IsNotEmpty() - title: string; + title?: string; + @IsOptional() @IsString() description?: string; + + @IsOptional() + @IsUrl() + thumbnailUrl?: string; + + isEmpty(): boolean { + return !this.title && !this.description && !this.thumbnailUrl; + } } diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 1b0e7756..bba223ab 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -67,6 +67,10 @@ export class MapController { @Param('id') id: number, @Body() updateMapInfoRequest: UpdateMapInfoRequest, ) { + if (updateMapInfoRequest.isEmpty()) { + throw new BadRequestException('수정할 정보가 없습니다.'); + } + await this.mapService.updateMapInfo(id, updateMapInfoRequest); return { id, ...updateMapInfoRequest }; } From 5346413ebdad52e7957c4b23740abcbfd5639268 Mon Sep 17 00:00:00 2001 From: miensoap Date: Mon, 25 Nov 2024 14:10:01 +0900 Subject: [PATCH 013/139] =?UTF-8?q?feat:=20=EB=A7=B5=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=90=9C=20=ED=95=80=20=EC=88=98=EC=A0=95=20API=20#17?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 색상/한줄평 수정 가능 --- .../src/map/dto/UpdatePlaceInMapRequest.ts | 16 +++++++ backend/src/map/entity/map-place.entity.ts | 20 ++++++-- backend/src/map/entity/map.entity.ts | 32 +++++++++---- .../exception/PlaceInMapNotFoundException.ts | 12 +++++ backend/src/map/map.controller.ts | 17 +++++++ backend/src/map/map.service.ts | 48 +++++++++++++------ 6 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 backend/src/map/dto/UpdatePlaceInMapRequest.ts create mode 100644 backend/src/map/exception/PlaceInMapNotFoundException.ts diff --git a/backend/src/map/dto/UpdatePlaceInMapRequest.ts b/backend/src/map/dto/UpdatePlaceInMapRequest.ts new file mode 100644 index 00000000..3076f870 --- /dev/null +++ b/backend/src/map/dto/UpdatePlaceInMapRequest.ts @@ -0,0 +1,16 @@ +import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { Color } from '@src/place/place.color.enum'; + +export class UpdatePlaceInMapRequest { + @IsOptional() + @IsEnum(Color) + color?: Color; + + @IsOptional() + @IsString() + comment?: string; + + isEmpty(): boolean { + return !this.color && !this.comment; + } +} diff --git a/backend/src/map/entity/map-place.entity.ts b/backend/src/map/entity/map-place.entity.ts index b52e6c0b..50f1a964 100644 --- a/backend/src/map/entity/map-place.entity.ts +++ b/backend/src/map/entity/map-place.entity.ts @@ -1,8 +1,8 @@ import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { BaseEntity } from '../../common/BaseEntity'; -import { Place } from '../../place/entity/place.entity'; +import { BaseEntity } from '@src/common/BaseEntity'; +import { Place } from '@src/place/entity/place.entity'; import { Map } from './map.entity'; -import { Color } from '../../place/place.color.enum'; +import { Color } from '@src/place/place.color.enum'; @Entity() export class MapPlace extends BaseEntity { @@ -35,4 +35,18 @@ export class MapPlace extends BaseEntity { place.description = description; return place; } + + /** + * 업데이트 정보를 가진 새 객체를 반환합니다. + * @param color + * @param comment + */ + update(color?: Color, comment?: string) { + return MapPlace.of( + this.placeId, + this.map, + color || this.color, + comment || this.description, + ); + } } diff --git a/backend/src/map/entity/map.entity.ts b/backend/src/map/entity/map.entity.ts index 32290d48..c84d07d5 100644 --- a/backend/src/map/entity/map.entity.ts +++ b/backend/src/map/entity/map.entity.ts @@ -1,8 +1,9 @@ import { Entity, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm'; -import { BaseEntity } from '../../common/BaseEntity'; -import { User } from '../../user/entity/user.entity'; +import { BaseEntity } from '@src/common/BaseEntity'; +import { User } from '@src/user/entity/user.entity'; import { MapPlace } from './map-place.entity'; -import { Color } from '../../place/place.color.enum'; +import { Color } from '@src/place/place.color.enum'; +import { PlaceInMapNotFoundException } from '@src/map/exception/PlaceInMapNotFoundException'; @Entity() export class Map extends BaseEntity { @@ -54,12 +55,11 @@ export class Map extends BaseEntity { this.mapPlaces.push(MapPlace.of(placeId, this, color, description)); } - deletePlace(placeId: number) { - this.mapPlaces = this.mapPlaces.filter((p) => p.placeId !== placeId); - } + getPlace(placeId: number) { + const mapPlace = this.mapPlaces.find((p) => p.placeId === placeId); + if (!mapPlace) throw new PlaceInMapNotFoundException(this.id, placeId); - hasPlace(placeId: number) { - return this.mapPlaces.some((p) => p.placeId === placeId); + return mapPlace; } async getPlacesWithComment() { @@ -71,4 +71,20 @@ export class Map extends BaseEntity { })), ); } + + hasPlace(placeId: number) { + return this.mapPlaces.some((p) => p.placeId === placeId); + } + + updatePlace(placeId: number, color?: Color, comment?: string) { + const updated = this.getPlace(placeId).update(color, comment); + + this.mapPlaces = this.mapPlaces.map((mapPlace) => + mapPlace.placeId === placeId ? updated : mapPlace, + ); + } + + deletePlace(placeId: number) { + this.mapPlaces = this.mapPlaces.filter((p) => p.placeId !== placeId); + } } diff --git a/backend/src/map/exception/PlaceInMapNotFoundException.ts b/backend/src/map/exception/PlaceInMapNotFoundException.ts new file mode 100644 index 00000000..57be2b13 --- /dev/null +++ b/backend/src/map/exception/PlaceInMapNotFoundException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class PlaceInMapNotFoundException extends BaseException { + constructor(mapId: number, mapPlaceId: number) { + super({ + code: 806, + message: `[${mapId}] 지도에 [${mapPlaceId}] 장소가 존재하지 않거나 삭제되었습니다.`, + status: HttpStatus.NOT_FOUND, + }); + } +} diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index bba223ab..35709de5 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -8,12 +8,14 @@ import { Param, Patch, BadRequestException, + Put, } from '@nestjs/common'; import { MapService } from './map.service'; import { CreateMapRequest } from './dto/CreateMapRequest'; import { UpdateMapInfoRequest } from './dto/UpdateMapInfoRequest'; import { AddPlaceToMapRequest } from './dto/AddPlaceToMapRequest'; import { ParseOptionalNumberPipe } from '@src/common/pipe/ParseOptionalNumberPipe'; +import { UpdatePlaceInMapRequest } from '@src/map/dto/UpdatePlaceInMapRequest'; @Controller('/maps') export class MapController { @@ -54,6 +56,21 @@ export class MapController { return await this.mapService.addPlace(id, placeId, color, comment); } + @Put('/:id/places/:placeId') + async updatePlaceInMap( + @Param('id') id: number, + @Param('placeId') placeId: number, + @Body() updatePlaceInMapRequest: UpdatePlaceInMapRequest, + ) { + if (updatePlaceInMapRequest.isEmpty()) { + throw new BadRequestException('수정할 정보가 없습니다.'); + } + const { color, comment } = updatePlaceInMapRequest; + + await this.mapService.updatePlace(id, placeId, color, comment); + return { mapId: id, placeId, color, comment }; + } + @Delete('/:id/places/:placeId') async deletePlaceFromMap( @Param('id') id: number, diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 8584d74e..7946c6f6 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -100,11 +100,6 @@ export class MapService { return this.mapRepository.update(id, { isPublic }); } - private async checkExists(id: number) { - if (!(await this.mapRepository.existById(id))) - throw new MapNotFoundException(id); - } - async addPlace( id: number, placeId: number, @@ -125,23 +120,48 @@ export class MapService { }; } - private async validatePlacesForMap(placeId: number, map: Map) { - if (!(await this.placeRepository.existById(placeId))) { - throw new InvalidPlaceToMapException(placeId); - } + async updatePlace( + id: number, + placeId: number, + color?: Color, + comment?: string, + ) { + const map = await this.getMapOrElseThrowNotFound(id); + map.updatePlace(placeId, color, comment); - if (map.hasPlace(placeId)) { - throw new DuplicatePlaceToMapException(placeId); - } + return this.mapRepository.save(map); } async deletePlace(id: number, placeId: number) { - const map = await this.mapRepository.findById(id); - if (!map) throw new MapNotFoundException(id); + const map = await this.getMapOrElseThrowNotFound(id); map.deletePlace(placeId); await this.mapRepository.save(map); return { deletedId: placeId }; } + + private async getMapOrElseThrowNotFound(id: number) { + const map = await this.mapRepository.findById(id); + if (!map) { + throw new MapNotFoundException(id); + } + return map; + } + + private async checkExists(id: number) { + if (!(await this.mapRepository.existById(id))) { + throw new MapNotFoundException(id); + } + } + + private async validatePlacesForMap(placeId: number, map: Map) { + if (!(await this.placeRepository.existById(placeId))) { + throw new InvalidPlaceToMapException(placeId); + } + + if (map.hasPlace(placeId)) { + throw new DuplicatePlaceToMapException(placeId); + } + } } From 6a39026370ff6bd392100fd0c565d18c23c6da81 Mon Sep 17 00:00:00 2001 From: miensoap Date: Mon, 25 Nov 2024 14:49:57 +0900 Subject: [PATCH 014/139] =?UTF-8?q?feat:=20=EC=BD=94=EC=8A=A4=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=90=9C=20=EC=9E=A5=EC=86=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20#178?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/course.controller.ts | 18 ++++++++++++ backend/src/course/course.service.ts | 29 +++++++++++++------ .../course/dto/UpdatePlaceInCourseRequest.ts | 11 +++++++ .../src/course/entity/course-place.entity.ts | 21 ++++++++++++-- backend/src/course/entity/course.entity.ts | 16 ++++++++++ .../PlaceInCourseNotFoundException.ts | 12 ++++++++ 6 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 backend/src/course/dto/UpdatePlaceInCourseRequest.ts create mode 100644 backend/src/course/exception/PlaceInCourseNotFoundException.ts diff --git a/backend/src/course/course.controller.ts b/backend/src/course/course.controller.ts index f322c8ea..627f7e23 100644 --- a/backend/src/course/course.controller.ts +++ b/backend/src/course/course.controller.ts @@ -19,6 +19,7 @@ import { JwtAuthGuard } from '../auth/JwtAuthGuard'; import { AuthUser } from '../auth/AuthUser.decorator'; import { CoursePermissionGuard } from './guards/CoursePermissionGuard'; import { ParseOptionalNumberPipe } from '@src/common/pipe/ParseOptionalNumberPipe'; +import { UpdatePlaceInCourseRequest } from '@src/course/dto/UpdatePlaceInCourseRequest'; @Controller('/courses') export class CourseController { @@ -68,6 +69,23 @@ export class CourseController { ); } + @Put('/:id/places/:placeId') + @UseGuards(JwtAuthGuard, CoursePermissionGuard) + async updatePlaceInCourse( + @Param('id') id: number, + @Param('placeId') placeId: number, + @Body() updatePlaceInCourseRequest: UpdatePlaceInCourseRequest, + ) { + const { comment } = updatePlaceInCourseRequest; + if (updatePlaceInCourseRequest.isEmpty()) { + throw new BadRequestException('수정할 정보가 없습니다.'); + } + + await this.courseService.updatePlace(id, placeId, comment); + + return { courseId: id, placeId: placeId, comment: comment }; + } + @Patch('/:id/info') @UseGuards(JwtAuthGuard, CoursePermissionGuard) async updateCourseInfo( diff --git a/backend/src/course/course.service.ts b/backend/src/course/course.service.ts index 2504c38a..990ed4dc 100644 --- a/backend/src/course/course.service.ts +++ b/backend/src/course/course.service.ts @@ -56,8 +56,7 @@ export class CourseService { } async getCourseById(id: number) { - const course = await this.courseRepository.findById(id); - if (!course) throw new CourseNotFoundException(id); + const course = await this.getCourseOrElseThrowNotFound(id); return await CourseDetailResponse.from(course); } @@ -102,18 +101,12 @@ export class CourseService { return this.courseRepository.updateIsPublicById(id, isPublic); } - private async validateCourseExistsById(id: number) { - if (!(await this.courseRepository.existById(id))) - throw new CourseNotFoundException(id); - } - @Transactional() async setPlacesOfCourse( id: number, setPlacesOfCourseRequest: SetPlacesOfCourseRequest, ) { - const course = await this.courseRepository.findById(id); - if (!course) throw new CourseNotFoundException(id); + const course = await this.getCourseOrElseThrowNotFound(id); await this.validatePlacesForCourse( setPlacesOfCourseRequest.places.map((p) => p.placeId), @@ -128,6 +121,24 @@ export class CourseService { }; } + async updatePlace(id: number, placeId: number, comment?: string) { + const course = await this.getCourseOrElseThrowNotFound(id); + + course.updatePlace(placeId, comment); + return this.courseRepository.save(course); + } + + private async getCourseOrElseThrowNotFound(id: number) { + const course = await this.courseRepository.findById(id); + if (!course) throw new CourseNotFoundException(id); + return course; + } + + private async validateCourseExistsById(id: number) { + if (!(await this.courseRepository.existById(id))) + throw new CourseNotFoundException(id); + } + private async validatePlacesForCourse(placeIds: number[]) { const notExistsPlaceIds = await Promise.all( placeIds.map(async (placeId) => { diff --git a/backend/src/course/dto/UpdatePlaceInCourseRequest.ts b/backend/src/course/dto/UpdatePlaceInCourseRequest.ts new file mode 100644 index 00000000..67630ca7 --- /dev/null +++ b/backend/src/course/dto/UpdatePlaceInCourseRequest.ts @@ -0,0 +1,11 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class UpdatePlaceInCourseRequest { + @IsOptional() + @IsString() + comment?: string; + + isEmpty(): boolean { + return !this.comment; + } +} diff --git a/backend/src/course/entity/course-place.entity.ts b/backend/src/course/entity/course-place.entity.ts index a621deab..fb7961ca 100644 --- a/backend/src/course/entity/course-place.entity.ts +++ b/backend/src/course/entity/course-place.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; -import { BaseEntity } from '../../common/BaseEntity'; -import { Place } from '../../place/entity/place.entity'; +import { BaseEntity } from '@src/common/BaseEntity'; +import { Place } from '@src/place/entity/place.entity'; import { Course } from './course.entity'; @Entity() @@ -8,6 +8,9 @@ export class CoursePlace extends BaseEntity { @Column() order: number; + @Column() + placeId: number; + @ManyToOne(() => Place, { onDelete: 'CASCADE', lazy: true }) @JoinColumn({ name: 'place_id' }) place: Promise; @@ -31,8 +34,22 @@ export class CoursePlace extends BaseEntity { const place = new CoursePlace(); place.course = course; place.order = order; + place.placeId = placeId; place.place = Promise.resolve({ id: placeId } as Place); place.description = description; return place; } + + /** + * 업데이트 정보를 가진 새 객체를 반환합니다. + * @param description + */ + update(description?: string) { + return CoursePlace.of( + this.order, + this.placeId, + this.course, + description || this.description, + ); + } } diff --git a/backend/src/course/entity/course.entity.ts b/backend/src/course/entity/course.entity.ts index e8c54301..35976c03 100644 --- a/backend/src/course/entity/course.entity.ts +++ b/backend/src/course/entity/course.entity.ts @@ -3,6 +3,7 @@ import { BaseEntity } from '../../common/BaseEntity'; import { User } from '../../user/entity/user.entity'; import { CoursePlace } from './course-place.entity'; import { SetPlacesOfCourseRequestItem } from '../dto/AddPlaceToCourseRequest'; +import { PlaceInCourseNotFoundException } from '@src/course/exception/PlaceInCourseNotFoundException'; @Entity() export class Course extends BaseEntity { @@ -58,6 +59,21 @@ export class Course extends BaseEntity { }); } + getPlace(placeId: number) { + const coursePlace = this.coursePlaces.find((cp) => cp.placeId === placeId); + if (!coursePlace) { + throw new PlaceInCourseNotFoundException(this.id, placeId); + } + return coursePlace; + } + + updatePlace(placeId: number, comment?: string) { + const updated = this.getPlace(placeId).update(comment); + this.coursePlaces = this.coursePlaces.map((coursePlace) => + coursePlace.placeId === placeId ? updated : coursePlace, + ); + } + async getPlacesWithComment() { const coursePlaces = this.coursePlaces.sort((a, b) => a.order - b.order); return await Promise.all( diff --git a/backend/src/course/exception/PlaceInCourseNotFoundException.ts b/backend/src/course/exception/PlaceInCourseNotFoundException.ts new file mode 100644 index 00000000..b53ea06e --- /dev/null +++ b/backend/src/course/exception/PlaceInCourseNotFoundException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class PlaceInCourseNotFoundException extends BaseException { + constructor(mapId: number, mapPlaceId: number) { + super({ + code: 906, + message: `[${mapId}] 코스에 [${mapPlaceId}] 장소가 존재하지 않거나 삭제되었습니다.`, + status: HttpStatus.NOT_FOUND, + }); + } +} From e488c8375af404808a868596764a00192d51f4c6 Mon Sep 17 00:00:00 2001 From: Soap Date: Mon, 25 Nov 2024 14:53:15 +0900 Subject: [PATCH 015/139] =?UTF-8?q?feat:=20PUT=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=EC=A0=81=EC=9A=A9=20#178?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/course.service.ts | 1 + backend/src/map/map.service.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/backend/src/course/course.service.ts b/backend/src/course/course.service.ts index 990ed4dc..603de300 100644 --- a/backend/src/course/course.service.ts +++ b/backend/src/course/course.service.ts @@ -121,6 +121,7 @@ export class CourseService { }; } + @Transactional() async updatePlace(id: number, placeId: number, comment?: string) { const course = await this.getCourseOrElseThrowNotFound(id); diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 7946c6f6..98068967 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -13,6 +13,7 @@ import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMap import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { UserRole } from '@src/user/user.role'; +import { Transactional } from 'typeorm-transactional'; @Injectable() export class MapService { @@ -120,6 +121,7 @@ export class MapService { }; } + @Transactional() async updatePlace( id: number, placeId: number, From 2fc3b2bad63b5a47940f25ece824d416a6d47d0a Mon Sep 17 00:00:00 2001 From: miensoap Date: Mon, 25 Nov 2024 17:19:44 +0900 Subject: [PATCH 016/139] =?UTF-8?q?refactor:=20=EC=8D=B8=EB=84=A4=EC=9D=BC?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20=EC=B2=98=EB=A6=AC=20=EB=8D=B0?= =?UTF-8?q?=EC=BD=94=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=A0=81=EC=9A=A9=20#17?= =?UTF-8?q?7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/common/consts.ts | 3 +++ backend/src/common/decorator/ReplaceEmptyWith.ts | 9 +++++++++ backend/src/course/dto/CreateCourseRequest.ts | 7 +++++-- backend/src/course/dto/UpdateCourseInfoRequest.ts | 3 +++ backend/src/course/entity/course.entity.ts | 4 +++- backend/src/map/dto/CreateMapRequest.ts | 7 +++++-- backend/src/map/dto/MapDetailResponse.ts | 2 +- backend/src/map/dto/MapListResponse.ts | 7 ++----- backend/src/map/dto/UpdateMapInfoRequest.ts | 3 +++ 9 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 backend/src/common/consts.ts create mode 100644 backend/src/common/decorator/ReplaceEmptyWith.ts diff --git a/backend/src/common/consts.ts b/backend/src/common/consts.ts new file mode 100644 index 00000000..6e5f579f --- /dev/null +++ b/backend/src/common/consts.ts @@ -0,0 +1,3 @@ +// Todo. 오브젝트 스토리지에 실제 이미지 저장 후 수정 +export const DEFAULT_THUMBNAIL_URL = + 'https://avatars.githubusercontent.com/u/87180146?v=4'; diff --git a/backend/src/common/decorator/ReplaceEmptyWith.ts b/backend/src/common/decorator/ReplaceEmptyWith.ts new file mode 100644 index 00000000..a639f5e4 --- /dev/null +++ b/backend/src/common/decorator/ReplaceEmptyWith.ts @@ -0,0 +1,9 @@ +import { Transform } from 'class-transformer'; + +/** + * 빈 문자열을 기본값으로 변환합니다. + * @param defaultValue + */ +export function ReplaceEmptyWith(defaultValue: string): PropertyDecorator { + return Transform(({ value }) => (value === '' ? defaultValue : value)); +} diff --git a/backend/src/course/dto/CreateCourseRequest.ts b/backend/src/course/dto/CreateCourseRequest.ts index 075f0bc7..36cc1490 100644 --- a/backend/src/course/dto/CreateCourseRequest.ts +++ b/backend/src/course/dto/CreateCourseRequest.ts @@ -4,9 +4,11 @@ import { IsNotEmpty, IsString, IsUrl, - ValidateIf, + IsOptional, } from 'class-validator'; import { Course } from '../entity/course.entity'; +import { ReplaceEmptyWith } from '@src/common/decorator/ReplaceEmptyWith'; +import { DEFAULT_THUMBNAIL_URL } from '@src/common/consts'; export class CreateCourseRequest { @IsString() @@ -20,7 +22,8 @@ export class CreateCourseRequest { @IsString() description?: string; - @ValidateIf((object, value) => value !== '') + @ReplaceEmptyWith(DEFAULT_THUMBNAIL_URL) + @IsOptional() @IsUrl() thumbnailUrl?: string; diff --git a/backend/src/course/dto/UpdateCourseInfoRequest.ts b/backend/src/course/dto/UpdateCourseInfoRequest.ts index 575c0ec4..23d3cd44 100644 --- a/backend/src/course/dto/UpdateCourseInfoRequest.ts +++ b/backend/src/course/dto/UpdateCourseInfoRequest.ts @@ -1,4 +1,6 @@ import { IsString, IsUrl, IsOptional } from 'class-validator'; +import { ReplaceEmptyWith } from '@src/common/decorator/ReplaceEmptyWith'; +import { DEFAULT_THUMBNAIL_URL } from '@src/common/consts'; export class UpdateCourseInfoRequest { @IsOptional() @@ -9,6 +11,7 @@ export class UpdateCourseInfoRequest { @IsString() description?: string; + @ReplaceEmptyWith(DEFAULT_THUMBNAIL_URL) @IsOptional() @IsUrl() thumbnailUrl?: string; diff --git a/backend/src/course/entity/course.entity.ts b/backend/src/course/entity/course.entity.ts index 35976c03..75a23b10 100644 --- a/backend/src/course/entity/course.entity.ts +++ b/backend/src/course/entity/course.entity.ts @@ -60,7 +60,9 @@ export class Course extends BaseEntity { } getPlace(placeId: number) { - const coursePlace = this.coursePlaces.find((cp) => cp.placeId === placeId); + const coursePlace = this.coursePlaces.find( + (coursePlace) => coursePlace.placeId === placeId, + ); if (!coursePlace) { throw new PlaceInCourseNotFoundException(this.id, placeId); } diff --git a/backend/src/map/dto/CreateMapRequest.ts b/backend/src/map/dto/CreateMapRequest.ts index a325b83f..6cb9edec 100644 --- a/backend/src/map/dto/CreateMapRequest.ts +++ b/backend/src/map/dto/CreateMapRequest.ts @@ -5,8 +5,10 @@ import { IsNotEmpty, IsUrl, IsBoolean, - ValidateIf, + IsOptional, } from 'class-validator'; +import { ReplaceEmptyWith } from '@src/common/decorator/ReplaceEmptyWith'; +import { DEFAULT_THUMBNAIL_URL } from '@src/common/consts'; export class CreateMapRequest { @IsString() @@ -20,7 +22,8 @@ export class CreateMapRequest { @IsString() description?: string; - @ValidateIf((object, value) => value !== '') + @ReplaceEmptyWith(DEFAULT_THUMBNAIL_URL) + @IsOptional() @IsUrl() thumbnailUrl?: string; diff --git a/backend/src/map/dto/MapDetailResponse.ts b/backend/src/map/dto/MapDetailResponse.ts index 4b7d30b6..095a8280 100644 --- a/backend/src/map/dto/MapDetailResponse.ts +++ b/backend/src/map/dto/MapDetailResponse.ts @@ -1,7 +1,7 @@ import { Map } from '../entity/map.entity'; import { UserIconResponse } from '../../user/dto/UserIconResponse'; import { PlaceListResponse } from '../../place/dto/PlaceListResponse'; -import { DEFAULT_THUMBNAIL_URL } from './MapListResponse'; +import { DEFAULT_THUMBNAIL_URL } from '@src/common/consts'; export class MapDetailResponse { constructor( diff --git a/backend/src/map/dto/MapListResponse.ts b/backend/src/map/dto/MapListResponse.ts index b4e5c04a..e80c434e 100644 --- a/backend/src/map/dto/MapListResponse.ts +++ b/backend/src/map/dto/MapListResponse.ts @@ -1,9 +1,6 @@ import { Map } from '../entity/map.entity'; -import { UserIconResponse } from '../../user/dto/UserIconResponse'; - -// Todo. 오브젝트 스토리지에 실제 이미지 저장 후 수정 -export const DEFAULT_THUMBNAIL_URL = - 'https://avatars.githubusercontent.com/u/87180146?v=4'; +import { UserIconResponse } from '@src/user/dto/UserIconResponse'; +import { DEFAULT_THUMBNAIL_URL } from '@src/common/consts'; export class MapListResponse { constructor( diff --git a/backend/src/map/dto/UpdateMapInfoRequest.ts b/backend/src/map/dto/UpdateMapInfoRequest.ts index 7917554d..074c9714 100644 --- a/backend/src/map/dto/UpdateMapInfoRequest.ts +++ b/backend/src/map/dto/UpdateMapInfoRequest.ts @@ -1,4 +1,6 @@ import { IsString, IsUrl, IsOptional } from 'class-validator'; +import { ReplaceEmptyWith } from '@src/common/decorator/ReplaceEmptyWith'; +import { DEFAULT_THUMBNAIL_URL } from '@src/common/consts'; export class UpdateMapInfoRequest { @IsOptional() @@ -9,6 +11,7 @@ export class UpdateMapInfoRequest { @IsString() description?: string; + @ReplaceEmptyWith(DEFAULT_THUMBNAIL_URL) @IsOptional() @IsUrl() thumbnailUrl?: string; From 3557c7185bfadd888b904c3920c9dba8196b4c76 Mon Sep 17 00:00:00 2001 From: miensoap Date: Mon, 25 Nov 2024 17:30:54 +0900 Subject: [PATCH 017/139] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=EA=B0=80=20=ED=8F=AC=ED=95=A8=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EC=9A=94=EC=B2=AD=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EA=B3=B5=ED=86=B5=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=98=88=EC=99=B8=20=EC=82=AC=EC=9A=A9=20#178?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/common/exception/EmptyRequestException.ts | 12 ++++++++++++ backend/src/course/course.controller.ts | 5 +++-- backend/src/map/map.controller.ts | 5 +++-- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 backend/src/common/exception/EmptyRequestException.ts diff --git a/backend/src/common/exception/EmptyRequestException.ts b/backend/src/common/exception/EmptyRequestException.ts new file mode 100644 index 00000000..7b7798a5 --- /dev/null +++ b/backend/src/common/exception/EmptyRequestException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class EmptyRequestException extends BaseException { + constructor(action: string = '작업') { + super({ + code: 4002, + message: `${action}에 필요한 정보가 없습니다.`, + status: HttpStatus.BAD_REQUEST, + }); + } +} diff --git a/backend/src/course/course.controller.ts b/backend/src/course/course.controller.ts index 627f7e23..2d41333f 100644 --- a/backend/src/course/course.controller.ts +++ b/backend/src/course/course.controller.ts @@ -20,6 +20,7 @@ import { AuthUser } from '../auth/AuthUser.decorator'; import { CoursePermissionGuard } from './guards/CoursePermissionGuard'; import { ParseOptionalNumberPipe } from '@src/common/pipe/ParseOptionalNumberPipe'; import { UpdatePlaceInCourseRequest } from '@src/course/dto/UpdatePlaceInCourseRequest'; +import { EmptyRequestException } from '@src/common/exception/EmptyRequestException'; @Controller('/courses') export class CourseController { @@ -78,7 +79,7 @@ export class CourseController { ) { const { comment } = updatePlaceInCourseRequest; if (updatePlaceInCourseRequest.isEmpty()) { - throw new BadRequestException('수정할 정보가 없습니다.'); + throw new EmptyRequestException('수정'); } await this.courseService.updatePlace(id, placeId, comment); @@ -93,7 +94,7 @@ export class CourseController { @Body() updateCourseInfoRequest: UpdateCourseInfoRequest, ) { if (updateCourseInfoRequest.isEmpty()) { - throw new BadRequestException('수정할 정보가 없습니다.'); + throw new EmptyRequestException('수정'); } await this.courseService.updateCourseInfo(id, updateCourseInfoRequest); diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 35709de5..0e0d70a4 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -16,6 +16,7 @@ import { UpdateMapInfoRequest } from './dto/UpdateMapInfoRequest'; import { AddPlaceToMapRequest } from './dto/AddPlaceToMapRequest'; import { ParseOptionalNumberPipe } from '@src/common/pipe/ParseOptionalNumberPipe'; import { UpdatePlaceInMapRequest } from '@src/map/dto/UpdatePlaceInMapRequest'; +import { EmptyRequestException } from '@src/common/exception/EmptyRequestException'; @Controller('/maps') export class MapController { @@ -63,7 +64,7 @@ export class MapController { @Body() updatePlaceInMapRequest: UpdatePlaceInMapRequest, ) { if (updatePlaceInMapRequest.isEmpty()) { - throw new BadRequestException('수정할 정보가 없습니다.'); + throw new EmptyRequestException('수정'); } const { color, comment } = updatePlaceInMapRequest; @@ -85,7 +86,7 @@ export class MapController { @Body() updateMapInfoRequest: UpdateMapInfoRequest, ) { if (updateMapInfoRequest.isEmpty()) { - throw new BadRequestException('수정할 정보가 없습니다.'); + throw new EmptyRequestException('수정'); } await this.mapService.updateMapInfo(id, updateMapInfoRequest); From 010c6716664371164d2db81255fffcbf71b29e6d Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:22:06 +0900 Subject: [PATCH 018/139] =?UTF-8?q?fix:=20map=20controller=20=EC=97=90=20?= =?UTF-8?q?=EC=99=84=EC=84=B1=EB=90=9C=20user=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.controller.ts | 20 ++++++++++++++------ backend/src/map/map.service.ts | 7 +------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 0e0d70a4..51592871 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -9,6 +9,7 @@ import { Patch, BadRequestException, Put, + UseGuards, } from '@nestjs/common'; import { MapService } from './map.service'; import { CreateMapRequest } from './dto/CreateMapRequest'; @@ -17,6 +18,8 @@ import { AddPlaceToMapRequest } from './dto/AddPlaceToMapRequest'; import { ParseOptionalNumberPipe } from '@src/common/pipe/ParseOptionalNumberPipe'; import { UpdatePlaceInMapRequest } from '@src/map/dto/UpdatePlaceInMapRequest'; import { EmptyRequestException } from '@src/common/exception/EmptyRequestException'; +import { AuthUser } from '@src/auth/AuthUser.decorator'; +import { JwtAuthGuard } from '@src/auth/JwtAuthGuard'; @Controller('/maps') export class MapController { @@ -31,10 +34,10 @@ export class MapController { return await this.mapService.searchMap(query, page, limit); } + @UseGuards(JwtAuthGuard) @Get('/my') - async getMyMapList() { - const userId = 1; // Todo. 로그인 기능 완성 후 수정 - return await this.mapService.getOwnMaps(userId); + async getMyMapList(@AuthUser() user: AuthUser) { + return await this.mapService.getOwnMaps(user.userId); } @Get('/:id') @@ -42,12 +45,16 @@ export class MapController { return await this.mapService.getMapById(id); } + @UseGuards(JwtAuthGuard) @Post() - async createMap(@Body() createMapRequest: CreateMapRequest) { - const userId = 1; // Todo. 로그인 기능 완성 후 수정 - return await this.mapService.createMap(userId, createMapRequest); + async createMap( + @Body() createMapRequest: CreateMapRequest, + @AuthUser() user: AuthUser, + ) { + return await this.mapService.createMap(user.userId, createMapRequest); } + @UseGuards(JwtAuthGuard) @Post('/:id/places') async addPlaceToMap( @Param('id') id: number, @@ -72,6 +79,7 @@ export class MapController { return { mapId: id, placeId, color, comment }; } + @UseGuards(JwtAuthGuard) @Delete('/:id/places/:placeId') async deletePlaceFromMap( @Param('id') id: number, diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 98068967..4e197e60 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -21,12 +21,7 @@ export class MapService { private readonly mapRepository: MapRepository, private readonly userRepository: UserRepository, private readonly placeRepository: PlaceRepository, - ) { - // Todo. 로그인 기능 완성 후 제거 - const testUser = new User('test', 'test', 'test', UserRole.MEMBER); - testUser.id = 1; - this.userRepository.upsert(testUser, { conflictPaths: ['id'] }); - } + ) {} // Todo. 작성자명 등 ... 검색 조건 추가 // Todo. fix : public 으로 조회해서 페이지마다 수 일정하게. (현재는 한 페이지에 10개 미만인 경우 존재) From c83c3b8a4c0849b6f94a8863c6450039fce3d589 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:02:33 +0900 Subject: [PATCH 019/139] =?UTF-8?q?fix:=20=EC=A7=80=EB=8F=84=EC=9D=98=20co?= =?UTF-8?q?ntroller=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80=20/=20Permis?= =?UTF-8?q?sion,=20isPublic,=20=EC=9C=A0=EC=A0=80=20=EC=A1=B4=EC=9E=AC=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/exception/MapPermissionException.ts | 12 ++++++++ backend/src/map/exception/TypeException.ts | 12 ++++++++ .../map/exception/UserNotFoundException.ts | 12 ++++++++ backend/src/map/guards/MapPermissionGuard.ts | 20 +++++++++++++ backend/src/map/map.controller.ts | 7 ++++- backend/src/map/map.service.ts | 28 +++++++++++++++++-- 6 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 backend/src/map/exception/MapPermissionException.ts create mode 100644 backend/src/map/exception/TypeException.ts create mode 100644 backend/src/map/exception/UserNotFoundException.ts create mode 100644 backend/src/map/guards/MapPermissionGuard.ts diff --git a/backend/src/map/exception/MapPermissionException.ts b/backend/src/map/exception/MapPermissionException.ts new file mode 100644 index 00000000..8fc8a414 --- /dev/null +++ b/backend/src/map/exception/MapPermissionException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class MapPermissionException extends BaseException { + constructor(id: number) { + super({ + code: 803, + message: `지도 ${id} 에 대한 권한이 없습니다.`, + status: HttpStatus.FORBIDDEN, + }); + } +} diff --git a/backend/src/map/exception/TypeException.ts b/backend/src/map/exception/TypeException.ts new file mode 100644 index 00000000..09127fdf --- /dev/null +++ b/backend/src/map/exception/TypeException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class TypeException extends BaseException { + constructor(where: string, toType: string, currentType: string) { + super({ + code: 804, + message: `${where} must be ${toType} not ${currentType}.`, + status: HttpStatus.BAD_REQUEST, + }); + } +} diff --git a/backend/src/map/exception/UserNotFoundException.ts b/backend/src/map/exception/UserNotFoundException.ts new file mode 100644 index 00000000..a79364c0 --- /dev/null +++ b/backend/src/map/exception/UserNotFoundException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class UserNotFoundException extends BaseException { + constructor(id: number) { + super({ + code: 803, + message: `id:${id} 유저가 존재하지 않거나 삭제되었습니다.`, + status: HttpStatus.NOT_FOUND, + }); + } +} diff --git a/backend/src/map/guards/MapPermissionGuard.ts b/backend/src/map/guards/MapPermissionGuard.ts new file mode 100644 index 00000000..e7d2b3bb --- /dev/null +++ b/backend/src/map/guards/MapPermissionGuard.ts @@ -0,0 +1,20 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { MapService } from '@src/map/map.service'; +import { MapPermissionException } from '@src/map/exception/MapPermissionException'; + +@Injectable() +export class MapPermissionGuard implements CanActivate { + constructor(private readonly mapService: MapService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const mapId = Number(request.params.id); + const userId = Number(request.user.userId); + + const map = await this.mapService.getMapById(mapId); + if (map.id !== userId) { + throw new MapPermissionException(mapId); + } + return true; + } +} diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 51592871..9587332e 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -20,6 +20,7 @@ import { UpdatePlaceInMapRequest } from '@src/map/dto/UpdatePlaceInMapRequest'; import { EmptyRequestException } from '@src/common/exception/EmptyRequestException'; import { AuthUser } from '@src/auth/AuthUser.decorator'; import { JwtAuthGuard } from '@src/auth/JwtAuthGuard'; +import { MapPermissionGuard } from '@src/map/guards/MapPermissionGuard'; @Controller('/maps') export class MapController { @@ -54,7 +55,7 @@ export class MapController { return await this.mapService.createMap(user.userId, createMapRequest); } - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, MapPermissionGuard) @Post('/:id/places') async addPlaceToMap( @Param('id') id: number, @@ -64,6 +65,7 @@ export class MapController { return await this.mapService.addPlace(id, placeId, color, comment); } + @UseGuards(JwtAuthGuard, MapPermissionGuard) @Put('/:id/places/:placeId') async updatePlaceInMap( @Param('id') id: number, @@ -88,6 +90,7 @@ export class MapController { return await this.mapService.deletePlace(id, placeId); } + @UseGuards(JwtAuthGuard, MapPermissionGuard) @Patch('/:id/info') async updateMapInfo( @Param('id') id: number, @@ -101,6 +104,7 @@ export class MapController { return { id, ...updateMapInfoRequest }; } + @UseGuards(JwtAuthGuard, MapPermissionGuard) @Patch('/:id/visibility') async updateMapVisibility( @Param('id') id: number, @@ -114,6 +118,7 @@ export class MapController { return { id, isPublic }; } + @UseGuards(JwtAuthGuard, MapPermissionGuard) @Delete('/:id') async deleteMap(@Param('id') id: number) { return await this.mapService.deleteMap(id); diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 4e197e60..8d3174c4 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -45,6 +45,7 @@ export class MapService { async getOwnMaps(userId: number, page: number = 1, pageSize: number = 10) { // Todo. 그룹 기능 추가 + await this.checkUserExist(userId); const totalCount = await this.mapRepository.count({ where: { user: { id: userId } }, }); @@ -70,9 +71,9 @@ export class MapService { } async createMap(userId: number, createMapForm: CreateMapRequest) { + await this.checkUserExist(userId); const user = { id: userId } as User; const map = createMapForm.toEntity(user); - return { id: (await this.mapRepository.save(map)).id }; } @@ -92,7 +93,7 @@ export class MapService { async updateMapVisibility(id: number, isPublic: boolean) { await this.checkExists(id); - + await this.checkPublicType(isPublic); return this.mapRepository.update(id, { isPublic }); } @@ -161,4 +162,27 @@ export class MapService { throw new DuplicatePlaceToMapException(placeId); } } + + private async checkUserExist(userId: number) { + if (!(await this.userRepository.findById(userId))) { + throw new UserNotFoundException(userId); + } + } + + private async checkPublicType(isPublic: any) { + if (typeof isPublic === 'boolean') { + return; + } + throw new TypeException('isPublic', 'boolean', typeof isPublic); + } + + async deletePlace(id: number, placeId: number) { + const map = await this.mapRepository.findById(id); + if (!map) throw new MapNotFoundException(id); + + map.deletePlace(placeId); + await this.mapRepository.save(map); + + return { deletedId: placeId }; + } } From 79ad42f4e0b6674b68f2e963a4bc3a5c0c664659 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:03:04 +0900 Subject: [PATCH 020/139] =?UTF-8?q?test:=20=EC=A7=80=EB=8F=84=20service=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.service.test.ts | 345 +++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 backend/test/map/map.service.test.ts diff --git a/backend/test/map/map.service.test.ts b/backend/test/map/map.service.test.ts new file mode 100644 index 00000000..c9c8f7ea --- /dev/null +++ b/backend/test/map/map.service.test.ts @@ -0,0 +1,345 @@ +import { MapService } from '@src/map/map.service'; +import { MapRepository } from '@src/map/map.repository'; +import { PlaceRepository } from '@src/place/place.repository'; +import { User } from '@src/user/entity/user.entity'; +import { UserFixture } from '@test/user/fixture/user.fixture'; +import { + createPublicMaps, + createPublicMapsWithTitle, +} from '@test/map/map.test.util'; +import { Map } from '@src/map/entity/map.entity'; +import { MapListResponse } from '@src/map/dto/MapListResponse'; +import { MapNotFoundException } from '@src/map/exception/MapNotFoundException'; +import { MapDetailResponse } from '@src/map/dto/MapDetailResponse'; +import { CreateMapRequest } from '@src/map/dto/CreateMapRequest'; +import { Color } from '@src/place/place.color.enum'; +import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMapException'; +import { DuplicatePlaceToMapException } from '@src/map/exception/DuplicatePlaceToMapException'; +import { UserRepository } from '@src/user/user.repository'; + +describe('MapService 테스트', () => { + let mapService: MapService; + let mapRepository: jest.Mocked; + let userRepository: jest.Mocked; + let placeRepository: jest.Mocked; + let fakeUser1: User; + let page: number; + let pageSize: number; + beforeAll(() => { + fakeUser1 = { + id: 1, + ...UserFixture.createUser({ oauthId: 'abc' }), + }; + [page, pageSize] = [1, 10]; + }); + beforeEach(async () => { + mapRepository = { + searchByTitleQuery: jest.fn(), + findAll: jest.fn(), + count: jest.fn(), + findById: jest.fn(), + findByUserId: jest.fn(), + save: jest.fn(), + softDelete: jest.fn(), + update: jest.fn(), + existById: jest.fn(), + } as unknown as jest.Mocked; + userRepository = { + findByProviderAndOauthId: jest.fn(), + createUser: jest.fn(), + findById: jest.fn(), + existById: jest.fn(), + } as unknown as jest.Mocked; + placeRepository = { + findByGooglePlaceId: jest.fn(), + findAll: jest.fn(), + searchByNameOrAddressQuery: jest.fn(), + existById: jest.fn(), + } as unknown as jest.Mocked; + mapService = new MapService(mapRepository, userRepository, placeRepository); + userRepository.existById.mockResolvedValue(true); + }); + describe('searchMap 메소드 테스트', () => { + it('파라미터 중 query 가 없을 경우 공개된 모든 지도를 반환한다.', async () => { + const mockMaps: Map[] = createPublicMaps(5, fakeUser1).map((map) => { + return { + ...map, + mapPlaces: [], + }; + }); + const spyFindAll = mapRepository.findAll.mockResolvedValue(mockMaps); + const spyCount = mapRepository.count.mockResolvedValue(mockMaps.length); + + const result = await mapService.searchMap(undefined, 1, 10); + const expectedMaps = await Promise.all( + mockMaps.map(async (mockMap) => await MapListResponse.from(mockMap)), + ); + expect(spyFindAll).toHaveBeenCalledWith(page, pageSize); + expect(result.maps).toEqual( + expect.arrayContaining( + expectedMaps.map((map) => expect.objectContaining(map)), + ), + ); + expect(spyCount).toHaveBeenCalledWith({ + where: { title: undefined, isPublic: true }, + }); + expect(result.currentPage).toEqual(page); + expect(result.totalPages).toEqual(Math.ceil(mockMaps.length / pageSize)); + }); + it('파라미터 중 쿼리(지도 title)가 있을 경우 해당 제목을 가진 지도들을 반환한다', async () => { + const searchTitle = 'cool'; + const mockCoolMaps: Map[] = createPublicMapsWithTitle( + 5, + fakeUser1, + 'cool map', + ).map((map) => { + return { + ...map, + mapPlaces: [], + }; + }); + const spySearchByTitleQuery = jest + .spyOn(mapRepository, 'searchByTitleQuery') + .mockResolvedValue(mockCoolMaps); + const spyCount = jest + .spyOn(mapRepository, 'count') + .mockResolvedValue(mockCoolMaps.length); + const expectedMaps = await Promise.all( + mockCoolMaps.map((map) => MapListResponse.from(map)), + ); + const result = await mapService.searchMap(searchTitle, 1, 10); + expect(spySearchByTitleQuery).toHaveBeenCalledWith( + 'cool', + page, + pageSize, + ); + expect(spyCount).toHaveBeenCalledWith({ + where: { title: 'cool', isPublic: true }, + }); + expect(result.maps).toEqual( + expect.arrayContaining( + expectedMaps.map((map) => expect.objectContaining(map)), + ), + ); + expect(result.currentPage).toEqual(page); + expect(result.totalPages).toEqual( + Math.ceil(mockCoolMaps.length / pageSize), + ); + }); + }); + describe('getOwnMaps 메소드 테스트', () => { + it('유저 아이디를 파라미터로 받아서 해당 유저의 지도를 반환한다.', async () => { + const fakeUserMaps = createPublicMaps(5, fakeUser1).map((map) => { + return { + ...map, + mapPlaces: [], + }; + }); + const spyFindUserById = + mapRepository.findByUserId.mockResolvedValue(fakeUserMaps); + const spyCount = mapRepository.count.mockResolvedValue( + fakeUserMaps.length, + ); + userRepository.findById.mockResolvedValue(fakeUser1); + const expectedMaps = await Promise.all( + fakeUserMaps.map((fakeUserMap) => MapListResponse.from(fakeUserMap)), + ); + const result = await mapService.getOwnMaps(fakeUser1.id); + expect(spyFindUserById).toHaveBeenCalledWith( + fakeUser1.id, + page, + pageSize, + ); + expect(spyCount).toHaveBeenCalledWith({ + where: { user: { id: fakeUser1.id } }, + }); + expect(result.maps).toEqual( + expect.arrayContaining( + expectedMaps.map((map) => expect.objectContaining(map)), + ), + ); + expect(result.totalPages).toEqual( + Math.ceil(fakeUserMaps.length / pageSize), + ); + expect(result.currentPage).toEqual(page); + }); + }); + describe('getMapById 메소드 테스트', () => { + it('파라미터로 받은 mapId 로 지도를 찾은 결과가 없을 때 MapNotFoundException 예외를 발생시킨다.', async () => { + const spyFindById = mapRepository.findById.mockResolvedValue(undefined); + await expect(mapService.getMapById(1)).rejects.toThrow( + MapNotFoundException, + ); + expect(spyFindById).toHaveBeenCalledWith(1); + }); + it('파라미터로 받은 mapId 로 지도를 찾은 결과가 있으면 결과를 반환한다.', async () => { + const publicMaps = createPublicMaps(1, fakeUser1)[0]; + publicMaps.mapPlaces = []; + const spyFindById = mapRepository.findById.mockResolvedValue(publicMaps); + const result = await mapService.getMapById(1); + const expectedMap = await MapDetailResponse.from(publicMaps); + expect(spyFindById).toHaveBeenCalledWith(1); + expect(result).toEqual(expectedMap); + }); + }); + describe('createMap 메소드 테스트', () => { + it('파라미터로 받은 유저 아이디로 지도를 생성하고, 지도 id 를 반환한다.', async () => { + const spyOnFindById = + userRepository.findById.mockResolvedValue(fakeUser1); + const publicMap = CreateMapRequest.from({ + title: 'test map', + description: 'This map is test map', + isPublic: true, + thumbnailUrl: 'basic_thumbnail.jpg', + }); + const resolvedMap = publicMap.toEntity(fakeUser1); + resolvedMap.mapPlaces = []; + const spyOnSave = mapRepository.save.mockResolvedValue(resolvedMap); + const result = await mapService.createMap(1, publicMap); + const saveCalledWith = { ...publicMap, user: { id: 1 } }; + expect(spyOnFindById).toHaveBeenCalledWith(1); + expect(spyOnSave).toHaveBeenCalledWith(saveCalledWith); + expect(result).toEqual(expect.objectContaining({ id: undefined })); + }); + }); + describe('deleteMap 메소드 테스트', () => { + it('파라미터로 mapId를 가진 지도가 없다면 MapNotFoundException 에러를 발생시킨다.', async () => { + const spyOnExistById = mapRepository.existById.mockResolvedValue(false); + const spyOnSoftDelete = mapRepository.softDelete; + await expect(mapService.deleteMap(1)).rejects.toThrow( + MapNotFoundException, + ); + expect(spyOnExistById).toHaveBeenCalledWith(1); + expect(spyOnSoftDelete).not.toHaveBeenCalled(); + }); + + it('파라미터로 mapId를 가진 지도가 있다면 삭제 후 삭제된 지도의 id 를 반환한다.', async () => { + const spyOnExistById = mapRepository.existById.mockResolvedValue(true); + const spyOnSoftDelete = mapRepository.softDelete; + const result = await mapService.deleteMap(1); + expect(result).toEqual({ id: 1 }); + expect(spyOnExistById).toHaveBeenCalledWith(1); + expect(spyOnSoftDelete).toHaveBeenCalledWith(1); + }); + }); + describe('updateMapInfo 메소드 테스트', () => { + it('업데이트 하려는 지도가 없을경우 MapNotFoundException 에러를 발생시킨다.', async () => { + const spyOnExistById = mapRepository.existById.mockResolvedValue(false); + const spyOnUpdate = mapRepository.update; + const updateInfo = { + title: 'update test title', + description: 'update test description', + }; + await expect(mapService.updateMapInfo(1, updateInfo)).rejects.toThrow( + MapNotFoundException, + ); + expect(spyOnExistById).toHaveBeenCalledWith(1); + expect(spyOnUpdate).not.toHaveBeenCalled(); + }); + it('업데이트 하려는 지도가 있을 경우 지도를 파라미터의 정보로 업데이트 한다.', async () => { + const spyOnExistById = mapRepository.existById.mockResolvedValue(true); + const spyOnUpdate = mapRepository.update; + const updateInfo = { + title: 'update test title', + description: 'update test description', + }; + await mapService.updateMapInfo(1, updateInfo); + expect(spyOnExistById).toHaveBeenCalledWith(1); + expect(spyOnUpdate).toBeCalledWith(1, updateInfo); + }); + }); + describe('updateMapVisibility 메소드 테스트', () => { + it('visibility 를 업데이트 하려는 지도가 없을 경우 MapNotFoundException 을 발생시킨다.', async () => { + const spyOnExistById = mapRepository.existById.mockResolvedValue(false); + const spyOnUpdate = mapRepository.update; + await expect(mapService.updateMapVisibility(1, true)).rejects.toThrow( + MapNotFoundException, + ); + expect(spyOnExistById).toHaveBeenCalledWith(1); + expect(spyOnUpdate).not.toHaveBeenCalled(); + }); + it('visibility를 업데이트 하려는 지도가 있을 경우 업데이트를 진행한다.', async () => { + const spyOnExistById = mapRepository.existById.mockResolvedValue(true); + const spyOnUpdate = mapRepository.update; + await mapService.updateMapVisibility(1, true); + expect(spyOnExistById).toHaveBeenCalledWith(1); + expect(spyOnUpdate).toBeCalledWith(1, { isPublic: true }); + }); + }); + describe('addPlace 메소드 테스트', () => { + it('장소를 추가하려는 지도가 없을 경우 MapNotFoundException 을 발생시킨다.', async () => { + const spyOnFindById = mapRepository.findById.mockResolvedValue(null); + const spyOnSave = mapRepository.save; + await expect( + mapService.addPlace(1, 2, 'BLUE' as Color, 'test'), + ).rejects.toThrow(MapNotFoundException); + expect(spyOnFindById).toHaveBeenCalledWith(1); + expect(spyOnSave).not.toHaveBeenCalled(); + }); + it('추가하려는 장소가 없을 경우 InvalidPlaceToMapException 를 발생시킨다.', async () => { + const map = createPublicMaps(1, fakeUser1)[0]; + const spyOnFindById = mapRepository.findById.mockResolvedValue(map); + const spyOnPlaceExistById = + placeRepository.existById.mockResolvedValue(false); + await expect( + mapService.addPlace(1, 1, 'RED' as Color, 'test'), + ).rejects.toThrow(InvalidPlaceToMapException); + expect(spyOnFindById).toHaveBeenCalledWith(1); + expect(spyOnPlaceExistById).toHaveBeenCalled(); + }); + it('추가하려는 장소가 이미 해당 지도에 있을경우 DuplicatePlaceToMapException 에러를 발생시킨다', async () => { + const map = createPublicMaps(1, fakeUser1)[0]; + map.mapPlaces = []; + map.mapPlaces.push({ placeId: 1 }); + const spyOnFindById = mapRepository.findById.mockResolvedValue(map); + const spyOnPlaceExistById = + placeRepository.existById.mockResolvedValue(true); + await expect( + mapService.addPlace(1, 1, 'RED' as Color, 'test'), + ).rejects.toThrow(DuplicatePlaceToMapException); + expect(spyOnPlaceExistById).toHaveBeenCalledWith(1); + expect(spyOnFindById).toHaveBeenCalled(); + }); + it('장소를 추가하려는 지도가 있을 경우 장소를 추가하고 장소 정보를 다시 반환한다.', async () => { + const map = createPublicMaps(1, fakeUser1)[0]; + map.mapPlaces = []; + const addPlace = { color: 'RED', comment: 'test', placeId: 2 }; + map.mapPlaces.push({ placeId: 1 }); + const spyOnFindById = mapRepository.findById.mockResolvedValue(map); + const spyOnPlaceExistById = + placeRepository.existById.mockResolvedValue(true); + const result = await mapService.addPlace( + 1, + addPlace.placeId, + addPlace.color as Color, + addPlace.comment, + ); + expect(result).toEqual(addPlace); + expect(spyOnFindById).toHaveBeenCalledWith(1); + expect(spyOnPlaceExistById).toHaveBeenCalledWith(addPlace.placeId); + }); + }); + describe('deletePlace 메소드 테스트', () => { + it('장소를 제거하려는 지도가 없을 경우 MapNotFoundException 에러를 발생시킨다.', async () => { + const spyFindById = mapRepository.findById.mockResolvedValue(null); + const spyMapSave = mapRepository.save; + await expect(mapService.deletePlace(1, 1)).rejects.toThrow( + MapNotFoundException, + ); + expect(spyFindById).toHaveBeenCalledWith(1); + expect(spyMapSave).not.toHaveBeenCalled(); + }); + it('mapId로 받은 지도에서 placeId 를 제거하고 해당 placeId 를 반환한다.', async () => { + const map = createPublicMaps(1, fakeUser1)[0]; + map.mapPlaces = []; + map.mapPlaces.push({ placeId: 1 }); + const expectResult = { deletedId: 1 }; + const spyFindById = mapRepository.findById.mockResolvedValue(map); + const spyMapSave = mapRepository.save; + const result = await mapService.deletePlace(1, 1); + expect(result).toEqual(expectResult); + expect(spyFindById).toHaveBeenCalledWith(1); + expect(spyMapSave).toHaveBeenCalled(); + }); + }); +}); From 21dbbbcaf5ad9f9a3f10963aabbd07a52ec30d32 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:03:34 +0900 Subject: [PATCH 021/139] =?UTF-8?q?test:=20=EC=A7=80=EB=8F=84=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map.integration.expectExcptions.ts | 28 + .../integration-test/map.integration.test.ts | 987 ++++++++++++++++++ .../integration-test/map.integration.util.ts | 94 ++ backend/test/map/map.test.util.ts | 55 + 4 files changed, 1164 insertions(+) create mode 100644 backend/test/map/integration-test/map.integration.expectExcptions.ts create mode 100644 backend/test/map/integration-test/map.integration.test.ts create mode 100644 backend/test/map/integration-test/map.integration.util.ts create mode 100644 backend/test/map/map.test.util.ts diff --git a/backend/test/map/integration-test/map.integration.expectExcptions.ts b/backend/test/map/integration-test/map.integration.expectExcptions.ts new file mode 100644 index 00000000..7db954c3 --- /dev/null +++ b/backend/test/map/integration-test/map.integration.expectExcptions.ts @@ -0,0 +1,28 @@ +export const INVALID_TOKEN_EXCEPTION = { + statusCode: 401, + message: '유효하지 않은 토큰입니다.', +}; + +export const EXPIRE_TOKEN_EXCEPTION = { + statusCode: 401, + message: '만료된 토큰입니다.', +}; + +export const EMPTY_TOKEN_EXCEPTION = { + statusCode: 401, + message: '토큰이 없습니다.', +}; + +export const MAP_NOT_FOUND_EXCEPTION = (id: number) => { + return { + statusCode: 404, + message: `id:${id} 지도가 존재하지 않거나 삭제되었습니다.`, + }; +}; + +export const MAP_PERMISSION_EXCEPTION = (id: number) => { + return { + statusCode: 403, + message: `지도 ${id} 에 대한 권한이 없습니다.`, + }; +}; diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts new file mode 100644 index 00000000..b47a6b8a --- /dev/null +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -0,0 +1,987 @@ +import { MapController } from '@src/map/map.controller'; +import { MapService } from '@src/map/map.service'; +import { UserFixture } from '@test/user/fixture/user.fixture'; +import { User } from '@src/user/entity/user.entity'; +import { UserRepository } from '@src/user/user.repository'; +import { PlaceRepository } from '@src/place/place.repository'; +import { MapRepository } from '@src/map/map.repository'; +import { DataSource } from 'typeorm'; +import { MySqlContainer, StartedMySqlContainer } from '@testcontainers/mysql'; +import { initDataSource } from '@test/config/datasource.config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { JWTHelper } from '@src/auth/JWTHelper'; +import { ConfigModule } from '@nestjs/config'; +import * as request from 'supertest'; +import { + createPlace, + createPrivateMaps, + createPublicMaps, +} from '@test/map/map.test.util'; +import { UserRole } from '@src/user/user.role'; +import { Map } from '@src/map/entity/map.entity'; +import { Color } from '@src/place/place.color.enum'; +import { + EMPTY_TOKEN_EXCEPTION, + EXPIRE_TOKEN_EXCEPTION, + INVALID_TOKEN_EXCEPTION, + MAP_NOT_FOUND_EXCEPTION, + MAP_PERMISSION_EXCEPTION, +} from '@test/map/integration-test/map.integration.expectExcptions'; +import { createInvalidToken } from '@test/map/integration-test/map.integration.util'; + +describe('MapController', () => { + let app: INestApplication; + + let container: StartedMySqlContainer; + let dataSource: DataSource; + + let userRepository: UserRepository; + let mapRepository: MapRepository; + let placeRepository: PlaceRepository; + + let mapService: MapService; + + let fakeUser1: User; + let fakeUser2: User; + let jwtHelper: JWTHelper; + let token: string; + + beforeAll(async () => { + token = null; + + container = await new MySqlContainer().withReuse().start(); + dataSource = await initDataSource(container); + + mapRepository = new MapRepository(dataSource); + placeRepository = new PlaceRepository(dataSource); + userRepository = new UserRepository(dataSource); + + fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); + fakeUser2 = UserFixture.createUser({ oauthId: 'def' }); + await userRepository.delete({}); + await userRepository.save([fakeUser1, fakeUser2]); + const places = createPlace(10); + await placeRepository.save(places); + }); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot()], + controllers: [MapController], + providers: [ + { + provide: DataSource, + useValue: dataSource, + }, + { + provide: PlaceRepository, + useFactory: (dataSource: DataSource) => + new PlaceRepository(dataSource), + inject: [DataSource], + }, + { + provide: UserRepository, + useFactory: (dataSource: DataSource) => + new UserRepository(dataSource), + inject: [DataSource], + }, + { + provide: MapRepository, + useFactory: (dataSource: DataSource) => new MapRepository(dataSource), + inject: [DataSource], + }, + { + provide: MapService, + useFactory: ( + mapRepository: MapRepository, + userRepository: UserRepository, + placeRepository: PlaceRepository, + ) => new MapService(mapRepository, userRepository, placeRepository), + inject: [MapRepository, UserRepository, PlaceRepository], + }, + JWTHelper, + ], + }).compile(); + app = module.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + jwtHelper = app.get(JWTHelper); + mapService = app.get(MapService); + await mapRepository.delete({}); + token = null; + await app.init(); + }); + afterEach(async () => { + await mapRepository.delete({}); + await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); + }); + afterAll(async () => { + await mapRepository.delete({}); + await userRepository.delete({}); + await dataSource.destroy(); + }); + describe('getMyMapList 메소드 테스트', () => { + it('GET /my 에 대한 요청에 해당 유저 ID 가 가지는 모든 지도의 정보를 반환한다.', async () => { + const fakeUser1 = await userRepository.findById(1); + const fakeUser2 = await userRepository.findById(2); + + const fakeUserOneMaps = createPublicMaps(3, fakeUser1); + const fakeUserTwoMaps = createPublicMaps(3, fakeUser2); + await mapRepository.save([...fakeUserOneMaps, ...fakeUserTwoMaps]); + + const userInfo = { + userId: fakeUser1.id, + role: fakeUser1.role, + }; + token = jwtHelper.generateToken('24h', userInfo); + return request(app.getHttpServer()) + .get('/maps/my') + .set('Authorization', `Bearer ${token}`) + .expect(200) + .then((response) => { + const gotMaps = response.body.maps; + expect(gotMaps.length).toEqual(fakeUserOneMaps.length); + gotMaps.forEach((gotMaps, index) => { + const expectedMap = fakeUserOneMaps[index]; + + expect(gotMaps.id).toEqual(expectedMap.id); + expect(gotMaps.title).toEqual(expectedMap.title); + expect(gotMaps.isPublic).toEqual(expectedMap.isPublic); + expect(gotMaps.thumbnailUrl).toEqual(expectedMap.thumbnailUrl); + expect(gotMaps.description).toEqual(expectedMap.description); + expect(gotMaps.pinCount).toEqual(0); + + expect(new Date(gotMaps.createdAt).toISOString()).toEqual( + new Date(expectedMap.createdAt).toISOString(), + ); + expect(new Date(gotMaps.updatedAt).toISOString()).toEqual( + new Date(expectedMap.updatedAt).toISOString(), + ); + }); + }); + }); + it('GET /my 에 대한 요청에 토큰이 없을 경우 AuthenticationException 에러를 발생시킨다.', async () => { + return request(app.getHttpServer()).get('/maps/my').expect(401); + }); + it('GET /my 에 대한 요청에 토큰이 만료됐을 경우 AuthenticationException 에러를 발생시킨다.', async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + + token = jwtHelper.generateToken('1s', payload); + await new Promise((resolve) => setTimeout(resolve, 1500)); + + return request(app.getHttpServer()) + .get('/maps/my') + .set('Authorization', `Bearer ${token}`) + .expect(401) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(EXPIRE_TOKEN_EXCEPTION), + ); + }); + }); + it('GET /my 에 대한 요청에 토큰이 조작됐을 경우 AuthenticationException 에러를 발생시킨다.', async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + + token = jwtHelper.generateToken('24h', payload); + const invalidToken = createInvalidToken(token); + + return request(app.getHttpServer()) + .get('/maps/my') + .set(`Authorization`, `Bearer ${invalidToken}`) + .expect(401); + }); + it('GET /my 에 대한 요청에 user id 에 해당하는 유저가 없을 경우 에러를 발생시킨다.', async () => { + const invalidUserInfo = { + id: 3, + nickname: 'unknown', + provider: 'GOOGLE', + role: UserRole.ADMIN, + }; + const payload = { + userId: invalidUserInfo.id, + role: invalidUserInfo.role, + }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .get('/maps/my') + .set(`Authorization`, `Bearer ${token}`) + .expect(404); + }); + }); + describe('getMapList 메소드 테스트', () => { + it('GET maps/ 에 대한 요청으로 공개 되어있는 지도 모두 반환한다.', async () => { + const publicMaps = createPublicMaps(5, fakeUser1); + const privateMaps = createPrivateMaps(5, fakeUser1); + await mapRepository.save([...publicMaps, ...privateMaps]); + return request(app.getHttpServer()) + .get('/maps') + .expect((response) => { + const gotMaps = response.body.maps; + expect(gotMaps.length).toEqual(publicMaps.length); + gotMaps.forEach((gotMap: Map, index) => { + expect(gotMap.title).toEqual(publicMaps[index].title); + expect(gotMap.description).toEqual(publicMaps[index].description); + expect(gotMap.isPublic).toEqual(publicMaps[index].isPublic); + expect(gotMap.thumbnailUrl).toEqual(publicMaps[index].thumbnailUrl); + }); + }); + }); + }); + describe('getMapDetail 메소드 테스트', () => { + it('GET /maps/:id 에 대해서 지도의 id 와 params 의 id 가 일치하는 지도의 정보를 반환한다.', async () => { + const maps = createPublicMaps(5, fakeUser1); + await mapRepository.save([...maps]); + const EXPECT_MAP_ID = 3; + return request(app.getHttpServer()) + .get(`/maps/${EXPECT_MAP_ID}`) + .expect(200) + .expect((response) => { + const gotMap = response.body; + expect(gotMap.title).toEqual(maps[EXPECT_MAP_ID - 1].title); + expect(gotMap.title).toEqual(maps[EXPECT_MAP_ID - 1].title); + expect(gotMap.description).toEqual( + maps[EXPECT_MAP_ID - 1].description, + ); + expect(gotMap.isPublic).toEqual(maps[EXPECT_MAP_ID - 1].isPublic); + expect(gotMap.thumbnailUrl).toEqual( + maps[EXPECT_MAP_ID - 1].thumbnailUrl, + ); + }); + }); + it('GET /maps/:id 요청을 받았을 때 지도의 id 와 params 의 id 가 일치하는 지도가 없을 경우 MapNotFoundException 를 발생시킨다.', async () => { + const maps = createPublicMaps(5, fakeUser1); + await mapRepository.save([...maps]); + const EXPECT_MAP_ID = 55; + const result = await request(app.getHttpServer()) + .get(`/maps/${EXPECT_MAP_ID}`) + .expect(404); + expect(result.body).toEqual( + expect.objectContaining(MAP_NOT_FOUND_EXCEPTION(EXPECT_MAP_ID)), + ); + }); + }); + describe('createMap 메소드 테스트', () => { + it('POST /maps/ 요청에 대해 토큰이 없을 경우 AuthenticationException 예외를 발생시킨다', async () => { + const result = await request(app.getHttpServer()) + .post('/maps/') + .send({ + title: 'Test Map', + description: 'This is a test map.', + isPublic: true, + thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', + }) + .expect(401); + expect(result.body).toEqual( + expect.objectContaining(EMPTY_TOKEN_EXCEPTION), + ); + }); + it('POST /maps/ 요청에 대해서 조작된 토큰과 함께 요청이 발생할 경우 AuthenticationException 예외를 발생시킨다', async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + + token = jwtHelper.generateToken('24h', payload); + const invalidToken = createInvalidToken(token); + + return request(app.getHttpServer()) + .post('/maps/') + .set(`Authorization`, `Bearer ${invalidToken}`) + .send({ + title: 'Test Map', + description: 'This is a test map.', + isPublic: true, + thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', + }) + .expect(401) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(INVALID_TOKEN_EXCEPTION), + ); + }); + }); + it('POST /maps/ 요청에 대해서 만료된 토큰과 함께 요청이 발생할 경우 AuthenticationException 예외를 발생시킨다', async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + + token = jwtHelper.generateToken('1s', payload); + await new Promise((resolve) => setTimeout(resolve, 1500)); + + return request(app.getHttpServer()) + .post('/maps/') + .set('Authorization', `Bearer ${token}`) + .send({ + title: 'Test Map', + description: 'This is a test map.', + isPublic: true, + thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', + }) + .expect(401) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(EXPIRE_TOKEN_EXCEPTION), + ); + }); + }); + it('POST /maps/ 요청에 대해 유저 정보가 없을 경우 UserNotFoundException 에러를 발생시킨다.', async () => { + const INVALID_USER_ID = 3; + const invalidUserInfo = { + id: INVALID_USER_ID, + nickname: 'unknown', + provider: 'GOOGLE', + role: UserRole.ADMIN, + }; + const payload = { + userId: invalidUserInfo.id, + role: invalidUserInfo.role, + }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .post('/maps/') + .set(`Authorization`, `Bearer ${token}`) + .send({ + title: 'Test Map', + description: 'This is a test map.', + isPublic: true, + thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', + }) + .expect(404) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 404, + message: `id:${INVALID_USER_ID} 유저가 존재하지 않거나 삭제되었습니다.`, + }), + ); + }); + }); + it('/POST /maps 요청의 Body 에 title 이 없을 경우 Bad Request 예외를 발생시킨다.', async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .post('/maps/') + .set('Authorization', `Bearer ${token}`) + .send({ + description: 'This is a test map.', + isPublic: true, + thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', + }) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: ['title should not be empty', 'title must be a string'], + }), + ); + }); + }); + it('/POST /maps 요청의 Body 에 description 이 없을 경우 Bad Request 예외를 발생시킨다.', async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .post('/maps/') + .set('Authorization', `Bearer ${token}`) + .send({ + title: 'Test Map', + isPublic: true, + thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', + }) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: ['description must be a string'], + }), + ); + }); + }); + it('/POST /maps 에 올바른 Body 와 유효한 토큰을 설정한 요청에 대해서 적절하게 저장하고, 저장한 지도에 대한 id 를 반환한다.', async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + + token = jwtHelper.generateToken('24h', payload); + const testMap = { + title: 'Test Map', + description: 'This is a test map.', + isPublic: true, + thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', + }; + return request(app.getHttpServer()) + .post('/maps/') + .set('Authorization', `Bearer ${token}`) + .send(testMap) + .expect(201) + .expect(async (response) => { + expect(response.body).toEqual( + expect.objectContaining({ + id: 1, + }), + ); + const mapInfo = await mapRepository.findById(response.body.id); + expect(mapInfo).toEqual(expect.objectContaining(testMap)); + }); + }); + }); + describe('addPlaceToMap 메소드 테스트', () => { + let publicMap: Map; + let testPlace: { placeId: number; comment: string; color: string }; + let payload: { userId: number; role: string }; + beforeEach(async () => { + publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); + testPlace = { + placeId: 5, + comment: 'Beautiful park with a lake', + color: 'BLUE', + }; + await mapService.addPlace( + 1, + 1, + testPlace.color as Color, + testPlace.comment, + ); + const fakeUserInfo = await userRepository.findById(1); + payload = { + userId: fakeUserInfo.id, + role: fakeUserInfo.role, + }; + }); + afterEach(async () => { + await mapRepository.delete({}); + }); + it('POST /maps/:id/places 요청의 Body의 placeId의 타입이 number가 아니라면 Bad Request 에러를 발생시킨다.', async () => { + token = jwtHelper.generateToken('24h', payload); + const InvalidTestPlace = { + placeId: 'invalid id', + comment: 'update test description', + color: 'BLUE', + }; + return request(app.getHttpServer()) + .post('/maps/1/places') + .set('Authorization', `Bearer ${token}`) + .send(InvalidTestPlace) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: [ + 'placeId must be a number conforming to the specified constraints', + ], + }), + ); + }); + }); + it('POST /maps/:id/places 요청의 Body의 comment의 타입이 string이 아니라면 Bad Request 에러를 발생시킨다.', async () => { + const InvalidTestPlace = { + placeId: 5, + comment: 9999999999, + color: 'BLUE', + }; + + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .post('/maps/1/places') + .set('Authorization', `Bearer ${token}`) + .send(InvalidTestPlace) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: ['comment must be a string'], + }), + ); + }); + }); + it('POST /maps/:id/places 요청의 Body의 color가 enum(Color) 아니라면 Bad Request 에러를 발생시킨다.', async () => { + const InvalidTestPlace = { + placeId: 5, + comment: 'update test description', + color: 'IVORY', + }; + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .post('/maps/1/places') + .set('Authorization', `Bearer ${token}`) + .send(InvalidTestPlace) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: [ + 'color must be one of the following values: RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE', + ], + }), + ); + }); + }); + it('POST /maps/:id/places 요청이 적절한 토큰과 Body를 가지지만 해당 지도에 해당 장소가 이미 있다면 DuplicatePlaceToMapException 에러를 발생시킨다.', async () => { + token = jwtHelper.generateToken('24h', payload); + await mapService.addPlace( + 1, + testPlace.placeId, + testPlace.color as Color, + testPlace.comment, + ); + return request(app.getHttpServer()) + .post('/maps/1/places') + .set('Authorization', `Bearer ${token}`) + .send(testPlace) + .expect(409) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 409, + message: + '이미 지도에 존재하는 장소입니다. : ' + testPlace.placeId, + }), + ); + }); + }); + it('POST /maps/:id/places 요청이 적절한 토큰과 Body를 가지지만 해당 유저의 지도가 아니라면 MapPermissionException 을 발생한다.', async () => { + const fakeUser2 = await userRepository.findById(2); + payload = { + userId: fakeUser2.id, + role: fakeUser2.role, + }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .post('/maps/1/places') + .set('Authorization', `Bearer ${token}`) + .send(testPlace) + .expect(403) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_PERMISSION_EXCEPTION(1)), + ); + }); + }); + it('POST /maps/:id/places 요청이 적절한 토큰과 Body를 가진다면 해당 지도에 해당 장소를 저장하고 저장된 지도의 정보를 반환한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .post('/maps/1/places') + .set('Authorization', `Bearer ${token}`) + .send(testPlace) + .expect(201) + .expect((response) => { + expect(response.body).toEqual(expect.objectContaining(testPlace)); + }); + }); + }); + describe('deletePlaceFromMap 메소드 테스트', () => { + let payload: { userId: number; role: string }; + let testPlace: { placeId: number; comment: string; color: string }; + beforeEach(async () => { + const fakeUserOneInfo = await userRepository.findById(1); + const publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); + testPlace = { + placeId: 1, + comment: 'Beautiful park with a lake', + color: 'BLUE', + }; + await mapService.addPlace( + 1, + testPlace.placeId, + testPlace.color as Color, + testPlace.comment, + ); + payload = { + userId: fakeUserOneInfo.id, + role: fakeUserOneInfo.role, + }; + }); + + it('DELETE /maps/:id/places/:placeId 요청의 지도의 id 를 찾지 못했을 경우 MapNotFoundException 예외를 발생한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .delete('/maps/3/places/1') + .set('Authorization', `Bearer ${token}`) + .expect(404) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_NOT_FOUND_EXCEPTION(3)), + ); + }); + }); + it('DELETE /maps/:id/places/:placeId 요청에 올바른 토큰과 지도 id 를 설정했지만, 해당 유저의 지도가 아닐경우 MapPermissionException 을 발생한다.', async () => { + const fakeUser2 = await userRepository.findById(2); + payload = { + userId: fakeUser2.id, + role: fakeUser2.role, + }; + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .delete('/maps/1/places/1') + .set('Authorization', `Bearer ${token}`) + .expect(403) + .expect(async (response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_PERMISSION_EXCEPTION(1)), + ); + }); + }); + it('DELETE /maps/:id/places/:placeId 요청에 올바른 토큰과 지도 id 를 설정할 경우 해당 지도에서 placeId 를 삭제하고 해당 placeId 를 반환한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + + return request(app.getHttpServer()) + .delete('/maps/1/places/1') + .set('Authorization', `Bearer ${token}`) + .expect(200) + .expect(async (response) => { + expect(response.body).toEqual( + expect.objectContaining({ + deletedId: 1, + }), + ); + const map = await mapRepository.findById(1); + expect(map.mapPlaces.length).toBe(0); + }); + }); + }); + describe('updateMapInfo 메소드 테스트', () => { + let publicMap: Map; + let testPlace: { placeId: number; comment: string; color: string }; + let payload: { userId: number; role: string }; + beforeEach(async () => { + publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); + testPlace = { + placeId: 5, + comment: 'Beautiful park with a lake', + color: 'BLUE', + }; + await mapService.addPlace( + 1, + 1, + testPlace.color as Color, + testPlace.comment, + ); + const fakeUserInfo = await userRepository.findById(1); + payload = { + userId: fakeUserInfo.id, + role: fakeUserInfo.role, + }; + }); + afterEach(async () => { + await mapRepository.delete({}); + }); + it('/PATCH /:id/info 요청 Body 에 title 이 없다면 예외를 발생한다..', async () => { + token = jwtHelper.generateToken('24h', payload); + const updateMapInfo = { + description: 'this is updated test map', + }; + return request(app.getHttpServer()) + .patch('/maps/1/info') + .send(updateMapInfo) + .set('Authorization', `Bearer ${token}`) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: ['title should not be empty', 'title must be a string'], + error: 'Bad Request', + }), + ); + }); + }); + it('/PATCH /:id/info 요청 Body 에 title 의 타입이 string이 아니라면 예외를 발생한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + const updateMapInfo = { + title: 124124, + description: 'this is updated test map', + }; + return request(app.getHttpServer()) + .patch('/maps/1/info') + .send(updateMapInfo) + .set('Authorization', `Bearer ${token}`) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: ['title must be a string'], + error: 'Bad Request', + }), + ); + }); + }); + it('/PATCH /:id/info 요청 Body 에 description 의 타입이 string이 아니라면 예외를 발생한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + const updateMapInfo = { + title: 'updated map title', + description: 111111, + }; + return request(app.getHttpServer()) + .patch('/maps/1/info') + .send(updateMapInfo) + .set('Authorization', `Bearer ${token}`) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: ['description must be a string'], + error: 'Bad Request', + }), + ); + }); + }); + it('/PATCH /:id/info 요청에 url params 의 지도 id 가 유효하지 않다면 MapNotFoundException 발생한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + const updateMapInfo = { + title: 'updated map title', + description: 'updated map description', + }; + return request(app.getHttpServer()) + .patch('/maps/2/info') + .send(updateMapInfo) + .set('Authorization', `Bearer ${token}`) + .expect(404) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_NOT_FOUND_EXCEPTION(2)), + ); + }); + }); + it('/PATCH /:id/info 요청에 올바른 토큰과 적절한 요청 body, params 를 가지지만 해당 유저의 지도가 아닐경우 MapPermissionException 을 발생한다.', async () => { + const fakeUser2 = await userRepository.findById(2); + payload = { + userId: fakeUser2.id, + role: fakeUser2.role, + }; + token = jwtHelper.generateToken('24h', payload); + const updateMapInfo = { + title: 'updated map title', + description: 'updated map description', + }; + return request(app.getHttpServer()) + .patch('/maps/1/info') + .send(updateMapInfo) + .set('Authorization', `Bearer ${token}`) + .expect(403) + .expect(async (response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_PERMISSION_EXCEPTION(1)), + ); + }); + }); + it('/PATCH /:id/info 요청에 올바른 토큰과 적절한 요청 body, params 를 가진다면 해당 지도의 정보를 업데이트하고 업데이트된 지도의 id 와 정보를 반환한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + const updateMapInfo = { + title: 'updated map title', + description: 'updated map description', + }; + return request(app.getHttpServer()) + .patch('/maps/1/info') + .send(updateMapInfo) + .set('Authorization', `Bearer ${token}`) + .expect(200) + .expect(async (response) => { + expect(response.body).toEqual({ + id: 1, + ...updateMapInfo, + }); + const updatedMap = await mapRepository.findById(1); + expect(updatedMap).toEqual( + expect.objectContaining({ + id: 1, + ...updateMapInfo, + }), + ); + }); + }); + }); + describe('updateMapVisibility 메소드 테스트', () => { + let publicMap: Map; + let testPlace: { placeId: number; comment: string; color: string }; + let payload: { userId: number; role: string }; + beforeEach(async () => { + publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); + testPlace = { + placeId: 5, + comment: 'Beautiful park with a lake', + color: 'BLUE', + }; + await mapService.addPlace( + 1, + 1, + testPlace.color as Color, + testPlace.comment, + ); + const fakeUserInfo = await userRepository.findById(1); + payload = { + userId: fakeUserInfo.id, + role: fakeUserInfo.role, + }; + }); + afterEach(async () => { + await mapRepository.delete({}); + }); + it('PATCH /maps/:id/visibility 요청의 body 의 isPublic 이 boolean 이 아닐경우 예외를 발생한다.', async () => { + const updateIsPublic = { isPublic: 'NOT BOOLEAN' }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .patch('/maps/1/visibility') + .send(updateIsPublic) + .set('Authorization', `Bearer ${token}`) + .expect(400) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + statusCode: 400, + message: 'isPublic must be boolean not string.', + }), + ); + }); + }); + it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body가 있을 경우 지도의 id 와 변경된 isPublic 을 반환한다.', async () => { + const updateIsPublic = { isPublic: false }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .patch('/maps/1/visibility') + .send(updateIsPublic) + .set('Authorization', `Bearer ${token}`) + .expect(200) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining({ + id: 1, + isPublic: false, + }), + ); + }); + }); + it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body를 가지지만 해당 유저의 지도가 아닐 경우 MapPermissionException 을 발생한다.', async () => { + const fakeUser2 = await userRepository.findById(2); + payload = { + userId: fakeUser2.id, + role: fakeUser2.role, + }; + const updateIsPublic = { isPublic: false }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .patch('/maps/1/visibility') + .send(updateIsPublic) + .set('Authorization', `Bearer ${token}`) + .expect(403) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_PERMISSION_EXCEPTION(1)), + ); + }); + }); + it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body가 있지만 지도가 없을 경우 MapNotFoundException 을 발생한다.', async () => { + const updateIsPublic = { isPublic: false }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .patch('/maps/5/visibility') + .send(updateIsPublic) + .set('Authorization', `Bearer ${token}`) + .expect(404) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_NOT_FOUND_EXCEPTION(5)), + ); + }); + }); + }); + describe('deleteMap 메소드 테스트', () => { + let publicMap: Map; + let testPlace: { placeId: number; comment: string; color: string }; + let payload: { userId: number; role: string }; + beforeEach(async () => { + publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); + testPlace = { + placeId: 5, + comment: 'Beautiful park with a lake', + color: 'BLUE', + }; + const fakeUserInfo = await userRepository.findById(1); + payload = { + userId: fakeUserInfo.id, + role: fakeUserInfo.role, + }; + }); + afterEach(async () => { + await mapRepository.delete({}); + }); + it('DELETE /maps/:id 요청에 적절한 토큰이 있지만 해당하는 지도가 없다면 예외를 발생한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .delete('/maps/5/') + .set('Authorization', `Bearer ${token}`) + .expect(404) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_NOT_FOUND_EXCEPTION(5)), + ); + }); + }); + it('DELETE /maps/:id 요청에 대해 적절한 토큰이 있지만, 해당 유저의 지도가 아닐 경우 MapPermissionException 을 발생한다.', async () => { + const fakeUser2 = await userRepository.findById(2); + payload = { + userId: fakeUser2.id, + role: fakeUser2.role, + }; + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .delete('/maps/1') + .set('Authorization', `Bearer ${token}`) + .expect(403) + .expect((response) => { + expect(response.body).toEqual( + expect.objectContaining(MAP_PERMISSION_EXCEPTION(1)), + ); + }); + }); + it('DELETE /maps/:id 요청에 대해 적절한 토큰이 있고 id 에 해당하는 지도가 있으면 삭제하고 id를 반환한다.', async () => { + token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) + .delete('/maps/1') + .set('Authorization', `Bearer ${token}`) + .expect(200) + .expect((response) => { + expect(response.body).toEqual(expect.objectContaining({ id: 1 })); + }); + }); + }); +}); diff --git a/backend/test/map/integration-test/map.integration.util.ts b/backend/test/map/integration-test/map.integration.util.ts new file mode 100644 index 00000000..eff82237 --- /dev/null +++ b/backend/test/map/integration-test/map.integration.util.ts @@ -0,0 +1,94 @@ +import { MySqlContainer } from '@testcontainers/mysql'; +import { initDataSource } from '@test/config/datasource.config'; +import { MapRepository } from '@src/map/map.repository'; +import { PlaceRepository } from '@src/place/place.repository'; +import { UserRepository } from '@src/user/user.repository'; +import { UserFixture } from '@test/user/fixture/user.fixture'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule } from '@nestjs/config'; +import { MapController } from '@src/map/map.controller'; +import { DataSource } from 'typeorm'; +import { MapService } from '@src/map/map.service'; +import { JWTHelper } from '@src/auth/JWTHelper'; +import { ValidationPipe } from '@nestjs/common'; + +export async function initializeDatabase() { + const container = await new MySqlContainer().withReuse().start(); + const dataSource = await initDataSource(container); + + const mapRepository = new MapRepository(dataSource); + const placeRepository = new PlaceRepository(dataSource); + const userRepository = new UserRepository(dataSource); + const fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); + const fakeUser2 = UserFixture.createUser({ oauthId: 'def' }); + return { + container, + dataSource, + mapRepository, + placeRepository, + userRepository, + fakeUser1, + fakeUser2, + }; +} + +export async function initializeTestModule(dataSource: DataSource) { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot()], + controllers: [MapController], + providers: [ + { + provide: DataSource, + useValue: dataSource, + }, + { + provide: PlaceRepository, + useFactory: (dataSource: DataSource) => new PlaceRepository(dataSource), + inject: [DataSource], + }, + { + provide: UserRepository, + useFactory: (dataSource: DataSource) => new UserRepository(dataSource), + inject: [DataSource], + }, + { + provide: MapRepository, + useFactory: (dataSource: DataSource) => new MapRepository(dataSource), + inject: [DataSource], + }, + { + provide: MapService, + useFactory: ( + mapRepository: MapRepository, + userRepository: UserRepository, + placeRepository: PlaceRepository, + ) => new MapService(mapRepository, userRepository, placeRepository), + inject: [MapRepository, UserRepository, PlaceRepository], + }, + JWTHelper, + ], + }).compile(); + const app = module.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + const jwtHelper = app.get(JWTHelper); + const mapService = app.get(MapService); + const userRepository = app.get(UserRepository); + const mapRepository = app.get(MapRepository); + await mapRepository.delete({}); + await app.init(); + return { app, jwtHelper, mapService, userRepository, mapRepository }; +} + +export async function createPayload(userRepository: UserRepository) { + const fakeUser = await userRepository.findById(1); + return { + userId: fakeUser.id, + role: fakeUser.role, + }; +} + +export function createInvalidToken(validToken: string): string { + const parts = validToken.split('.'); + parts[1] = Buffer.from('{"userId":1,"role":"admin"}').toString('base64'); // 조작된 페이로드 + return parts.join('.'); +} diff --git a/backend/test/map/map.test.util.ts b/backend/test/map/map.test.util.ts new file mode 100644 index 00000000..445d9475 --- /dev/null +++ b/backend/test/map/map.test.util.ts @@ -0,0 +1,55 @@ +import { User } from '@src/user/entity/user.entity'; +import { MapFixture } from '@test/map/fixture/map.fixture'; +import { PlaceFixture } from '@test/place/fixture/place.fixture'; + +export function createPublicMaps(count: number, user: User) { + const maps = []; + for (let i = 1; i <= count + 1; i++) { + const map = MapFixture.createMap({ + user: user, + title: `public test map ${i}`, + }); + maps.push(map); + } + return maps; +} + +export function createPrivateMaps(count: number, user: User) { + const maps = []; + for (let i = 1; i <= count + 1; i++) { + const map = MapFixture.createMap({ + user: user, + title: `private test map ${i}`, + isPublic: false, + }); + maps.push(map); + } + return maps; +} + +export function createPublicMapsWithTitle( + count: number, + user: User, + title: string, +) { + const maps = []; + for (let i = 1; i <= count + 1; i++) { + const map = MapFixture.createMap({ + user: user, + title: `${title} ${i}`, + }); + maps.push(map); + } + return maps; +} + +export function createPlace(count: number) { + const places = []; + for (let i = 1; i <= count + 1; i++) { + const place = PlaceFixture.createPlace({ + googlePlaceId: `google_place_${i}`, + }); + places.push(place); + } + return places; +} From f48a3868f60991bf84fdf346c1e8ff0e251e4225 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:23:00 +0900 Subject: [PATCH 022/139] =?UTF-8?q?fix:=20lint=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/integration-test/map.integration.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index b47a6b8a..1312aaaf 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -925,16 +925,10 @@ describe('MapController', () => { }); describe('deleteMap 메소드 테스트', () => { let publicMap: Map; - let testPlace: { placeId: number; comment: string; color: string }; let payload: { userId: number; role: string }; beforeEach(async () => { publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); - testPlace = { - placeId: 5, - comment: 'Beautiful park with a lake', - color: 'BLUE', - }; const fakeUserInfo = await userRepository.findById(1); payload = { userId: fakeUserInfo.id, From fb5bea540b54317b0170b8ae159de249e02ec0c9 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:50:39 +0900 Subject: [PATCH 023/139] =?UTF-8?q?fix:=20=EC=A7=80=EB=8F=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B0=92=2010=EC=97=90=EC=84=9C=2015?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 9587332e..6ce03ecd 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -30,7 +30,7 @@ export class MapController { async getMapList( @Query('query') query?: string, @Query('page', new ParseOptionalNumberPipe(1)) page?: number, - @Query('limit', new ParseOptionalNumberPipe(10)) limit?: number, + @Query('limit', new ParseOptionalNumberPipe(15)) limit?: number, ) { return await this.mapService.searchMap(query, page, limit); } From 0cb5770af87fef98ba42dddf1decf0ad5a5e87c4 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:51:17 +0900 Subject: [PATCH 024/139] =?UTF-8?q?fix:=20map=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20secretKey=EA=B0=80=20=EC=97=86?= =?UTF-8?q?=EC=96=B4=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20override=EB=A1=9C=20=ED=95=B4=EC=85=9C=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/integration-test/map.integration.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index 1312aaaf..300b663e 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -13,6 +13,7 @@ import { INestApplication, ValidationPipe } from '@nestjs/common'; import { JWTHelper } from '@src/auth/JWTHelper'; import { ConfigModule } from '@nestjs/config'; import * as request from 'supertest'; +import * as jwt from 'jsonwebtoken'; import { createPlace, createPrivateMaps, @@ -101,7 +102,18 @@ describe('MapController', () => { }, JWTHelper, ], - }).compile(); + }) + .overrideProvider(JWTHelper) + .useValue({ + jwtSecretKey: 'test-key', + generateToken: (expiresIn: string | number, payload: any = {}) => { + return jwt.sign(payload, 'test-key', { expiresIn }); + }, + verifyToken: (refreshToken: string) => { + return jwt.verify(refreshToken, 'test-key'); + }, + }) + .compile(); app = module.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ transform: true })); jwtHelper = app.get(JWTHelper); From 0e3220a45c9c63d926fb1a111ed37b6e36814e05 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:54:48 +0900 Subject: [PATCH 025/139] =?UTF-8?q?refactor:=20MapService=EC=9D=98=20repos?= =?UTF-8?q?itory=20=EB=93=A4=EC=9D=84=20=EB=AA=A8=ED=82=B9=ED=95=A0=20?= =?UTF-8?q?=EB=95=8C=20createMock=20=EC=82=AC=EC=9A=A9/=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=EC=97=86=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=82=AD=EC=A0=9C/=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 1 + backend/test/map/map.service.test.ts | 67 +++++++++++++--------------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/backend/package.json b/backend/package.json index a1721e82..4714ad74 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "@nestjs/schedule": "^4.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "@golevelup/ts-jest": "^0.6.1", "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.2", "mysql2": "^3.11.3", diff --git a/backend/test/map/map.service.test.ts b/backend/test/map/map.service.test.ts index c9c8f7ea..9080dc4f 100644 --- a/backend/test/map/map.service.test.ts +++ b/backend/test/map/map.service.test.ts @@ -16,6 +16,8 @@ import { Color } from '@src/place/place.color.enum'; import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMapException'; import { DuplicatePlaceToMapException } from '@src/map/exception/DuplicatePlaceToMapException'; import { UserRepository } from '@src/user/user.repository'; +import { createMock } from '@golevelup/ts-jest'; +import { MapPlace } from '@src/map/entity/map-place.entity'; describe('MapService 테스트', () => { let mapService: MapService; @@ -33,7 +35,7 @@ describe('MapService 테스트', () => { [page, pageSize] = [1, 10]; }); beforeEach(async () => { - mapRepository = { + mapRepository = createMock({ searchByTitleQuery: jest.fn(), findAll: jest.fn(), count: jest.fn(), @@ -43,29 +45,27 @@ describe('MapService 테스트', () => { softDelete: jest.fn(), update: jest.fn(), existById: jest.fn(), - } as unknown as jest.Mocked; - userRepository = { + }); + userRepository = createMock({ findByProviderAndOauthId: jest.fn(), createUser: jest.fn(), findById: jest.fn(), existById: jest.fn(), - } as unknown as jest.Mocked; - placeRepository = { + }); + placeRepository = createMock({ findByGooglePlaceId: jest.fn(), findAll: jest.fn(), searchByNameOrAddressQuery: jest.fn(), existById: jest.fn(), - } as unknown as jest.Mocked; + }); mapService = new MapService(mapRepository, userRepository, placeRepository); userRepository.existById.mockResolvedValue(true); }); describe('searchMap 메소드 테스트', () => { it('파라미터 중 query 가 없을 경우 공개된 모든 지도를 반환한다.', async () => { - const mockMaps: Map[] = createPublicMaps(5, fakeUser1).map((map) => { - return { - ...map, - mapPlaces: [], - }; + const mockMaps: Map[] = createPublicMaps(5, fakeUser1); + mockMaps.forEach((map) => { + map.mapPlaces = []; }); const spyFindAll = mapRepository.findAll.mockResolvedValue(mockMaps); const spyCount = mapRepository.count.mockResolvedValue(mockMaps.length); @@ -86,17 +86,15 @@ describe('MapService 테스트', () => { expect(result.currentPage).toEqual(page); expect(result.totalPages).toEqual(Math.ceil(mockMaps.length / pageSize)); }); - it('파라미터 중 쿼리(지도 title)가 있을 경우 해당 제목을 가진 지도들을 반환한다', async () => { + it('파라미터 중 쿼리가 있을 경우 해당 제목을 가진 지도들을 반환한다', async () => { const searchTitle = 'cool'; const mockCoolMaps: Map[] = createPublicMapsWithTitle( 5, fakeUser1, 'cool map', - ).map((map) => { - return { - ...map, - mapPlaces: [], - }; + ); + mockCoolMaps.forEach((map) => { + map.mapPlaces = []; }); const spySearchByTitleQuery = jest .spyOn(mapRepository, 'searchByTitleQuery') @@ -109,31 +107,26 @@ describe('MapService 테스트', () => { ); const result = await mapService.searchMap(searchTitle, 1, 10); expect(spySearchByTitleQuery).toHaveBeenCalledWith( - 'cool', + searchTitle, page, pageSize, ); expect(spyCount).toHaveBeenCalledWith({ - where: { title: 'cool', isPublic: true }, + where: { title: searchTitle, isPublic: true }, }); expect(result.maps).toEqual( expect.arrayContaining( expectedMaps.map((map) => expect.objectContaining(map)), ), ); - expect(result.currentPage).toEqual(page); - expect(result.totalPages).toEqual( - Math.ceil(mockCoolMaps.length / pageSize), - ); }); }); describe('getOwnMaps 메소드 테스트', () => { it('유저 아이디를 파라미터로 받아서 해당 유저의 지도를 반환한다.', async () => { - const fakeUserMaps = createPublicMaps(5, fakeUser1).map((map) => { - return { - ...map, - mapPlaces: [], - }; + const fakeUserMaps = createPublicMaps(5, fakeUser1); + + fakeUserMaps.forEach((map) => { + map.mapPlaces = []; }); const spyFindUserById = mapRepository.findByUserId.mockResolvedValue(fakeUserMaps); @@ -158,10 +151,6 @@ describe('MapService 테스트', () => { expectedMaps.map((map) => expect.objectContaining(map)), ), ); - expect(result.totalPages).toEqual( - Math.ceil(fakeUserMaps.length / pageSize), - ); - expect(result.currentPage).toEqual(page); }); }); describe('getMapById 메소드 테스트', () => { @@ -290,7 +279,11 @@ describe('MapService 테스트', () => { it('추가하려는 장소가 이미 해당 지도에 있을경우 DuplicatePlaceToMapException 에러를 발생시킨다', async () => { const map = createPublicMaps(1, fakeUser1)[0]; map.mapPlaces = []; - map.mapPlaces.push({ placeId: 1 }); + const place = new MapPlace(); + place.placeId = 1; + place.color = 'RED' as Color; + place.description = 'test'; + map.mapPlaces.push(place); const spyOnFindById = mapRepository.findById.mockResolvedValue(map); const spyOnPlaceExistById = placeRepository.existById.mockResolvedValue(true); @@ -304,7 +297,9 @@ describe('MapService 테스트', () => { const map = createPublicMaps(1, fakeUser1)[0]; map.mapPlaces = []; const addPlace = { color: 'RED', comment: 'test', placeId: 2 }; - map.mapPlaces.push({ placeId: 1 }); + const place = new MapPlace(); + place.placeId = 1; + map.mapPlaces.push(place); const spyOnFindById = mapRepository.findById.mockResolvedValue(map); const spyOnPlaceExistById = placeRepository.existById.mockResolvedValue(true); @@ -332,7 +327,9 @@ describe('MapService 테스트', () => { it('mapId로 받은 지도에서 placeId 를 제거하고 해당 placeId 를 반환한다.', async () => { const map = createPublicMaps(1, fakeUser1)[0]; map.mapPlaces = []; - map.mapPlaces.push({ placeId: 1 }); + const newPlace = new MapPlace(); + newPlace.placeId = 1; + map.mapPlaces.push(newPlace); const expectResult = { deletedId: 1 }; const spyFindById = mapRepository.findById.mockResolvedValue(map); const spyMapSave = mapRepository.save; From f1e0055100a232ae5daf7593f62c5c82e799f77c Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:55:32 +0900 Subject: [PATCH 026/139] =?UTF-8?q?fix:=20MapPermissionGuard=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=EB=B6=80=EB=B6=84=20=EC=A7=80=EB=8F=84=EC=9D=98=20?= =?UTF-8?q?user=20Id=20=EC=99=80=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=EC=9D=98=20userId=20=EB=A5=BC=20=EB=B9=84=EA=B5=90=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/guards/MapPermissionGuard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/map/guards/MapPermissionGuard.ts b/backend/src/map/guards/MapPermissionGuard.ts index e7d2b3bb..dedefb33 100644 --- a/backend/src/map/guards/MapPermissionGuard.ts +++ b/backend/src/map/guards/MapPermissionGuard.ts @@ -12,7 +12,7 @@ export class MapPermissionGuard implements CanActivate { const userId = Number(request.user.userId); const map = await this.mapService.getMapById(mapId); - if (map.id !== userId) { + if (map.user.id !== userId) { throw new MapPermissionException(mapId); } return true; From acba08f0a7241aeda18f665d936bc2821b4408b5 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:56:59 +0900 Subject: [PATCH 027/139] =?UTF-8?q?fix:=20dto=20=EB=A1=9C=20isPublic=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20/=20=EA=B7=B8=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/map/dto/UpdateMapVisibilityRequest.ts | 7 +++++++ backend/src/map/map.controller.ts | 14 +++++++------- .../map/integration-test/map.integration.test.ts | 4 ++-- .../map/integration-test/map.integration.util.ts | 16 +++++----------- 4 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 backend/src/map/dto/UpdateMapVisibilityRequest.ts diff --git a/backend/src/map/dto/UpdateMapVisibilityRequest.ts b/backend/src/map/dto/UpdateMapVisibilityRequest.ts new file mode 100644 index 00000000..ab6ab451 --- /dev/null +++ b/backend/src/map/dto/UpdateMapVisibilityRequest.ts @@ -0,0 +1,7 @@ +import { IsBoolean, IsNotEmpty } from 'class-validator'; + +export class UpdateMapVisibilityRequest { + @IsNotEmpty() + @IsBoolean() + isPublic: boolean; +} diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 6ce03ecd..c041a0a6 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -21,6 +21,7 @@ import { EmptyRequestException } from '@src/common/exception/EmptyRequestExcepti import { AuthUser } from '@src/auth/AuthUser.decorator'; import { JwtAuthGuard } from '@src/auth/JwtAuthGuard'; import { MapPermissionGuard } from '@src/map/guards/MapPermissionGuard'; +import { UpdateMapVisibilityRequest } from '@src/map/dto/UpdateMapVisibilityRequest'; @Controller('/maps') export class MapController { @@ -108,14 +109,13 @@ export class MapController { @Patch('/:id/visibility') async updateMapVisibility( @Param('id') id: number, - @Body('isPublic') isPublic: boolean, + @Body() updateMapVisibilityRequest: UpdateMapVisibilityRequest, ) { - if (typeof isPublic !== 'boolean') { - throw new BadRequestException('공개 여부는 boolean 타입이어야 합니다.'); - } - - await this.mapService.updateMapVisibility(id, isPublic); - return { id, isPublic }; + await this.mapService.updateMapVisibility( + id, + updateMapVisibilityRequest.isPublic, + ); + return { id, isPublic: updateMapVisibilityRequest.isPublic }; } @UseGuards(JwtAuthGuard, MapPermissionGuard) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index 300b663e..761440ce 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -31,7 +31,7 @@ import { } from '@test/map/integration-test/map.integration.expectExcptions'; import { createInvalidToken } from '@test/map/integration-test/map.integration.util'; -describe('MapController', () => { +describe('MapController 통합 테스트', () => { let app: INestApplication; let container: StartedMySqlContainer; @@ -879,7 +879,7 @@ describe('MapController', () => { expect(response.body).toEqual( expect.objectContaining({ statusCode: 400, - message: 'isPublic must be boolean not string.', + message: ['isPublic must be a boolean value'], }), ); }); diff --git a/backend/test/map/integration-test/map.integration.util.ts b/backend/test/map/integration-test/map.integration.util.ts index eff82237..7b6ee666 100644 --- a/backend/test/map/integration-test/map.integration.util.ts +++ b/backend/test/map/integration-test/map.integration.util.ts @@ -79,16 +79,10 @@ export async function initializeTestModule(dataSource: DataSource) { return { app, jwtHelper, mapService, userRepository, mapRepository }; } -export async function createPayload(userRepository: UserRepository) { - const fakeUser = await userRepository.findById(1); - return { - userId: fakeUser.id, - role: fakeUser.role, - }; -} - export function createInvalidToken(validToken: string): string { - const parts = validToken.split('.'); - parts[1] = Buffer.from('{"userId":1,"role":"admin"}').toString('base64'); // 조작된 페이로드 - return parts.join('.'); + const [header, , signature] = validToken.split('.'); + const manipulatedPayload = Buffer.from( + '{"userId":1,"role":"admin"}', + ).toString('base64'); + return [header, manipulatedPayload, signature].join('.'); } From dc292374c407187f21903dae89164d27c47f6a7c Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:57:25 +0900 Subject: [PATCH 028/139] =?UTF-8?q?refactor:=20fixture=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=95=A8=EC=88=98=20=EA=B3=A0=EC=B0=A8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.test.util.ts | 78 ++++++++++++++++--------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/backend/test/map/map.test.util.ts b/backend/test/map/map.test.util.ts index 445d9475..bd121078 100644 --- a/backend/test/map/map.test.util.ts +++ b/backend/test/map/map.test.util.ts @@ -1,22 +1,36 @@ import { User } from '@src/user/entity/user.entity'; import { MapFixture } from '@test/map/fixture/map.fixture'; import { PlaceFixture } from '@test/place/fixture/place.fixture'; +import { Map } from '@src/map/entity/map.entity'; -export function createPublicMaps(count: number, user: User) { - const maps = []; - for (let i = 1; i <= count + 1; i++) { - const map = MapFixture.createMap({ - user: user, - title: `public test map ${i}`, - }); - maps.push(map); - } - return maps; +function createEntities( + createFn: (index: number, ...args: any[]) => T, + quantity: number, + ...args: any[] +): T[] { + return Array.from({ length: quantity }, (_, i) => createFn(i + 1, ...args)); +} + +function createMapWithOptions( + index: number, + user: User, + options: Partial = {}, +): Map { + return MapFixture.createMap({ + user, + title: `test map ${index}`, + isPublic: true, + ...options, + }); +} + +export function createPublicMaps(quantity: number, user: User) { + return createEntities((i) => createMapWithOptions(i, user), quantity); } -export function createPrivateMaps(count: number, user: User) { +export function createPrivateMaps(quantity: number, user: User) { const maps = []; - for (let i = 1; i <= count + 1; i++) { + for (let i = 1; i <= quantity + 1; i++) { const map = MapFixture.createMap({ user: user, title: `private test map ${i}`, @@ -27,29 +41,19 @@ export function createPrivateMaps(count: number, user: User) { return maps; } -export function createPublicMapsWithTitle( - count: number, +export const createPlace = (quantity: number) => { + return createEntities( + (i) => PlaceFixture.createPlace({ googlePlaceId: `google_place_${i}` }), + quantity, + ); +}; +export const createPublicMapsWithTitle = ( + quantity: number, user: User, - title: string, -) { - const maps = []; - for (let i = 1; i <= count + 1; i++) { - const map = MapFixture.createMap({ - user: user, - title: `${title} ${i}`, - }); - maps.push(map); - } - return maps; -} - -export function createPlace(count: number) { - const places = []; - for (let i = 1; i <= count + 1; i++) { - const place = PlaceFixture.createPlace({ - googlePlaceId: `google_place_${i}`, - }); - places.push(place); - } - return places; -} + baseTitle: string, +) => { + return createEntities( + (i) => createMapWithOptions(i, user, { title: `${baseTitle} ${i}` }), + quantity, + ); +}; From a139878c871316c49d0335ea211cf2d3cc2a3526 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:58:06 +0900 Subject: [PATCH 029/139] =?UTF-8?q?refactor:=20dto=20=EB=A1=9C=20isPublic?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=ED=95=98=EA=B3=A0=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=9C=EC=83=9D=EC=8B=9C=ED=82=A4=EA=B8=B0?= =?UTF-8?q?=EC=97=90=20service=20=EC=97=90=EC=84=9C=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EC=A0=81=EC=9D=B8=20=EC=9D=BC=EC=9D=84=20=EC=88=98=ED=96=89?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.service.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 8d3174c4..9ebb714a 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -14,6 +14,20 @@ import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { UserRole } from '@src/user/user.role'; import { Transactional } from 'typeorm-transactional'; +import { MapRepository } from './map.repository'; +import { User } from '../user/entity/user.entity'; +import { MapListResponse } from './dto/MapListResponse'; +import { MapDetailResponse } from './dto/MapDetailResponse'; +import { UserRepository } from '../user/user.repository'; +import { UpdateMapInfoRequest } from './dto/UpdateMapInfoRequest'; +import { CreateMapRequest } from './dto/CreateMapRequest'; +import { MapNotFoundException } from './exception/MapNotFoundException'; +import { DuplicatePlaceToMapException } from './exception/DuplicatePlaceToMapException'; +import { PlaceRepository } from '../place/place.repository'; +import { InvalidPlaceToMapException } from './exception/InvalidPlaceToMapException'; +import { Map } from './entity/map.entity'; +import { Color } from '../place/place.color.enum'; +import { UserNotFoundException } from '@src/map/exception/UserNotFoundException'; @Injectable() export class MapService { @@ -93,7 +107,6 @@ export class MapService { async updateMapVisibility(id: number, isPublic: boolean) { await this.checkExists(id); - await this.checkPublicType(isPublic); return this.mapRepository.update(id, { isPublic }); } @@ -169,13 +182,6 @@ export class MapService { } } - private async checkPublicType(isPublic: any) { - if (typeof isPublic === 'boolean') { - return; - } - throw new TypeException('isPublic', 'boolean', typeof isPublic); - } - async deletePlace(id: number, placeId: number) { const map = await this.mapRepository.findById(id); if (!map) throw new MapNotFoundException(id); From b685d4a0d191b8066aba33bafdd94b8e576a951a Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:58:36 +0900 Subject: [PATCH 030/139] =?UTF-8?q?fix:=20dto=EB=A1=9C=20isPublic=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0=EC=97=90=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EC=98=88=EC=99=B8=20=EC=82=AD=EC=A0=9C=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/exception/TypeException.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 backend/src/map/exception/TypeException.ts diff --git a/backend/src/map/exception/TypeException.ts b/backend/src/map/exception/TypeException.ts deleted file mode 100644 index 09127fdf..00000000 --- a/backend/src/map/exception/TypeException.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BaseException } from '@src/common/exception/BaseException'; -import { HttpStatus } from '@nestjs/common'; - -export class TypeException extends BaseException { - constructor(where: string, toType: string, currentType: string) { - super({ - code: 804, - message: `${where} must be ${toType} not ${currentType}.`, - status: HttpStatus.BAD_REQUEST, - }); - } -} From 192a877466e9d2e449a354305fdc8dd1e746305a Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:47:36 +0900 Subject: [PATCH 031/139] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20=EC=8B=A4=EC=A0=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.service.test.ts | 365 ++++++++++++++------------- 1 file changed, 185 insertions(+), 180 deletions(-) diff --git a/backend/test/map/map.service.test.ts b/backend/test/map/map.service.test.ts index 9080dc4f..c6147e78 100644 --- a/backend/test/map/map.service.test.ts +++ b/backend/test/map/map.service.test.ts @@ -4,116 +4,166 @@ import { PlaceRepository } from '@src/place/place.repository'; import { User } from '@src/user/entity/user.entity'; import { UserFixture } from '@test/user/fixture/user.fixture'; import { + createPlace, + createPrivateMaps, createPublicMaps, createPublicMapsWithTitle, } from '@test/map/map.test.util'; import { Map } from '@src/map/entity/map.entity'; import { MapListResponse } from '@src/map/dto/MapListResponse'; +import { UserRepository } from '@src/user/user.repository'; +import { MySqlContainer, StartedMySqlContainer } from '@testcontainers/mysql'; +import { initDataSource } from '@test/config/datasource.config'; +import { initializeTransactionalContext } from 'typeorm-transactional'; +import { DataSource } from 'typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MapController } from '@src/map/map.controller'; +import { INestApplication } from '@nestjs/common'; import { MapNotFoundException } from '@src/map/exception/MapNotFoundException'; import { MapDetailResponse } from '@src/map/dto/MapDetailResponse'; import { CreateMapRequest } from '@src/map/dto/CreateMapRequest'; import { Color } from '@src/place/place.color.enum'; import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMapException'; import { DuplicatePlaceToMapException } from '@src/map/exception/DuplicatePlaceToMapException'; -import { UserRepository } from '@src/user/user.repository'; -import { createMock } from '@golevelup/ts-jest'; -import { MapPlace } from '@src/map/entity/map-place.entity'; +import { Place } from '@src/place/entity/place.entity'; +import { ConfigModule } from '@nestjs/config'; +import { JWTHelper } from '@src/auth/JWTHelper'; describe('MapService 테스트', () => { + let app: INestApplication; + let container: StartedMySqlContainer; + let dataSource: DataSource; + let mapService: MapService; - let mapRepository: jest.Mocked; - let userRepository: jest.Mocked; - let placeRepository: jest.Mocked; + + let mapRepository: MapRepository; + let userRepository: UserRepository; + let placeRepository: PlaceRepository; + let fakeUser1: User; let page: number; let pageSize: number; - beforeAll(() => { - fakeUser1 = { - id: 1, - ...UserFixture.createUser({ oauthId: 'abc' }), - }; + beforeAll(async () => { + fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); + + container = await new MySqlContainer().withReuse().start(); + dataSource = await initDataSource(container); + initializeTransactionalContext(); + + mapRepository = new MapRepository(dataSource); + placeRepository = new PlaceRepository(dataSource); + userRepository = new UserRepository(dataSource); + mapService = new MapService(mapRepository, userRepository, placeRepository); + + await userRepository.delete({}); + await mapRepository.delete({}); + await placeRepository.delete({}); + await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); + await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); + await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); + + const fakeUser1Entity = await userRepository.save(fakeUser1); + + const places = createPlace(10); + await placeRepository.save(places); [page, pageSize] = [1, 10]; }); beforeEach(async () => { - mapRepository = createMock({ - searchByTitleQuery: jest.fn(), - findAll: jest.fn(), - count: jest.fn(), - findById: jest.fn(), - findByUserId: jest.fn(), - save: jest.fn(), - softDelete: jest.fn(), - update: jest.fn(), - existById: jest.fn(), - }); - userRepository = createMock({ - findByProviderAndOauthId: jest.fn(), - createUser: jest.fn(), - findById: jest.fn(), - existById: jest.fn(), - }); - placeRepository = createMock({ - findByGooglePlaceId: jest.fn(), - findAll: jest.fn(), - searchByNameOrAddressQuery: jest.fn(), - existById: jest.fn(), - }); - mapService = new MapService(mapRepository, userRepository, placeRepository); - userRepository.existById.mockResolvedValue(true); + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot()], + controllers: [MapController], + providers: [ + { + provide: DataSource, + useValue: dataSource, + }, + { + provide: PlaceRepository, + useFactory: (dataSource: DataSource) => + new PlaceRepository(dataSource), + inject: [DataSource], + }, + { + provide: UserRepository, + useFactory: (dataSource: DataSource) => + new UserRepository(dataSource), + inject: [DataSource], + }, + { + provide: MapRepository, + useFactory: (dataSource: DataSource) => new MapRepository(dataSource), + inject: [DataSource], + }, + { + provide: MapService, + useFactory: ( + mapRepository: MapRepository, + userRepository: UserRepository, + placeRepository: PlaceRepository, + ) => new MapService(mapRepository, userRepository, placeRepository), + inject: [MapRepository, UserRepository, PlaceRepository], + }, + JWTHelper, + ], + }).compile(); + app = module.createNestApplication(); + mapService = app.get(MapService); + await mapRepository.delete({}); + await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); + await app.init(); + }); + afterAll(async () => { + await placeRepository.delete({}); + await mapRepository.delete({}); + await userRepository.delete({}); + await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); + await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); + await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); + await dataSource.destroy(); + await app.close(); }); describe('searchMap 메소드 테스트', () => { it('파라미터 중 query 가 없을 경우 공개된 모든 지도를 반환한다.', async () => { - const mockMaps: Map[] = createPublicMaps(5, fakeUser1); - mockMaps.forEach((map) => { - map.mapPlaces = []; + const publicMaps: Map[] = createPublicMaps(5, fakeUser1); + const privateMaps = createPrivateMaps(5, fakeUser1); + const publicMapEntities = await mapRepository.save([...publicMaps]); + await mapRepository.save([...privateMaps]); + publicMapEntities.forEach((publicMapEntity) => { + publicMapEntity.mapPlaces = []; }); - const spyFindAll = mapRepository.findAll.mockResolvedValue(mockMaps); - const spyCount = mapRepository.count.mockResolvedValue(mockMaps.length); + const expected = await Promise.all( + publicMapEntities.map(MapListResponse.from), + ); const result = await mapService.searchMap(undefined, 1, 10); - const expectedMaps = await Promise.all( - mockMaps.map(async (mockMap) => await MapListResponse.from(mockMap)), - ); - expect(spyFindAll).toHaveBeenCalledWith(page, pageSize); + expect(result.maps).toEqual( expect.arrayContaining( - expectedMaps.map((map) => expect.objectContaining(map)), + expected.map((response) => expect.objectContaining(response)), ), ); - expect(spyCount).toHaveBeenCalledWith({ - where: { title: undefined, isPublic: true }, - }); expect(result.currentPage).toEqual(page); - expect(result.totalPages).toEqual(Math.ceil(mockMaps.length / pageSize)); + expect(result.totalPages).toEqual( + Math.ceil(publicMapEntities.length / pageSize), + ); }); it('파라미터 중 쿼리가 있을 경우 해당 제목을 가진 지도들을 반환한다', async () => { const searchTitle = 'cool'; - const mockCoolMaps: Map[] = createPublicMapsWithTitle( + const coolMaps: Map[] = createPublicMapsWithTitle( 5, fakeUser1, 'cool map', ); - mockCoolMaps.forEach((map) => { - map.mapPlaces = []; + const savedMaps = await mapRepository.save([...coolMaps]); + savedMaps.forEach((mapEntity) => { + mapEntity.mapPlaces = []; }); - const spySearchByTitleQuery = jest - .spyOn(mapRepository, 'searchByTitleQuery') - .mockResolvedValue(mockCoolMaps); - const spyCount = jest - .spyOn(mapRepository, 'count') - .mockResolvedValue(mockCoolMaps.length); const expectedMaps = await Promise.all( - mockCoolMaps.map((map) => MapListResponse.from(map)), + savedMaps.map((savedMap) => MapListResponse.from(savedMap)), ); + const result = await mapService.searchMap(searchTitle, 1, 10); - expect(spySearchByTitleQuery).toHaveBeenCalledWith( - searchTitle, - page, - pageSize, - ); - expect(spyCount).toHaveBeenCalledWith({ - where: { title: searchTitle, isPublic: true }, - }); + expect(result.maps).toEqual( expect.arrayContaining( expectedMaps.map((map) => expect.objectContaining(map)), @@ -124,28 +174,14 @@ describe('MapService 테스트', () => { describe('getOwnMaps 메소드 테스트', () => { it('유저 아이디를 파라미터로 받아서 해당 유저의 지도를 반환한다.', async () => { const fakeUserMaps = createPublicMaps(5, fakeUser1); - - fakeUserMaps.forEach((map) => { - map.mapPlaces = []; - }); - const spyFindUserById = - mapRepository.findByUserId.mockResolvedValue(fakeUserMaps); - const spyCount = mapRepository.count.mockResolvedValue( - fakeUserMaps.length, - ); - userRepository.findById.mockResolvedValue(fakeUser1); + const savedMaps = await mapRepository.save([...fakeUserMaps]); + savedMaps.forEach((savedMap) => (savedMap.mapPlaces = [])); const expectedMaps = await Promise.all( fakeUserMaps.map((fakeUserMap) => MapListResponse.from(fakeUserMap)), ); + const result = await mapService.getOwnMaps(fakeUser1.id); - expect(spyFindUserById).toHaveBeenCalledWith( - fakeUser1.id, - page, - pageSize, - ); - expect(spyCount).toHaveBeenCalledWith({ - where: { user: { id: fakeUser1.id } }, - }); + expect(result.maps).toEqual( expect.arrayContaining( expectedMaps.map((map) => expect.objectContaining(map)), @@ -155,188 +191,157 @@ describe('MapService 테스트', () => { }); describe('getMapById 메소드 테스트', () => { it('파라미터로 받은 mapId 로 지도를 찾은 결과가 없을 때 MapNotFoundException 예외를 발생시킨다.', async () => { - const spyFindById = mapRepository.findById.mockResolvedValue(undefined); await expect(mapService.getMapById(1)).rejects.toThrow( MapNotFoundException, ); - expect(spyFindById).toHaveBeenCalledWith(1); }); it('파라미터로 받은 mapId 로 지도를 찾은 결과가 있으면 결과를 반환한다.', async () => { - const publicMaps = createPublicMaps(1, fakeUser1)[0]; - publicMaps.mapPlaces = []; - const spyFindById = mapRepository.findById.mockResolvedValue(publicMaps); - const result = await mapService.getMapById(1); - const expectedMap = await MapDetailResponse.from(publicMaps); - expect(spyFindById).toHaveBeenCalledWith(1); + const publicMap = createPublicMaps(1, fakeUser1)[0]; + const publicMapEntity = await mapRepository.save(publicMap); + publicMapEntity.mapPlaces = []; + const expectedMap = await MapDetailResponse.from(publicMapEntity); + + const result = await mapService.getMapById(publicMapEntity.id); + expect(result).toEqual(expectedMap); }); }); describe('createMap 메소드 테스트', () => { it('파라미터로 받은 유저 아이디로 지도를 생성하고, 지도 id 를 반환한다.', async () => { - const spyOnFindById = - userRepository.findById.mockResolvedValue(fakeUser1); const publicMap = CreateMapRequest.from({ title: 'test map', description: 'This map is test map', isPublic: true, thumbnailUrl: 'basic_thumbnail.jpg', }); - const resolvedMap = publicMap.toEntity(fakeUser1); - resolvedMap.mapPlaces = []; - const spyOnSave = mapRepository.save.mockResolvedValue(resolvedMap); + const result = await mapService.createMap(1, publicMap); - const saveCalledWith = { ...publicMap, user: { id: 1 } }; - expect(spyOnFindById).toHaveBeenCalledWith(1); - expect(spyOnSave).toHaveBeenCalledWith(saveCalledWith); - expect(result).toEqual(expect.objectContaining({ id: undefined })); + + const publicMapEntity = await mapRepository.findById(1); + expect(result).toEqual( + expect.objectContaining({ id: publicMapEntity.id }), + ); }); }); describe('deleteMap 메소드 테스트', () => { it('파라미터로 mapId를 가진 지도가 없다면 MapNotFoundException 에러를 발생시킨다.', async () => { - const spyOnExistById = mapRepository.existById.mockResolvedValue(false); - const spyOnSoftDelete = mapRepository.softDelete; await expect(mapService.deleteMap(1)).rejects.toThrow( MapNotFoundException, ); - expect(spyOnExistById).toHaveBeenCalledWith(1); - expect(spyOnSoftDelete).not.toHaveBeenCalled(); }); - it('파라미터로 mapId를 가진 지도가 있다면 삭제 후 삭제된 지도의 id 를 반환한다.', async () => { - const spyOnExistById = mapRepository.existById.mockResolvedValue(true); - const spyOnSoftDelete = mapRepository.softDelete; + const publicMap = createPublicMaps(1, fakeUser1)[0]; + const publicMapEntity = await mapRepository.save(publicMap); + const result = await mapService.deleteMap(1); - expect(result).toEqual({ id: 1 }); - expect(spyOnExistById).toHaveBeenCalledWith(1); - expect(spyOnSoftDelete).toHaveBeenCalledWith(1); + + expect(result.id).toEqual(publicMapEntity.id); }); }); describe('updateMapInfo 메소드 테스트', () => { it('업데이트 하려는 지도가 없을경우 MapNotFoundException 에러를 발생시킨다.', async () => { - const spyOnExistById = mapRepository.existById.mockResolvedValue(false); - const spyOnUpdate = mapRepository.update; const updateInfo = { title: 'update test title', description: 'update test description', }; + await expect(mapService.updateMapInfo(1, updateInfo)).rejects.toThrow( MapNotFoundException, ); - expect(spyOnExistById).toHaveBeenCalledWith(1); - expect(spyOnUpdate).not.toHaveBeenCalled(); }); it('업데이트 하려는 지도가 있을 경우 지도를 파라미터의 정보로 업데이트 한다.', async () => { - const spyOnExistById = mapRepository.existById.mockResolvedValue(true); - const spyOnUpdate = mapRepository.update; + const publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); const updateInfo = { title: 'update test title', description: 'update test description', }; + await mapService.updateMapInfo(1, updateInfo); - expect(spyOnExistById).toHaveBeenCalledWith(1); - expect(spyOnUpdate).toBeCalledWith(1, updateInfo); + + const publicMapEntity = await mapRepository.findById(1); + expect(publicMapEntity.title).toEqual(updateInfo.title); + expect(publicMapEntity.description).toEqual(updateInfo.description); }); }); describe('updateMapVisibility 메소드 테스트', () => { it('visibility 를 업데이트 하려는 지도가 없을 경우 MapNotFoundException 을 발생시킨다.', async () => { - const spyOnExistById = mapRepository.existById.mockResolvedValue(false); - const spyOnUpdate = mapRepository.update; await expect(mapService.updateMapVisibility(1, true)).rejects.toThrow( MapNotFoundException, ); - expect(spyOnExistById).toHaveBeenCalledWith(1); - expect(spyOnUpdate).not.toHaveBeenCalled(); }); it('visibility를 업데이트 하려는 지도가 있을 경우 업데이트를 진행한다.', async () => { - const spyOnExistById = mapRepository.existById.mockResolvedValue(true); - const spyOnUpdate = mapRepository.update; + const privateMap = createPrivateMaps(1, fakeUser1)[0]; + await mapRepository.save(privateMap); + await mapService.updateMapVisibility(1, true); - expect(spyOnExistById).toHaveBeenCalledWith(1); - expect(spyOnUpdate).toBeCalledWith(1, { isPublic: true }); + + const privateMapEntity = await mapRepository.findById(1); + expect(privateMapEntity.isPublic).toEqual(true); }); }); describe('addPlace 메소드 테스트', () => { it('장소를 추가하려는 지도가 없을 경우 MapNotFoundException 을 발생시킨다.', async () => { - const spyOnFindById = mapRepository.findById.mockResolvedValue(null); - const spyOnSave = mapRepository.save; await expect( mapService.addPlace(1, 2, 'BLUE' as Color, 'test'), ).rejects.toThrow(MapNotFoundException); - expect(spyOnFindById).toHaveBeenCalledWith(1); - expect(spyOnSave).not.toHaveBeenCalled(); }); it('추가하려는 장소가 없을 경우 InvalidPlaceToMapException 를 발생시킨다.', async () => { - const map = createPublicMaps(1, fakeUser1)[0]; - const spyOnFindById = mapRepository.findById.mockResolvedValue(map); - const spyOnPlaceExistById = - placeRepository.existById.mockResolvedValue(false); + const publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); + await expect( - mapService.addPlace(1, 1, 'RED' as Color, 'test'), + mapService.addPlace(1, 777777, 'RED' as Color, 'test'), ).rejects.toThrow(InvalidPlaceToMapException); - expect(spyOnFindById).toHaveBeenCalledWith(1); - expect(spyOnPlaceExistById).toHaveBeenCalled(); }); it('추가하려는 장소가 이미 해당 지도에 있을경우 DuplicatePlaceToMapException 에러를 발생시킨다', async () => { - const map = createPublicMaps(1, fakeUser1)[0]; - map.mapPlaces = []; - const place = new MapPlace(); - place.placeId = 1; - place.color = 'RED' as Color; - place.description = 'test'; - map.mapPlaces.push(place); - const spyOnFindById = mapRepository.findById.mockResolvedValue(map); - const spyOnPlaceExistById = - placeRepository.existById.mockResolvedValue(true); + const publicMap = createPublicMaps(1, fakeUser1)[0]; + const publicMapEntity = await mapRepository.save(publicMap); + const alreadyAddPlace: Place = await placeRepository.findById( + publicMapEntity.id, + ); + publicMapEntity.mapPlaces = []; + publicMapEntity.addPlace(alreadyAddPlace.id, 'RED' as Color, 'test'); + await mapRepository.save(publicMapEntity); + await expect( mapService.addPlace(1, 1, 'RED' as Color, 'test'), ).rejects.toThrow(DuplicatePlaceToMapException); - expect(spyOnPlaceExistById).toHaveBeenCalledWith(1); - expect(spyOnFindById).toHaveBeenCalled(); }); it('장소를 추가하려는 지도가 있을 경우 장소를 추가하고 장소 정보를 다시 반환한다.', async () => { - const map = createPublicMaps(1, fakeUser1)[0]; - map.mapPlaces = []; - const addPlace = { color: 'RED', comment: 'test', placeId: 2 }; - const place = new MapPlace(); - place.placeId = 1; - map.mapPlaces.push(place); - const spyOnFindById = mapRepository.findById.mockResolvedValue(map); - const spyOnPlaceExistById = - placeRepository.existById.mockResolvedValue(true); + const publicMap = createPublicMaps(1, fakeUser1)[0]; + const savedMap = await mapRepository.save(publicMap); + const addPlace = await placeRepository.findById(savedMap.id); + const expectedResult = { + placeId: addPlace.id, + comment: 'test', + color: 'RED' as Color, + }; + const result = await mapService.addPlace( 1, - addPlace.placeId, - addPlace.color as Color, - addPlace.comment, + expectedResult.placeId, + expectedResult.color, + expectedResult.comment, ); - expect(result).toEqual(addPlace); - expect(spyOnFindById).toHaveBeenCalledWith(1); - expect(spyOnPlaceExistById).toHaveBeenCalledWith(addPlace.placeId); + + expect(result).toEqual(expect.objectContaining(expectedResult)); }); }); describe('deletePlace 메소드 테스트', () => { it('장소를 제거하려는 지도가 없을 경우 MapNotFoundException 에러를 발생시킨다.', async () => { - const spyFindById = mapRepository.findById.mockResolvedValue(null); - const spyMapSave = mapRepository.save; await expect(mapService.deletePlace(1, 1)).rejects.toThrow( MapNotFoundException, ); - expect(spyFindById).toHaveBeenCalledWith(1); - expect(spyMapSave).not.toHaveBeenCalled(); }); it('mapId로 받은 지도에서 placeId 를 제거하고 해당 placeId 를 반환한다.', async () => { - const map = createPublicMaps(1, fakeUser1)[0]; - map.mapPlaces = []; - const newPlace = new MapPlace(); - newPlace.placeId = 1; - map.mapPlaces.push(newPlace); + const publicMap = createPublicMaps(1, fakeUser1)[0]; + await mapRepository.save(publicMap); const expectResult = { deletedId: 1 }; - const spyFindById = mapRepository.findById.mockResolvedValue(map); - const spyMapSave = mapRepository.save; + const result = await mapService.deletePlace(1, 1); + expect(result).toEqual(expectResult); - expect(spyFindById).toHaveBeenCalledWith(1); - expect(spyMapSave).toHaveBeenCalled(); }); }); }); From 1e22fc776b19a2b5bf356c9662efca4bfa2b0fbc Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:48:05 +0900 Subject: [PATCH 032/139] =?UTF-8?q?fix:=20createPrivateMaps=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=201=EA=B0=9C=20=EB=8D=94=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=EC=99=80=20=EA=B3=A0?= =?UTF-8?q?=EC=B0=A8=ED=95=A8=EC=88=98=20=EC=82=AC=EC=9A=A9=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.test.util.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/backend/test/map/map.test.util.ts b/backend/test/map/map.test.util.ts index bd121078..3a30d9c8 100644 --- a/backend/test/map/map.test.util.ts +++ b/backend/test/map/map.test.util.ts @@ -28,18 +28,16 @@ export function createPublicMaps(quantity: number, user: User) { return createEntities((i) => createMapWithOptions(i, user), quantity); } -export function createPrivateMaps(quantity: number, user: User) { - const maps = []; - for (let i = 1; i <= quantity + 1; i++) { - const map = MapFixture.createMap({ - user: user, - title: `private test map ${i}`, - isPublic: false, - }); - maps.push(map); - } - return maps; -} +export const createPrivateMaps = (count: number, user: User) => { + return createEntities( + (i) => + createMapWithOptions(i, user, { + isPublic: false, + title: `private test map ${i}`, + }), + count, + ); +}; export const createPlace = (quantity: number) => { return createEntities( From 259ff839f4176aa03b8aaff23dc794144792b82d Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:48:33 +0900 Subject: [PATCH 033/139] =?UTF-8?q?fix:=20given/when/then=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=EB=A1=9C=20=EB=82=98=EB=88=94=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration-test/map.integration.test.ts | 146 +++++++++++++----- 1 file changed, 107 insertions(+), 39 deletions(-) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index 761440ce..28261dfd 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -30,10 +30,10 @@ import { MAP_PERMISSION_EXCEPTION, } from '@test/map/integration-test/map.integration.expectExcptions'; import { createInvalidToken } from '@test/map/integration-test/map.integration.util'; +import { initializeTransactionalContext } from 'typeorm-transactional'; describe('MapController 통합 테스트', () => { let app: INestApplication; - let container: StartedMySqlContainer; let dataSource: DataSource; @@ -45,23 +45,36 @@ describe('MapController 통합 테스트', () => { let fakeUser1: User; let fakeUser2: User; + + let fakeUser1Id: number; + let fakeUser2Id: number; + let jwtHelper: JWTHelper; let token: string; beforeAll(async () => { token = null; + fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); + fakeUser2 = UserFixture.createUser({ oauthId: 'def' }); container = await new MySqlContainer().withReuse().start(); dataSource = await initDataSource(container); + initializeTransactionalContext(); mapRepository = new MapRepository(dataSource); placeRepository = new PlaceRepository(dataSource); userRepository = new UserRepository(dataSource); - fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); - fakeUser2 = UserFixture.createUser({ oauthId: 'def' }); + await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); await userRepository.delete({}); - await userRepository.save([fakeUser1, fakeUser2]); + + const [fakeUser1Entity, fakeUser2Entity] = await userRepository.save([ + fakeUser1, + fakeUser2, + ]); + fakeUser1Id = fakeUser1Entity.id; + fakeUser2Id = fakeUser2Entity.id; + const places = createPlace(10); await placeRepository.save(places); }); @@ -114,10 +127,13 @@ describe('MapController 통합 테스트', () => { }, }) .compile(); + app = module.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ transform: true })); + jwtHelper = app.get(JWTHelper); mapService = app.get(MapService); + await mapRepository.delete({}); token = null; await app.init(); @@ -129,39 +145,42 @@ describe('MapController 통합 테스트', () => { afterAll(async () => { await mapRepository.delete({}); await userRepository.delete({}); + await placeRepository.delete({}); + await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); + await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); + await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); await dataSource.destroy(); + await app.close(); }); describe('getMyMapList 메소드 테스트', () => { it('GET /my 에 대한 요청에 해당 유저 ID 가 가지는 모든 지도의 정보를 반환한다.', async () => { - const fakeUser1 = await userRepository.findById(1); - const fakeUser2 = await userRepository.findById(2); - + const fakeUser1 = await userRepository.findById(fakeUser1Id); + const fakeUser2 = await userRepository.findById(fakeUser2Id); const fakeUserOneMaps = createPublicMaps(3, fakeUser1); const fakeUserTwoMaps = createPublicMaps(3, fakeUser2); await mapRepository.save([...fakeUserOneMaps, ...fakeUserTwoMaps]); - const userInfo = { userId: fakeUser1.id, role: fakeUser1.role, }; token = jwtHelper.generateToken('24h', userInfo); + return request(app.getHttpServer()) .get('/maps/my') .set('Authorization', `Bearer ${token}`) + .expect(200) .then((response) => { const gotMaps = response.body.maps; expect(gotMaps.length).toEqual(fakeUserOneMaps.length); gotMaps.forEach((gotMaps, index) => { const expectedMap = fakeUserOneMaps[index]; - expect(gotMaps.id).toEqual(expectedMap.id); expect(gotMaps.title).toEqual(expectedMap.title); expect(gotMaps.isPublic).toEqual(expectedMap.isPublic); expect(gotMaps.thumbnailUrl).toEqual(expectedMap.thumbnailUrl); expect(gotMaps.description).toEqual(expectedMap.description); expect(gotMaps.pinCount).toEqual(0); - expect(new Date(gotMaps.createdAt).toISOString()).toEqual( new Date(expectedMap.createdAt).toISOString(), ); @@ -175,18 +194,18 @@ describe('MapController 통합 테스트', () => { return request(app.getHttpServer()).get('/maps/my').expect(401); }); it('GET /my 에 대한 요청에 토큰이 만료됐을 경우 AuthenticationException 에러를 발생시킨다.', async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; - token = jwtHelper.generateToken('1s', payload); await new Promise((resolve) => setTimeout(resolve, 1500)); return request(app.getHttpServer()) .get('/maps/my') .set('Authorization', `Bearer ${token}`) + .expect(401) .expect((response) => { expect(response.body).toEqual( @@ -195,23 +214,23 @@ describe('MapController 통합 테스트', () => { }); }); it('GET /my 에 대한 요청에 토큰이 조작됐을 경우 AuthenticationException 에러를 발생시킨다.', async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; - token = jwtHelper.generateToken('24h', payload); const invalidToken = createInvalidToken(token); return request(app.getHttpServer()) .get('/maps/my') .set(`Authorization`, `Bearer ${invalidToken}`) + .expect(401); }); it('GET /my 에 대한 요청에 user id 에 해당하는 유저가 없을 경우 에러를 발생시킨다.', async () => { const invalidUserInfo = { - id: 3, + id: 999999, nickname: 'unknown', provider: 'GOOGLE', role: UserRole.ADMIN, @@ -221,9 +240,11 @@ describe('MapController 통합 테스트', () => { role: invalidUserInfo.role, }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .get('/maps/my') .set(`Authorization`, `Bearer ${token}`) + .expect(404); }); }); @@ -232,8 +253,10 @@ describe('MapController 통합 테스트', () => { const publicMaps = createPublicMaps(5, fakeUser1); const privateMaps = createPrivateMaps(5, fakeUser1); await mapRepository.save([...publicMaps, ...privateMaps]); + return request(app.getHttpServer()) .get('/maps') + .expect((response) => { const gotMaps = response.body.maps; expect(gotMaps.length).toEqual(publicMaps.length); @@ -251,8 +274,10 @@ describe('MapController 통합 테스트', () => { const maps = createPublicMaps(5, fakeUser1); await mapRepository.save([...maps]); const EXPECT_MAP_ID = 3; + return request(app.getHttpServer()) .get(`/maps/${EXPECT_MAP_ID}`) + .expect(200) .expect((response) => { const gotMap = response.body; @@ -271,8 +296,10 @@ describe('MapController 통합 테스트', () => { const maps = createPublicMaps(5, fakeUser1); await mapRepository.save([...maps]); const EXPECT_MAP_ID = 55; + const result = await request(app.getHttpServer()) .get(`/maps/${EXPECT_MAP_ID}`) + .expect(404); expect(result.body).toEqual( expect.objectContaining(MAP_NOT_FOUND_EXCEPTION(EXPECT_MAP_ID)), @@ -289,18 +316,18 @@ describe('MapController 통합 테스트', () => { isPublic: true, thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', }) + .expect(401); expect(result.body).toEqual( expect.objectContaining(EMPTY_TOKEN_EXCEPTION), ); }); it('POST /maps/ 요청에 대해서 조작된 토큰과 함께 요청이 발생할 경우 AuthenticationException 예외를 발생시킨다', async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; - token = jwtHelper.generateToken('24h', payload); const invalidToken = createInvalidToken(token); @@ -313,6 +340,7 @@ describe('MapController 통합 테스트', () => { isPublic: true, thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', }) + .expect(401) .expect((response) => { expect(response.body).toEqual( @@ -321,12 +349,11 @@ describe('MapController 통합 테스트', () => { }); }); it('POST /maps/ 요청에 대해서 만료된 토큰과 함께 요청이 발생할 경우 AuthenticationException 예외를 발생시킨다', async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; - token = jwtHelper.generateToken('1s', payload); await new Promise((resolve) => setTimeout(resolve, 1500)); @@ -339,6 +366,7 @@ describe('MapController 통합 테스트', () => { isPublic: true, thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', }) + .expect(401) .expect((response) => { expect(response.body).toEqual( @@ -347,7 +375,7 @@ describe('MapController 통합 테스트', () => { }); }); it('POST /maps/ 요청에 대해 유저 정보가 없을 경우 UserNotFoundException 에러를 발생시킨다.', async () => { - const INVALID_USER_ID = 3; + const INVALID_USER_ID = 99999; const invalidUserInfo = { id: INVALID_USER_ID, nickname: 'unknown', @@ -359,6 +387,7 @@ describe('MapController 통합 테스트', () => { role: invalidUserInfo.role, }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .post('/maps/') .set(`Authorization`, `Bearer ${token}`) @@ -368,6 +397,7 @@ describe('MapController 통합 테스트', () => { isPublic: true, thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', }) + .expect(404) .expect((response) => { expect(response.body).toEqual( @@ -379,12 +409,11 @@ describe('MapController 통합 테스트', () => { }); }); it('/POST /maps 요청의 Body 에 title 이 없을 경우 Bad Request 예외를 발생시킨다.', async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; - token = jwtHelper.generateToken('24h', payload); return request(app.getHttpServer()) @@ -395,6 +424,7 @@ describe('MapController 통합 테스트', () => { isPublic: true, thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', }) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -406,12 +436,11 @@ describe('MapController 통합 테스트', () => { }); }); it('/POST /maps 요청의 Body 에 description 이 없을 경우 Bad Request 예외를 발생시킨다.', async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; - token = jwtHelper.generateToken('24h', payload); return request(app.getHttpServer()) @@ -422,6 +451,7 @@ describe('MapController 통합 테스트', () => { isPublic: true, thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', }) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -433,12 +463,11 @@ describe('MapController 통합 테스트', () => { }); }); it('/POST /maps 에 올바른 Body 와 유효한 토큰을 설정한 요청에 대해서 적절하게 저장하고, 저장한 지도에 대한 id 를 반환한다.', async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; - token = jwtHelper.generateToken('24h', payload); const testMap = { title: 'Test Map', @@ -446,10 +475,12 @@ describe('MapController 통합 테스트', () => { isPublic: true, thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', }; + return request(app.getHttpServer()) .post('/maps/') .set('Authorization', `Bearer ${token}`) .send(testMap) + .expect(201) .expect(async (response) => { expect(response.body).toEqual( @@ -480,13 +511,14 @@ describe('MapController 통합 테스트', () => { testPlace.color as Color, testPlace.comment, ); - const fakeUserInfo = await userRepository.findById(1); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, }; }); afterEach(async () => { + await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT=1;`); await mapRepository.delete({}); }); it('POST /maps/:id/places 요청의 Body의 placeId의 타입이 number가 아니라면 Bad Request 에러를 발생시킨다.', async () => { @@ -496,10 +528,12 @@ describe('MapController 통합 테스트', () => { comment: 'update test description', color: 'BLUE', }; + return request(app.getHttpServer()) .post('/maps/1/places') .set('Authorization', `Bearer ${token}`) .send(InvalidTestPlace) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -518,13 +552,13 @@ describe('MapController 통합 테스트', () => { comment: 9999999999, color: 'BLUE', }; - token = jwtHelper.generateToken('24h', payload); return request(app.getHttpServer()) .post('/maps/1/places') .set('Authorization', `Bearer ${token}`) .send(InvalidTestPlace) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -547,6 +581,7 @@ describe('MapController 통합 테스트', () => { .post('/maps/1/places') .set('Authorization', `Bearer ${token}`) .send(InvalidTestPlace) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -567,10 +602,12 @@ describe('MapController 통합 테스트', () => { testPlace.color as Color, testPlace.comment, ); + return request(app.getHttpServer()) .post('/maps/1/places') .set('Authorization', `Bearer ${token}`) .send(testPlace) + .expect(409) .expect((response) => { expect(response.body).toEqual( @@ -583,16 +620,18 @@ describe('MapController 통합 테스트', () => { }); }); it('POST /maps/:id/places 요청이 적절한 토큰과 Body를 가지지만 해당 유저의 지도가 아니라면 MapPermissionException 을 발생한다.', async () => { - const fakeUser2 = await userRepository.findById(2); + const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { userId: fakeUser2.id, role: fakeUser2.role, }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .post('/maps/1/places') .set('Authorization', `Bearer ${token}`) .send(testPlace) + .expect(403) .expect((response) => { expect(response.body).toEqual( @@ -607,6 +646,7 @@ describe('MapController 통합 테스트', () => { .post('/maps/1/places') .set('Authorization', `Bearer ${token}`) .send(testPlace) + .expect(201) .expect((response) => { expect(response.body).toEqual(expect.objectContaining(testPlace)); @@ -617,7 +657,7 @@ describe('MapController 통합 테스트', () => { let payload: { userId: number; role: string }; let testPlace: { placeId: number; comment: string; color: string }; beforeEach(async () => { - const fakeUserOneInfo = await userRepository.findById(1); + const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); testPlace = { @@ -636,13 +676,13 @@ describe('MapController 통합 테스트', () => { role: fakeUserOneInfo.role, }; }); - it('DELETE /maps/:id/places/:placeId 요청의 지도의 id 를 찾지 못했을 경우 MapNotFoundException 예외를 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); return request(app.getHttpServer()) .delete('/maps/3/places/1') .set('Authorization', `Bearer ${token}`) + .expect(404) .expect((response) => { expect(response.body).toEqual( @@ -651,7 +691,7 @@ describe('MapController 통합 테스트', () => { }); }); it('DELETE /maps/:id/places/:placeId 요청에 올바른 토큰과 지도 id 를 설정했지만, 해당 유저의 지도가 아닐경우 MapPermissionException 을 발생한다.', async () => { - const fakeUser2 = await userRepository.findById(2); + const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { userId: fakeUser2.id, role: fakeUser2.role, @@ -661,6 +701,7 @@ describe('MapController 통합 테스트', () => { return request(app.getHttpServer()) .delete('/maps/1/places/1') .set('Authorization', `Bearer ${token}`) + .expect(403) .expect(async (response) => { expect(response.body).toEqual( @@ -672,8 +713,9 @@ describe('MapController 통합 테스트', () => { token = jwtHelper.generateToken('24h', payload); return request(app.getHttpServer()) - .delete('/maps/1/places/1') + .delete(`/maps/1/places/1`) .set('Authorization', `Bearer ${token}`) + .expect(200) .expect(async (response) => { expect(response.body).toEqual( @@ -704,7 +746,7 @@ describe('MapController 통합 테스트', () => { testPlace.color as Color, testPlace.comment, ); - const fakeUserInfo = await userRepository.findById(1); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, @@ -718,10 +760,12 @@ describe('MapController 통합 테스트', () => { const updateMapInfo = { description: 'this is updated test map', }; + return request(app.getHttpServer()) .patch('/maps/1/info') .send(updateMapInfo) .set('Authorization', `Bearer ${token}`) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -739,10 +783,12 @@ describe('MapController 통합 테스트', () => { title: 124124, description: 'this is updated test map', }; + return request(app.getHttpServer()) .patch('/maps/1/info') .send(updateMapInfo) .set('Authorization', `Bearer ${token}`) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -760,10 +806,12 @@ describe('MapController 통합 테스트', () => { title: 'updated map title', description: 111111, }; + return request(app.getHttpServer()) .patch('/maps/1/info') .send(updateMapInfo) .set('Authorization', `Bearer ${token}`) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -781,10 +829,12 @@ describe('MapController 통합 테스트', () => { title: 'updated map title', description: 'updated map description', }; + return request(app.getHttpServer()) .patch('/maps/2/info') .send(updateMapInfo) .set('Authorization', `Bearer ${token}`) + .expect(404) .expect((response) => { expect(response.body).toEqual( @@ -793,7 +843,7 @@ describe('MapController 통합 테스트', () => { }); }); it('/PATCH /:id/info 요청에 올바른 토큰과 적절한 요청 body, params 를 가지지만 해당 유저의 지도가 아닐경우 MapPermissionException 을 발생한다.', async () => { - const fakeUser2 = await userRepository.findById(2); + const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { userId: fakeUser2.id, role: fakeUser2.role, @@ -803,10 +853,12 @@ describe('MapController 통합 테스트', () => { title: 'updated map title', description: 'updated map description', }; + return request(app.getHttpServer()) .patch('/maps/1/info') .send(updateMapInfo) .set('Authorization', `Bearer ${token}`) + .expect(403) .expect(async (response) => { expect(response.body).toEqual( @@ -820,10 +872,12 @@ describe('MapController 통합 테스트', () => { title: 'updated map title', description: 'updated map description', }; + return request(app.getHttpServer()) .patch('/maps/1/info') .send(updateMapInfo) .set('Authorization', `Bearer ${token}`) + .expect(200) .expect(async (response) => { expect(response.body).toEqual({ @@ -858,7 +912,7 @@ describe('MapController 통합 테스트', () => { testPlace.color as Color, testPlace.comment, ); - const fakeUserInfo = await userRepository.findById(1); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, @@ -870,10 +924,12 @@ describe('MapController 통합 테스트', () => { it('PATCH /maps/:id/visibility 요청의 body 의 isPublic 이 boolean 이 아닐경우 예외를 발생한다.', async () => { const updateIsPublic = { isPublic: 'NOT BOOLEAN' }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .patch('/maps/1/visibility') .send(updateIsPublic) .set('Authorization', `Bearer ${token}`) + .expect(400) .expect((response) => { expect(response.body).toEqual( @@ -887,10 +943,12 @@ describe('MapController 통합 테스트', () => { it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body가 있을 경우 지도의 id 와 변경된 isPublic 을 반환한다.', async () => { const updateIsPublic = { isPublic: false }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .patch('/maps/1/visibility') .send(updateIsPublic) .set('Authorization', `Bearer ${token}`) + .expect(200) .expect((response) => { expect(response.body).toEqual( @@ -902,17 +960,19 @@ describe('MapController 통합 테스트', () => { }); }); it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body를 가지지만 해당 유저의 지도가 아닐 경우 MapPermissionException 을 발생한다.', async () => { - const fakeUser2 = await userRepository.findById(2); + const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { userId: fakeUser2.id, role: fakeUser2.role, }; const updateIsPublic = { isPublic: false }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .patch('/maps/1/visibility') .send(updateIsPublic) .set('Authorization', `Bearer ${token}`) + .expect(403) .expect((response) => { expect(response.body).toEqual( @@ -923,10 +983,12 @@ describe('MapController 통합 테스트', () => { it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body가 있지만 지도가 없을 경우 MapNotFoundException 을 발생한다.', async () => { const updateIsPublic = { isPublic: false }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .patch('/maps/5/visibility') .send(updateIsPublic) .set('Authorization', `Bearer ${token}`) + .expect(404) .expect((response) => { expect(response.body).toEqual( @@ -941,7 +1003,7 @@ describe('MapController 통합 테스트', () => { beforeEach(async () => { publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); - const fakeUserInfo = await userRepository.findById(1); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, @@ -952,9 +1014,11 @@ describe('MapController 통합 테스트', () => { }); it('DELETE /maps/:id 요청에 적절한 토큰이 있지만 해당하는 지도가 없다면 예외를 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .delete('/maps/5/') .set('Authorization', `Bearer ${token}`) + .expect(404) .expect((response) => { expect(response.body).toEqual( @@ -963,15 +1027,17 @@ describe('MapController 통합 테스트', () => { }); }); it('DELETE /maps/:id 요청에 대해 적절한 토큰이 있지만, 해당 유저의 지도가 아닐 경우 MapPermissionException 을 발생한다.', async () => { - const fakeUser2 = await userRepository.findById(2); + const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { userId: fakeUser2.id, role: fakeUser2.role, }; token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .delete('/maps/1') .set('Authorization', `Bearer ${token}`) + .expect(403) .expect((response) => { expect(response.body).toEqual( @@ -981,9 +1047,11 @@ describe('MapController 통합 테스트', () => { }); it('DELETE /maps/:id 요청에 대해 적절한 토큰이 있고 id 에 해당하는 지도가 있으면 삭제하고 id를 반환한다.', async () => { token = jwtHelper.generateToken('24h', payload); + return request(app.getHttpServer()) .delete('/maps/1') .set('Authorization', `Bearer ${token}`) + .expect(200) .expect((response) => { expect(response.body).toEqual(expect.objectContaining({ id: 1 })); From 9a45c7a165aaff4ec7c831d52c09b220d233d8be Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:09:02 +0900 Subject: [PATCH 034/139] =?UTF-8?q?test:=20=EC=88=98=EC=A0=95=EB=90=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20=EB=94=B0=EB=9D=BC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.controller.ts | 11 ++++----- backend/src/map/map.service.ts | 24 ------------------- .../integration-test/map.integration.test.ts | 22 ----------------- backend/test/map/map.service.test.ts | 17 +++++++------ 4 files changed, 13 insertions(+), 61 deletions(-) diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index c041a0a6..4b76ab95 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -1,14 +1,13 @@ import { - Controller, - Get, - Post, Body, - Query, + Controller, Delete, + Get, Param, Patch, - BadRequestException, + Post, Put, + Query, UseGuards, } from '@nestjs/common'; import { MapService } from './map.service'; @@ -82,7 +81,7 @@ export class MapController { return { mapId: id, placeId, color, comment }; } - @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard, MapPermissionGuard) @Delete('/:id/places/:placeId') async deletePlaceFromMap( @Param('id') id: number, diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 9ebb714a..b41e6ff9 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -12,21 +12,7 @@ import { PlaceRepository } from '@src/place/place.repository'; import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMapException'; import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; -import { UserRole } from '@src/user/user.role'; import { Transactional } from 'typeorm-transactional'; -import { MapRepository } from './map.repository'; -import { User } from '../user/entity/user.entity'; -import { MapListResponse } from './dto/MapListResponse'; -import { MapDetailResponse } from './dto/MapDetailResponse'; -import { UserRepository } from '../user/user.repository'; -import { UpdateMapInfoRequest } from './dto/UpdateMapInfoRequest'; -import { CreateMapRequest } from './dto/CreateMapRequest'; -import { MapNotFoundException } from './exception/MapNotFoundException'; -import { DuplicatePlaceToMapException } from './exception/DuplicatePlaceToMapException'; -import { PlaceRepository } from '../place/place.repository'; -import { InvalidPlaceToMapException } from './exception/InvalidPlaceToMapException'; -import { Map } from './entity/map.entity'; -import { Color } from '../place/place.color.enum'; import { UserNotFoundException } from '@src/map/exception/UserNotFoundException'; @Injectable() @@ -181,14 +167,4 @@ export class MapService { throw new UserNotFoundException(userId); } } - - async deletePlace(id: number, placeId: number) { - const map = await this.mapRepository.findById(id); - if (!map) throw new MapNotFoundException(id); - - map.deletePlace(placeId); - await this.mapRepository.save(map); - - return { deletedId: placeId }; - } } diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index 28261dfd..dccc84f0 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -755,28 +755,6 @@ describe('MapController 통합 테스트', () => { afterEach(async () => { await mapRepository.delete({}); }); - it('/PATCH /:id/info 요청 Body 에 title 이 없다면 예외를 발생한다..', async () => { - token = jwtHelper.generateToken('24h', payload); - const updateMapInfo = { - description: 'this is updated test map', - }; - - return request(app.getHttpServer()) - .patch('/maps/1/info') - .send(updateMapInfo) - .set('Authorization', `Bearer ${token}`) - - .expect(400) - .expect((response) => { - expect(response.body).toEqual( - expect.objectContaining({ - statusCode: 400, - message: ['title should not be empty', 'title must be a string'], - error: 'Bad Request', - }), - ); - }); - }); it('/PATCH /:id/info 요청 Body 에 title 의 타입이 string이 아니라면 예외를 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); const updateMapInfo = { diff --git a/backend/test/map/map.service.test.ts b/backend/test/map/map.service.test.ts index c6147e78..ec18c86e 100644 --- a/backend/test/map/map.service.test.ts +++ b/backend/test/map/map.service.test.ts @@ -28,6 +28,7 @@ import { DuplicatePlaceToMapException } from '@src/map/exception/DuplicatePlaceT import { Place } from '@src/place/entity/place.entity'; import { ConfigModule } from '@nestjs/config'; import { JWTHelper } from '@src/auth/JWTHelper'; +import { UpdateMapInfoRequest } from '@src/map/dto/UpdateMapInfoRequest'; describe('MapService 테스트', () => { let app: INestApplication; @@ -62,7 +63,7 @@ describe('MapService 테스트', () => { await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); - const fakeUser1Entity = await userRepository.save(fakeUser1); + await userRepository.save(fakeUser1); const places = createPlace(10); await placeRepository.save(places); @@ -240,10 +241,9 @@ describe('MapService 테스트', () => { }); describe('updateMapInfo 메소드 테스트', () => { it('업데이트 하려는 지도가 없을경우 MapNotFoundException 에러를 발생시킨다.', async () => { - const updateInfo = { - title: 'update test title', - description: 'update test description', - }; + const updateInfo = new UpdateMapInfoRequest(); + updateInfo.title = 'update test title'; + updateInfo.description = 'update test description'; await expect(mapService.updateMapInfo(1, updateInfo)).rejects.toThrow( MapNotFoundException, @@ -252,10 +252,9 @@ describe('MapService 테스트', () => { it('업데이트 하려는 지도가 있을 경우 지도를 파라미터의 정보로 업데이트 한다.', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); - const updateInfo = { - title: 'update test title', - description: 'update test description', - }; + const updateInfo = new UpdateMapInfoRequest(); + updateInfo.title = 'update test title'; + updateInfo.description = 'update test description'; await mapService.updateMapInfo(1, updateInfo); From b81d6891df4f36916d02647d202f688745d08b67 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:11:15 +0900 Subject: [PATCH 035/139] fix: yarn.lock --- backend/yarn.lock | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/backend/yarn.lock b/backend/yarn.lock index 3476f7e1..06e85483 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -424,6 +424,11 @@ resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== +"@golevelup/ts-jest@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@golevelup/ts-jest/-/ts-jest-0.6.1.tgz#d3aa4aa337d84477b06b0d25a81254da73cccbe9" + integrity sha512-ubs2xao8q2BsFWD6g2GnkWLLIeVHsPQKYMihzk3v0ptegKJhlNRZvk8wodDx2U2MI7cyKh43mueafZ9xpYdwHQ== + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" @@ -5482,7 +5487,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5514,7 +5528,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6160,7 +6181,7 @@ wordwrapjs@^5.1.0: resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-5.1.0.tgz#4c4d20446dcc670b14fa115ef4f8fd9947af2b3a" integrity sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -6178,6 +6199,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" From a9a2772a24d66b99e970ba8dbf75a0590329453b Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:05:16 +0900 Subject: [PATCH 036/139] =?UTF-8?q?fix:=20=EC=9C=A0=EC=A0=80=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=8C.=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.service.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index b41e6ff9..af20791e 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -13,7 +13,6 @@ import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMap import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { Transactional } from 'typeorm-transactional'; -import { UserNotFoundException } from '@src/map/exception/UserNotFoundException'; @Injectable() export class MapService { @@ -45,7 +44,6 @@ export class MapService { async getOwnMaps(userId: number, page: number = 1, pageSize: number = 10) { // Todo. 그룹 기능 추가 - await this.checkUserExist(userId); const totalCount = await this.mapRepository.count({ where: { user: { id: userId } }, }); @@ -71,7 +69,6 @@ export class MapService { } async createMap(userId: number, createMapForm: CreateMapRequest) { - await this.checkUserExist(userId); const user = { id: userId } as User; const map = createMapForm.toEntity(user); return { id: (await this.mapRepository.save(map)).id }; @@ -161,10 +158,4 @@ export class MapService { throw new DuplicatePlaceToMapException(placeId); } } - - private async checkUserExist(userId: number) { - if (!(await this.userRepository.findById(userId))) { - throw new UserNotFoundException(userId); - } - } } From 0958eb63617aed3b5fb3bc453dd94eb71d799277 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:05:33 +0900 Subject: [PATCH 037/139] =?UTF-8?q?test:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=82=AD=EC=A0=9C=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration-test/map.integration.test.ts | 54 ------------------- 1 file changed, 54 deletions(-) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index dccc84f0..7c352958 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -19,7 +19,6 @@ import { createPrivateMaps, createPublicMaps, } from '@test/map/map.test.util'; -import { UserRole } from '@src/user/user.role'; import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { @@ -228,25 +227,6 @@ describe('MapController 통합 테스트', () => { .expect(401); }); - it('GET /my 에 대한 요청에 user id 에 해당하는 유저가 없을 경우 에러를 발생시킨다.', async () => { - const invalidUserInfo = { - id: 999999, - nickname: 'unknown', - provider: 'GOOGLE', - role: UserRole.ADMIN, - }; - const payload = { - userId: invalidUserInfo.id, - role: invalidUserInfo.role, - }; - token = jwtHelper.generateToken('24h', payload); - - return request(app.getHttpServer()) - .get('/maps/my') - .set(`Authorization`, `Bearer ${token}`) - - .expect(404); - }); }); describe('getMapList 메소드 테스트', () => { it('GET maps/ 에 대한 요청으로 공개 되어있는 지도 모두 반환한다.', async () => { @@ -374,40 +354,6 @@ describe('MapController 통합 테스트', () => { ); }); }); - it('POST /maps/ 요청에 대해 유저 정보가 없을 경우 UserNotFoundException 에러를 발생시킨다.', async () => { - const INVALID_USER_ID = 99999; - const invalidUserInfo = { - id: INVALID_USER_ID, - nickname: 'unknown', - provider: 'GOOGLE', - role: UserRole.ADMIN, - }; - const payload = { - userId: invalidUserInfo.id, - role: invalidUserInfo.role, - }; - token = jwtHelper.generateToken('24h', payload); - - return request(app.getHttpServer()) - .post('/maps/') - .set(`Authorization`, `Bearer ${token}`) - .send({ - title: 'Test Map', - description: 'This is a test map.', - isPublic: true, - thumbnailUrl: 'http://example.com/test-map-thumbnail.jpg', - }) - - .expect(404) - .expect((response) => { - expect(response.body).toEqual( - expect.objectContaining({ - statusCode: 404, - message: `id:${INVALID_USER_ID} 유저가 존재하지 않거나 삭제되었습니다.`, - }), - ); - }); - }); it('/POST /maps 요청의 Body 에 title 이 없을 경우 Bad Request 예외를 발생시킨다.', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { From 9f575023b37b5c78d1b5a30fa545490349acd4ae Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:39:31 +0900 Subject: [PATCH 038/139] =?UTF-8?q?test:=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EC=A4=84=20=EB=B0=94=EA=BF=88?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80,=20=EA=B3=B5=ED=86=B5=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=ED=95=A8=EC=88=98=ED=99=94=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration-test/map.integration.test.ts | 85 +++++++++++++++---- .../integration-test/map.integration.util.ts | 14 +++ backend/test/map/map.service.test.ts | 49 ++++++----- 3 files changed, 113 insertions(+), 35 deletions(-) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index 7c352958..7ac37ea2 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -28,7 +28,10 @@ import { MAP_NOT_FOUND_EXCEPTION, MAP_PERMISSION_EXCEPTION, } from '@test/map/integration-test/map.integration.expectExcptions'; -import { createInvalidToken } from '@test/map/integration-test/map.integration.util'; +import { + createInvalidToken, + initMapUserPlaceTable, +} from '@test/map/integration-test/map.integration.util'; import { initializeTransactionalContext } from 'typeorm-transactional'; describe('MapController 통합 테스트', () => { @@ -71,12 +74,14 @@ describe('MapController 통합 테스트', () => { fakeUser1, fakeUser2, ]); + fakeUser1Id = fakeUser1Entity.id; fakeUser2Id = fakeUser2Entity.id; const places = createPlace(10); await placeRepository.save(places); }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ConfigModule.forRoot()], @@ -88,20 +93,15 @@ describe('MapController 통합 테스트', () => { }, { provide: PlaceRepository, - useFactory: (dataSource: DataSource) => - new PlaceRepository(dataSource), - inject: [DataSource], + useValue: placeRepository, }, { provide: UserRepository, - useFactory: (dataSource: DataSource) => - new UserRepository(dataSource), - inject: [DataSource], + useValue: userRepository, }, { provide: MapRepository, - useFactory: (dataSource: DataSource) => new MapRepository(dataSource), - inject: [DataSource], + useValue: mapRepository, }, { provide: MapService, @@ -137,20 +137,19 @@ describe('MapController 통합 테스트', () => { token = null; await app.init(); }); + afterEach(async () => { await mapRepository.delete({}); await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); }); + afterAll(async () => { - await mapRepository.delete({}); - await userRepository.delete({}); - await placeRepository.delete({}); - await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); - await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); - await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); + await initMapUserPlaceTable(mapRepository, userRepository, placeRepository); + await dataSource.destroy(); await app.close(); }); + describe('getMyMapList 메소드 테스트', () => { it('GET /my 에 대한 요청에 해당 유저 ID 가 가지는 모든 지도의 정보를 반환한다.', async () => { const fakeUser1 = await userRepository.findById(fakeUser1Id); @@ -189,9 +188,11 @@ describe('MapController 통합 테스트', () => { }); }); }); + it('GET /my 에 대한 요청에 토큰이 없을 경우 AuthenticationException 에러를 발생시킨다.', async () => { return request(app.getHttpServer()).get('/maps/my').expect(401); }); + it('GET /my 에 대한 요청에 토큰이 만료됐을 경우 AuthenticationException 에러를 발생시킨다.', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { @@ -212,6 +213,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('GET /my 에 대한 요청에 토큰이 조작됐을 경우 AuthenticationException 에러를 발생시킨다.', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { @@ -228,6 +230,7 @@ describe('MapController 통합 테스트', () => { .expect(401); }); }); + describe('getMapList 메소드 테스트', () => { it('GET maps/ 에 대한 요청으로 공개 되어있는 지도 모두 반환한다.', async () => { const publicMaps = createPublicMaps(5, fakeUser1); @@ -249,6 +252,7 @@ describe('MapController 통합 테스트', () => { }); }); }); + describe('getMapDetail 메소드 테스트', () => { it('GET /maps/:id 에 대해서 지도의 id 와 params 의 id 가 일치하는 지도의 정보를 반환한다.', async () => { const maps = createPublicMaps(5, fakeUser1); @@ -272,6 +276,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('GET /maps/:id 요청을 받았을 때 지도의 id 와 params 의 id 가 일치하는 지도가 없을 경우 MapNotFoundException 를 발생시킨다.', async () => { const maps = createPublicMaps(5, fakeUser1); await mapRepository.save([...maps]); @@ -286,6 +291,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + describe('createMap 메소드 테스트', () => { it('POST /maps/ 요청에 대해 토큰이 없을 경우 AuthenticationException 예외를 발생시킨다', async () => { const result = await request(app.getHttpServer()) @@ -302,6 +308,7 @@ describe('MapController 통합 테스트', () => { expect.objectContaining(EMPTY_TOKEN_EXCEPTION), ); }); + it('POST /maps/ 요청에 대해서 조작된 토큰과 함께 요청이 발생할 경우 AuthenticationException 예외를 발생시킨다', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { @@ -328,6 +335,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('POST /maps/ 요청에 대해서 만료된 토큰과 함께 요청이 발생할 경우 AuthenticationException 예외를 발생시킨다', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { @@ -354,6 +362,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('/POST /maps 요청의 Body 에 title 이 없을 경우 Bad Request 예외를 발생시킨다.', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { @@ -381,6 +390,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('/POST /maps 요청의 Body 에 description 이 없을 경우 Bad Request 예외를 발생시킨다.', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { @@ -408,6 +418,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('/POST /maps 에 올바른 Body 와 유효한 토큰을 설정한 요청에 대해서 적절하게 저장하고, 저장한 지도에 대한 id 를 반환한다.', async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const payload = { @@ -439,10 +450,12 @@ describe('MapController 통합 테스트', () => { }); }); }); + describe('addPlaceToMap 메소드 테스트', () => { let publicMap: Map; let testPlace: { placeId: number; comment: string; color: string }; let payload: { userId: number; role: string }; + beforeEach(async () => { publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); @@ -451,22 +464,26 @@ describe('MapController 통합 테스트', () => { comment: 'Beautiful park with a lake', color: 'BLUE', }; + await mapService.addPlace( 1, 1, testPlace.color as Color, testPlace.comment, ); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, }; }); + afterEach(async () => { await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT=1;`); await mapRepository.delete({}); }); + it('POST /maps/:id/places 요청의 Body의 placeId의 타입이 number가 아니라면 Bad Request 에러를 발생시킨다.', async () => { token = jwtHelper.generateToken('24h', payload); const InvalidTestPlace = { @@ -492,6 +509,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('POST /maps/:id/places 요청의 Body의 comment의 타입이 string이 아니라면 Bad Request 에러를 발생시킨다.', async () => { const InvalidTestPlace = { placeId: 5, @@ -515,6 +533,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('POST /maps/:id/places 요청의 Body의 color가 enum(Color) 아니라면 Bad Request 에러를 발생시킨다.', async () => { const InvalidTestPlace = { placeId: 5, @@ -540,6 +559,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('POST /maps/:id/places 요청이 적절한 토큰과 Body를 가지지만 해당 지도에 해당 장소가 이미 있다면 DuplicatePlaceToMapException 에러를 발생시킨다.', async () => { token = jwtHelper.generateToken('24h', payload); await mapService.addPlace( @@ -565,6 +585,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('POST /maps/:id/places 요청이 적절한 토큰과 Body를 가지지만 해당 유저의 지도가 아니라면 MapPermissionException 을 발생한다.', async () => { const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { @@ -585,6 +606,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('POST /maps/:id/places 요청이 적절한 토큰과 Body를 가진다면 해당 지도에 해당 장소를 저장하고 저장된 지도의 정보를 반환한다.', async () => { token = jwtHelper.generateToken('24h', payload); @@ -599,13 +621,16 @@ describe('MapController 통합 테스트', () => { }); }); }); + describe('deletePlaceFromMap 메소드 테스트', () => { let payload: { userId: number; role: string }; let testPlace: { placeId: number; comment: string; color: string }; + beforeEach(async () => { const fakeUserOneInfo = await userRepository.findById(fakeUser1Id); const publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); + testPlace = { placeId: 1, comment: 'Beautiful park with a lake', @@ -617,11 +642,13 @@ describe('MapController 통합 테스트', () => { testPlace.color as Color, testPlace.comment, ); + payload = { userId: fakeUserOneInfo.id, role: fakeUserOneInfo.role, }; }); + it('DELETE /maps/:id/places/:placeId 요청의 지도의 id 를 찾지 못했을 경우 MapNotFoundException 예외를 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); @@ -636,6 +663,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('DELETE /maps/:id/places/:placeId 요청에 올바른 토큰과 지도 id 를 설정했지만, 해당 유저의 지도가 아닐경우 MapPermissionException 을 발생한다.', async () => { const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { @@ -655,6 +683,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('DELETE /maps/:id/places/:placeId 요청에 올바른 토큰과 지도 id 를 설정할 경우 해당 지도에서 placeId 를 삭제하고 해당 placeId 를 반환한다.', async () => { token = jwtHelper.generateToken('24h', payload); @@ -674,13 +703,16 @@ describe('MapController 통합 테스트', () => { }); }); }); + describe('updateMapInfo 메소드 테스트', () => { let publicMap: Map; let testPlace: { placeId: number; comment: string; color: string }; let payload: { userId: number; role: string }; + beforeEach(async () => { publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); + testPlace = { placeId: 5, comment: 'Beautiful park with a lake', @@ -692,15 +724,18 @@ describe('MapController 통합 테스트', () => { testPlace.color as Color, testPlace.comment, ); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, }; }); + afterEach(async () => { await mapRepository.delete({}); }); + it('/PATCH /:id/info 요청 Body 에 title 의 타입이 string이 아니라면 예외를 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); const updateMapInfo = { @@ -724,6 +759,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('/PATCH /:id/info 요청 Body 에 description 의 타입이 string이 아니라면 예외를 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); const updateMapInfo = { @@ -747,6 +783,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('/PATCH /:id/info 요청에 url params 의 지도 id 가 유효하지 않다면 MapNotFoundException 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); const updateMapInfo = { @@ -766,6 +803,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('/PATCH /:id/info 요청에 올바른 토큰과 적절한 요청 body, params 를 가지지만 해당 유저의 지도가 아닐경우 MapPermissionException 을 발생한다.', async () => { const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { @@ -790,6 +828,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('/PATCH /:id/info 요청에 올바른 토큰과 적절한 요청 body, params 를 가진다면 해당 지도의 정보를 업데이트하고 업데이트된 지도의 id 와 정보를 반환한다.', async () => { token = jwtHelper.generateToken('24h', payload); const updateMapInfo = { @@ -818,13 +857,16 @@ describe('MapController 통합 테스트', () => { }); }); }); + describe('updateMapVisibility 메소드 테스트', () => { let publicMap: Map; let testPlace: { placeId: number; comment: string; color: string }; let payload: { userId: number; role: string }; + beforeEach(async () => { publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); + testPlace = { placeId: 5, comment: 'Beautiful park with a lake', @@ -836,15 +878,18 @@ describe('MapController 통합 테스트', () => { testPlace.color as Color, testPlace.comment, ); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, }; }); + afterEach(async () => { await mapRepository.delete({}); }); + it('PATCH /maps/:id/visibility 요청의 body 의 isPublic 이 boolean 이 아닐경우 예외를 발생한다.', async () => { const updateIsPublic = { isPublic: 'NOT BOOLEAN' }; token = jwtHelper.generateToken('24h', payload); @@ -864,6 +909,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body가 있을 경우 지도의 id 와 변경된 isPublic 을 반환한다.', async () => { const updateIsPublic = { isPublic: false }; token = jwtHelper.generateToken('24h', payload); @@ -883,6 +929,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body를 가지지만 해당 유저의 지도가 아닐 경우 MapPermissionException 을 발생한다.', async () => { const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { @@ -904,6 +951,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('PATCH /maps/:id/visibility 요청에 적절한 토큰과 body가 있지만 지도가 없을 경우 MapNotFoundException 을 발생한다.', async () => { const updateIsPublic = { isPublic: false }; token = jwtHelper.generateToken('24h', payload); @@ -921,21 +969,26 @@ describe('MapController 통합 테스트', () => { }); }); }); + describe('deleteMap 메소드 테스트', () => { let publicMap: Map; let payload: { userId: number; role: string }; + beforeEach(async () => { publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); + const fakeUserInfo = await userRepository.findById(fakeUser1Id); payload = { userId: fakeUserInfo.id, role: fakeUserInfo.role, }; }); + afterEach(async () => { await mapRepository.delete({}); }); + it('DELETE /maps/:id 요청에 적절한 토큰이 있지만 해당하는 지도가 없다면 예외를 발생한다.', async () => { token = jwtHelper.generateToken('24h', payload); @@ -950,6 +1003,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('DELETE /maps/:id 요청에 대해 적절한 토큰이 있지만, 해당 유저의 지도가 아닐 경우 MapPermissionException 을 발생한다.', async () => { const fakeUser2 = await userRepository.findById(fakeUser2Id); payload = { @@ -969,6 +1023,7 @@ describe('MapController 통합 테스트', () => { ); }); }); + it('DELETE /maps/:id 요청에 대해 적절한 토큰이 있고 id 에 해당하는 지도가 있으면 삭제하고 id를 반환한다.', async () => { token = jwtHelper.generateToken('24h', payload); diff --git a/backend/test/map/integration-test/map.integration.util.ts b/backend/test/map/integration-test/map.integration.util.ts index 7b6ee666..a3a37165 100644 --- a/backend/test/map/integration-test/map.integration.util.ts +++ b/backend/test/map/integration-test/map.integration.util.ts @@ -86,3 +86,17 @@ export function createInvalidToken(validToken: string): string { ).toString('base64'); return [header, manipulatedPayload, signature].join('.'); } + +export async function initMapUserPlaceTable( + mapRepository: MapRepository, + userRepository: UserRepository, + placeRepository: PlaceRepository, +) { + await mapRepository.delete({}); + await userRepository.delete({}); + await placeRepository.delete({}); + + await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); + await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); + await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); +} diff --git a/backend/test/map/map.service.test.ts b/backend/test/map/map.service.test.ts index ec18c86e..d76b0c28 100644 --- a/backend/test/map/map.service.test.ts +++ b/backend/test/map/map.service.test.ts @@ -29,6 +29,7 @@ import { Place } from '@src/place/entity/place.entity'; import { ConfigModule } from '@nestjs/config'; import { JWTHelper } from '@src/auth/JWTHelper'; import { UpdateMapInfoRequest } from '@src/map/dto/UpdateMapInfoRequest'; +import { initMapUserPlaceTable } from '@test/map/integration-test/map.integration.util'; describe('MapService 테스트', () => { let app: INestApplication; @@ -44,6 +45,7 @@ describe('MapService 테스트', () => { let fakeUser1: User; let page: number; let pageSize: number; + beforeAll(async () => { fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); @@ -56,12 +58,7 @@ describe('MapService 테스트', () => { userRepository = new UserRepository(dataSource); mapService = new MapService(mapRepository, userRepository, placeRepository); - await userRepository.delete({}); - await mapRepository.delete({}); - await placeRepository.delete({}); - await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); - await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); - await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); + await initMapUserPlaceTable(mapRepository, userRepository, placeRepository); await userRepository.save(fakeUser1); @@ -69,6 +66,7 @@ describe('MapService 테스트', () => { await placeRepository.save(places); [page, pageSize] = [1, 10]; }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ConfigModule.forRoot()], @@ -80,20 +78,15 @@ describe('MapService 테스트', () => { }, { provide: PlaceRepository, - useFactory: (dataSource: DataSource) => - new PlaceRepository(dataSource), - inject: [DataSource], + useValue: placeRepository, }, { provide: UserRepository, - useFactory: (dataSource: DataSource) => - new UserRepository(dataSource), - inject: [DataSource], + useValue: userRepository, }, { provide: MapRepository, - useFactory: (dataSource: DataSource) => new MapRepository(dataSource), - inject: [DataSource], + useValue: mapRepository, }, { provide: MapService, @@ -107,22 +100,21 @@ describe('MapService 테스트', () => { JWTHelper, ], }).compile(); + app = module.createNestApplication(); mapService = app.get(MapService); await mapRepository.delete({}); await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); await app.init(); }); + afterAll(async () => { - await placeRepository.delete({}); - await mapRepository.delete({}); - await userRepository.delete({}); - await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); - await mapRepository.query(`ALTER TABLE MAP AUTO_INCREMENT = 1`); - await placeRepository.query(`ALTER TABLE PLACE AUTO_INCREMENT = 1`); + await initMapUserPlaceTable(mapRepository, userRepository, placeRepository); + await dataSource.destroy(); await app.close(); }); + describe('searchMap 메소드 테스트', () => { it('파라미터 중 query 가 없을 경우 공개된 모든 지도를 반환한다.', async () => { const publicMaps: Map[] = createPublicMaps(5, fakeUser1); @@ -148,6 +140,7 @@ describe('MapService 테스트', () => { Math.ceil(publicMapEntities.length / pageSize), ); }); + it('파라미터 중 쿼리가 있을 경우 해당 제목을 가진 지도들을 반환한다', async () => { const searchTitle = 'cool'; const coolMaps: Map[] = createPublicMapsWithTitle( @@ -172,6 +165,7 @@ describe('MapService 테스트', () => { ); }); }); + describe('getOwnMaps 메소드 테스트', () => { it('유저 아이디를 파라미터로 받아서 해당 유저의 지도를 반환한다.', async () => { const fakeUserMaps = createPublicMaps(5, fakeUser1); @@ -190,12 +184,14 @@ describe('MapService 테스트', () => { ); }); }); + describe('getMapById 메소드 테스트', () => { it('파라미터로 받은 mapId 로 지도를 찾은 결과가 없을 때 MapNotFoundException 예외를 발생시킨다.', async () => { await expect(mapService.getMapById(1)).rejects.toThrow( MapNotFoundException, ); }); + it('파라미터로 받은 mapId 로 지도를 찾은 결과가 있으면 결과를 반환한다.', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; const publicMapEntity = await mapRepository.save(publicMap); @@ -207,6 +203,7 @@ describe('MapService 테스트', () => { expect(result).toEqual(expectedMap); }); }); + describe('createMap 메소드 테스트', () => { it('파라미터로 받은 유저 아이디로 지도를 생성하고, 지도 id 를 반환한다.', async () => { const publicMap = CreateMapRequest.from({ @@ -224,12 +221,14 @@ describe('MapService 테스트', () => { ); }); }); + describe('deleteMap 메소드 테스트', () => { it('파라미터로 mapId를 가진 지도가 없다면 MapNotFoundException 에러를 발생시킨다.', async () => { await expect(mapService.deleteMap(1)).rejects.toThrow( MapNotFoundException, ); }); + it('파라미터로 mapId를 가진 지도가 있다면 삭제 후 삭제된 지도의 id 를 반환한다.', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; const publicMapEntity = await mapRepository.save(publicMap); @@ -239,6 +238,7 @@ describe('MapService 테스트', () => { expect(result.id).toEqual(publicMapEntity.id); }); }); + describe('updateMapInfo 메소드 테스트', () => { it('업데이트 하려는 지도가 없을경우 MapNotFoundException 에러를 발생시킨다.', async () => { const updateInfo = new UpdateMapInfoRequest(); @@ -249,6 +249,7 @@ describe('MapService 테스트', () => { MapNotFoundException, ); }); + it('업데이트 하려는 지도가 있을 경우 지도를 파라미터의 정보로 업데이트 한다.', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); @@ -263,12 +264,14 @@ describe('MapService 테스트', () => { expect(publicMapEntity.description).toEqual(updateInfo.description); }); }); + describe('updateMapVisibility 메소드 테스트', () => { it('visibility 를 업데이트 하려는 지도가 없을 경우 MapNotFoundException 을 발생시킨다.', async () => { await expect(mapService.updateMapVisibility(1, true)).rejects.toThrow( MapNotFoundException, ); }); + it('visibility를 업데이트 하려는 지도가 있을 경우 업데이트를 진행한다.', async () => { const privateMap = createPrivateMaps(1, fakeUser1)[0]; await mapRepository.save(privateMap); @@ -279,12 +282,14 @@ describe('MapService 테스트', () => { expect(privateMapEntity.isPublic).toEqual(true); }); }); + describe('addPlace 메소드 테스트', () => { it('장소를 추가하려는 지도가 없을 경우 MapNotFoundException 을 발생시킨다.', async () => { await expect( mapService.addPlace(1, 2, 'BLUE' as Color, 'test'), ).rejects.toThrow(MapNotFoundException); }); + it('추가하려는 장소가 없을 경우 InvalidPlaceToMapException 를 발생시킨다.', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); @@ -293,6 +298,7 @@ describe('MapService 테스트', () => { mapService.addPlace(1, 777777, 'RED' as Color, 'test'), ).rejects.toThrow(InvalidPlaceToMapException); }); + it('추가하려는 장소가 이미 해당 지도에 있을경우 DuplicatePlaceToMapException 에러를 발생시킨다', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; const publicMapEntity = await mapRepository.save(publicMap); @@ -307,6 +313,7 @@ describe('MapService 테스트', () => { mapService.addPlace(1, 1, 'RED' as Color, 'test'), ).rejects.toThrow(DuplicatePlaceToMapException); }); + it('장소를 추가하려는 지도가 있을 경우 장소를 추가하고 장소 정보를 다시 반환한다.', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; const savedMap = await mapRepository.save(publicMap); @@ -327,12 +334,14 @@ describe('MapService 테스트', () => { expect(result).toEqual(expect.objectContaining(expectedResult)); }); }); + describe('deletePlace 메소드 테스트', () => { it('장소를 제거하려는 지도가 없을 경우 MapNotFoundException 에러를 발생시킨다.', async () => { await expect(mapService.deletePlace(1, 1)).rejects.toThrow( MapNotFoundException, ); }); + it('mapId로 받은 지도에서 placeId 를 제거하고 해당 placeId 를 반환한다.', async () => { const publicMap = createPublicMaps(1, fakeUser1)[0]; await mapRepository.save(publicMap); From a83a00a95b6ea57dc232a756dcd91992b7db0d32 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:44:08 +0900 Subject: [PATCH 039/139] =?UTF-8?q?fix:=20course=20=EB=B0=98=ED=99=98=2015?= =?UTF-8?q?=EA=B0=9C=EB=A1=9C=20=EC=A6=9D=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/course.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/course/course.controller.ts b/backend/src/course/course.controller.ts index 2d41333f..92b7c6d0 100644 --- a/backend/src/course/course.controller.ts +++ b/backend/src/course/course.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Delete, @@ -9,7 +10,6 @@ import { Put, Query, UseGuards, - BadRequestException, } from '@nestjs/common'; import { CreateCourseRequest } from './dto/CreateCourseRequest'; import { UpdateCourseInfoRequest } from './dto/UpdateCourseInfoRequest'; @@ -30,7 +30,7 @@ export class CourseController { async getCourseList( @Query('query') query?: string, @Query('page', new ParseOptionalNumberPipe(1)) page?: number, - @Query('limit', new ParseOptionalNumberPipe(10)) limit?: number, + @Query('limit', new ParseOptionalNumberPipe(15)) limit?: number, ) { return await this.courseService.searchPublicCourses(query, page, limit); } From 06ee97c0a398a975fcad1e64b875a7f294174801 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:08:00 +0900 Subject: [PATCH 040/139] =?UTF-8?q?fix:=20=EC=93=B0=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 4714ad74..a1721e82 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,6 @@ "@nestjs/schedule": "^4.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", - "@golevelup/ts-jest": "^0.6.1", "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.2", "mysql2": "^3.11.3", From c611c0f9834917c5cbc633acdd2f1e6a818d34fb Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:12:45 +0900 Subject: [PATCH 041/139] fix: yarn.lock --- backend/yarn.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/yarn.lock b/backend/yarn.lock index 06e85483..119e598d 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -424,11 +424,6 @@ resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== -"@golevelup/ts-jest@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@golevelup/ts-jest/-/ts-jest-0.6.1.tgz#d3aa4aa337d84477b06b0d25a81254da73cccbe9" - integrity sha512-ubs2xao8q2BsFWD6g2GnkWLLIeVHsPQKYMihzk3v0ptegKJhlNRZvk8wodDx2U2MI7cyKh43mueafZ9xpYdwHQ== - "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" From f4c59295b6985d50926d801d555d9de2411e0526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EA=B8=88=EC=9E=A5?= <90228925+koomchang@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:34:04 +0900 Subject: [PATCH 042/139] =?UTF-8?q?docs:=20README=EC=97=90=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EC=82=AC=EC=A7=84=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2338819a..96dc4e9f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ >개인적인 기억과 감상을 담은 지도를 통해 새로운 발견을 하고, 좋아하는 사람들을 팔로우하며 일상의 영감을 얻는 소셜 지도 서비스입니다. +### 시스템 아키텍처 + + +
+
|🏷️ 바로가기| [**팀 Notion**](https://elastic-bread-9ef.notion.site/12963e6f4ee98074b6f9f70cfa9ac836) | [그라운드 룰](https://github.com/boostcampwm-2024/web09-DailyRoad/wiki/%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%A3%B0) | [컨벤션](#) | [기획/디자인](#) | [문서](#) | From 143aa6445277a0a698575985540f2cfef1487fbc Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 01:50:20 +0900 Subject: [PATCH 043/139] =?UTF-8?q?reafactor:=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=95=9E?= =?UTF-8?q?=EC=97=90=20`E`=20prefix=20=EB=B6=99=EC=9D=B4=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/common/exception/BaseException.ts | 4 ++-- ...lasticSearchException.ts => ElasticSearchSaveException.ts} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename backend/src/search/exception/{ElasticSearchException.ts => ElasticSearchSaveException.ts} (100%) diff --git a/backend/src/common/exception/BaseException.ts b/backend/src/common/exception/BaseException.ts index 5ee5b3f3..02ac6fa5 100644 --- a/backend/src/common/exception/BaseException.ts +++ b/backend/src/common/exception/BaseException.ts @@ -9,8 +9,8 @@ export class BaseException extends HttpException { this.code = exceptionType.code; } - getCode(): number { - return this.code; + getCode(): string { + return `E${this.code}`; } getMessage(): string { diff --git a/backend/src/search/exception/ElasticSearchException.ts b/backend/src/search/exception/ElasticSearchSaveException.ts similarity index 100% rename from backend/src/search/exception/ElasticSearchException.ts rename to backend/src/search/exception/ElasticSearchSaveException.ts From 738f62ffd631a4ff2df7699a0d4495fdc2f0bebe Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 01:50:46 +0900 Subject: [PATCH 044/139] =?UTF-8?q?refactor:=20=EC=9D=B8=EC=A6=9D/?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=84=B8=EB=B6=84=ED=99=94=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=90=EA=B2=80=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/auth/exception/AuthenticationException.ts | 2 +- backend/src/auth/exception/AuthorizationException.ts | 2 +- backend/src/auth/exception/ExpiredTokenException.ts | 12 ++++++++++++ backend/src/auth/exception/InvalidTokenException.ts | 12 ++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 backend/src/auth/exception/ExpiredTokenException.ts create mode 100644 backend/src/auth/exception/InvalidTokenException.ts diff --git a/backend/src/auth/exception/AuthenticationException.ts b/backend/src/auth/exception/AuthenticationException.ts index ca94cfcc..595ade33 100644 --- a/backend/src/auth/exception/AuthenticationException.ts +++ b/backend/src/auth/exception/AuthenticationException.ts @@ -4,7 +4,7 @@ import { HttpStatus } from '@nestjs/common'; export class AuthenticationException extends BaseException { constructor(message: string = '인증에 실패했습니다.') { super({ - code: 601, + code: 500, message: message, status: HttpStatus.UNAUTHORIZED, }); diff --git a/backend/src/auth/exception/AuthorizationException.ts b/backend/src/auth/exception/AuthorizationException.ts index f78ad5cf..20984d4f 100644 --- a/backend/src/auth/exception/AuthorizationException.ts +++ b/backend/src/auth/exception/AuthorizationException.ts @@ -4,7 +4,7 @@ import { HttpStatus } from '@nestjs/common'; export class AuthorizationException extends BaseException { constructor(message: string = '해당 작업에 대한 권한이 없습니다.') { super({ - code: 602, + code: 510, message: message, status: HttpStatus.FORBIDDEN, }); diff --git a/backend/src/auth/exception/ExpiredTokenException.ts b/backend/src/auth/exception/ExpiredTokenException.ts new file mode 100644 index 00000000..ce3b18fc --- /dev/null +++ b/backend/src/auth/exception/ExpiredTokenException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class ExpiredTokenException extends BaseException { + constructor(message: string = '만료된 토큰입니다.') { + super({ + code: 502, + message: message, + status: HttpStatus.UNAUTHORIZED, + }); + } +} diff --git a/backend/src/auth/exception/InvalidTokenException.ts b/backend/src/auth/exception/InvalidTokenException.ts new file mode 100644 index 00000000..dd19e84d --- /dev/null +++ b/backend/src/auth/exception/InvalidTokenException.ts @@ -0,0 +1,12 @@ +import { BaseException } from '@src/common/exception/BaseException'; +import { HttpStatus } from '@nestjs/common'; + +export class InvalidTokenException extends BaseException { + constructor(message: string = '유효하지 않은 토큰입니다.') { + super({ + code: 501, + message: message, + status: HttpStatus.UNAUTHORIZED, + }); + } +} From 8de26b243dbcaf15d7d91d5082190366d0bbefd4 Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 01:51:22 +0900 Subject: [PATCH 045/139] =?UTF-8?q?refactor:=20=EC=BD=94=EC=8A=A4=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/course/exception/ConsecutivePlaceException.ts | 4 ++-- backend/src/course/exception/CourseNotFoundException.ts | 4 ++-- backend/src/course/exception/CoursePermissionException.ts | 2 +- backend/src/course/exception/InvalidPlaceToCourseException.ts | 4 ++-- .../src/course/exception/PlaceInCourseNotFoundException.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/src/course/exception/ConsecutivePlaceException.ts b/backend/src/course/exception/ConsecutivePlaceException.ts index 5e8e8101..1d72d219 100644 --- a/backend/src/course/exception/ConsecutivePlaceException.ts +++ b/backend/src/course/exception/ConsecutivePlaceException.ts @@ -1,10 +1,10 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class ConsecutivePlaceException extends BaseException { constructor() { super({ - code: 904, + code: 405, message: '동일한 장소는 연속된 순서로 추가할 수 없습니다.', status: HttpStatus.BAD_REQUEST, }); diff --git a/backend/src/course/exception/CourseNotFoundException.ts b/backend/src/course/exception/CourseNotFoundException.ts index b71684ad..bea00438 100644 --- a/backend/src/course/exception/CourseNotFoundException.ts +++ b/backend/src/course/exception/CourseNotFoundException.ts @@ -1,10 +1,10 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class CourseNotFoundException extends BaseException { constructor(id: number) { super({ - code: 902, + code: 401, message: `id:${id} 코스가 존재하지 않거나 삭제되었습니다.`, status: HttpStatus.NOT_FOUND, }); diff --git a/backend/src/course/exception/CoursePermissionException.ts b/backend/src/course/exception/CoursePermissionException.ts index d848939a..998f257b 100644 --- a/backend/src/course/exception/CoursePermissionException.ts +++ b/backend/src/course/exception/CoursePermissionException.ts @@ -4,7 +4,7 @@ import { HttpStatus } from '@nestjs/common'; export class CoursePermissionException extends BaseException { constructor(id: number) { super({ - code: 905, + code: 406, message: `id:${id} 코스에 대한 권한이 없습니다.`, status: HttpStatus.FORBIDDEN, }); diff --git a/backend/src/course/exception/InvalidPlaceToCourseException.ts b/backend/src/course/exception/InvalidPlaceToCourseException.ts index 4ae815b5..e0867371 100644 --- a/backend/src/course/exception/InvalidPlaceToCourseException.ts +++ b/backend/src/course/exception/InvalidPlaceToCourseException.ts @@ -1,10 +1,10 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class InvalidPlaceToCourseException extends BaseException { constructor(invalidPlaceIds: number[]) { super({ - code: 903, + code: 404, message: `존재하지 않는 장소를 코스에 추가할 수 없습니다. : ${invalidPlaceIds.join(', ')}`, status: HttpStatus.BAD_REQUEST, }); diff --git a/backend/src/course/exception/PlaceInCourseNotFoundException.ts b/backend/src/course/exception/PlaceInCourseNotFoundException.ts index b53ea06e..196caad3 100644 --- a/backend/src/course/exception/PlaceInCourseNotFoundException.ts +++ b/backend/src/course/exception/PlaceInCourseNotFoundException.ts @@ -4,7 +4,7 @@ import { HttpStatus } from '@nestjs/common'; export class PlaceInCourseNotFoundException extends BaseException { constructor(mapId: number, mapPlaceId: number) { super({ - code: 906, + code: 402, message: `[${mapId}] 코스에 [${mapPlaceId}] 장소가 존재하지 않거나 삭제되었습니다.`, status: HttpStatus.NOT_FOUND, }); From 920628c00850479f4277db32b3393c410a30ffce Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 01:51:35 +0900 Subject: [PATCH 046/139] =?UTF-8?q?refactor:=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/exception/PlaceInMapNotFoundException.ts | 2 +- backend/src/place/exception/PlaceAlreadyExistsException.ts | 4 ++-- backend/src/place/exception/PlaceNotFoundException.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/map/exception/PlaceInMapNotFoundException.ts b/backend/src/map/exception/PlaceInMapNotFoundException.ts index 57be2b13..43d3c901 100644 --- a/backend/src/map/exception/PlaceInMapNotFoundException.ts +++ b/backend/src/map/exception/PlaceInMapNotFoundException.ts @@ -4,7 +4,7 @@ import { HttpStatus } from '@nestjs/common'; export class PlaceInMapNotFoundException extends BaseException { constructor(mapId: number, mapPlaceId: number) { super({ - code: 806, + code: 302, message: `[${mapId}] 지도에 [${mapPlaceId}] 장소가 존재하지 않거나 삭제되었습니다.`, status: HttpStatus.NOT_FOUND, }); diff --git a/backend/src/place/exception/PlaceAlreadyExistsException.ts b/backend/src/place/exception/PlaceAlreadyExistsException.ts index bf3f8ead..63bdb539 100644 --- a/backend/src/place/exception/PlaceAlreadyExistsException.ts +++ b/backend/src/place/exception/PlaceAlreadyExistsException.ts @@ -1,10 +1,10 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class PlaceAlreadyExistsException extends BaseException { constructor() { super({ - code: 1001, + code: 203, message: '장소가 이미 존재합니다.', status: HttpStatus.CONFLICT, }); diff --git a/backend/src/place/exception/PlaceNotFoundException.ts b/backend/src/place/exception/PlaceNotFoundException.ts index 167e4af8..aa29ba79 100644 --- a/backend/src/place/exception/PlaceNotFoundException.ts +++ b/backend/src/place/exception/PlaceNotFoundException.ts @@ -1,4 +1,4 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class PlaceNotFoundException extends BaseException { @@ -7,7 +7,7 @@ export class PlaceNotFoundException extends BaseException { ? `id:${id} 장소가 존재하지 않습니다.` : '장소가 존재하지 않습니다.'; super({ - code: 1002, + code: 201, message, status: HttpStatus.NOT_FOUND, }); From 298fea5811e9341761fb8c72c8c5309378e5bb55 Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 01:51:47 +0900 Subject: [PATCH 047/139] =?UTF-8?q?refactor:=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/exception/DuplicatePlaceToMapException.ts | 4 ++-- backend/src/map/exception/InvalidPlaceToMapException.ts | 4 ++-- backend/src/map/exception/MapNotFoundException.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/map/exception/DuplicatePlaceToMapException.ts b/backend/src/map/exception/DuplicatePlaceToMapException.ts index fb66e210..88ae7d3b 100644 --- a/backend/src/map/exception/DuplicatePlaceToMapException.ts +++ b/backend/src/map/exception/DuplicatePlaceToMapException.ts @@ -1,10 +1,10 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class DuplicatePlaceToMapException extends BaseException { constructor(id: number) { super({ - code: 805, + code: 303, message: '이미 지도에 존재하는 장소입니다. : ' + id, status: HttpStatus.CONFLICT, }); diff --git a/backend/src/map/exception/InvalidPlaceToMapException.ts b/backend/src/map/exception/InvalidPlaceToMapException.ts index 4544969c..51898c4e 100644 --- a/backend/src/map/exception/InvalidPlaceToMapException.ts +++ b/backend/src/map/exception/InvalidPlaceToMapException.ts @@ -1,10 +1,10 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class InvalidPlaceToMapException extends BaseException { constructor(invalidPlaceId: number) { super({ - code: 803, + code: 304, message: `존재하지 않는 장소를 지도에 추가할 수 없습니다. : ${invalidPlaceId}`, status: HttpStatus.BAD_REQUEST, }); diff --git a/backend/src/map/exception/MapNotFoundException.ts b/backend/src/map/exception/MapNotFoundException.ts index d2577510..511d59ab 100644 --- a/backend/src/map/exception/MapNotFoundException.ts +++ b/backend/src/map/exception/MapNotFoundException.ts @@ -1,10 +1,10 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class MapNotFoundException extends BaseException { constructor(id: number) { super({ - code: 802, + code: 301, message: `id:${id} 지도가 존재하지 않거나 삭제되었습니다.`, status: HttpStatus.NOT_FOUND, }); From a748c7cc6e2a1657f9a25d48a9842a88f186ffbc Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 01:52:11 +0900 Subject: [PATCH 048/139] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5/?= =?UTF-8?q?=EC=9C=A0=EC=A0=80/=EC=99=B8=EB=B6=80=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=A0=90?= =?UTF-8?q?=EA=B2=80=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/common/exception/EmptyRequestException.ts | 2 +- .../src/search/exception/ElasticSearchSaveException.ts | 8 +++----- backend/src/search/search.service.ts | 4 ++-- .../src/storage/exception/CloudFunctionsFetchException.ts | 8 ++++---- backend/src/user/exception/UserNotFoundException.ts | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/backend/src/common/exception/EmptyRequestException.ts b/backend/src/common/exception/EmptyRequestException.ts index 7b7798a5..60149d36 100644 --- a/backend/src/common/exception/EmptyRequestException.ts +++ b/backend/src/common/exception/EmptyRequestException.ts @@ -4,7 +4,7 @@ import { HttpStatus } from '@nestjs/common'; export class EmptyRequestException extends BaseException { constructor(action: string = '작업') { super({ - code: 4002, + code: 601, message: `${action}에 필요한 정보가 없습니다.`, status: HttpStatus.BAD_REQUEST, }); diff --git a/backend/src/search/exception/ElasticSearchSaveException.ts b/backend/src/search/exception/ElasticSearchSaveException.ts index a224e91a..13d3e295 100644 --- a/backend/src/search/exception/ElasticSearchSaveException.ts +++ b/backend/src/search/exception/ElasticSearchSaveException.ts @@ -1,13 +1,11 @@ import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; -export class ElasticSearchException extends BaseException { +export class ElasticSearchSaveException extends BaseException { constructor(id?: number) { - const message = id - ? `id:${id} ElasticSearch에 데이터를 저장하는데 실패했습니다.` - : 'ElasticSearch에 데이터를 저장하는데 실패했습니다.'; + const message = `${id ? `id:${id}` : ''} ElasticSearch에 데이터를 저장하는데 실패했습니다.`; super({ - code: 1003, + code: 3001, message, status: HttpStatus.INTERNAL_SERVER_ERROR, }); diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index 47f0e1fa..b4c5516d 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -3,7 +3,7 @@ import { PlaceSearchResponse } from '@src/search/dto/PlaceSearchResponse'; import { PlaceSearchHit } from '@src/search/search.type'; import { ElasticSearchQuery } from '@src/search/query/ElasticSearchQuery'; import { ElasticsearchService } from '@nestjs/elasticsearch'; -import { ElasticSearchException } from '@src/search/exception/ElasticSearchException'; +import { ElasticSearchSaveException } from '@src/search/exception/ElasticSearchSaveException'; import { Place } from '@src/place/entity/place.entity'; import { ESPlaceSaveDTO } from '@src/search/dto/ESPlaceSaveDTO'; import { ElasticSearchConfig } from '@src/config/ElasticSearchConfig'; @@ -29,7 +29,7 @@ export class SearchService { this.logger.error( `Elasticsearch에 장소 저장 중 에러가 발생했습니다: ${error}`, ); - throw new ElasticSearchException(place.id); + throw new ElasticSearchSaveException(place.id); } } diff --git a/backend/src/storage/exception/CloudFunctionsFetchException.ts b/backend/src/storage/exception/CloudFunctionsFetchException.ts index 410ef059..579ffb18 100644 --- a/backend/src/storage/exception/CloudFunctionsFetchException.ts +++ b/backend/src/storage/exception/CloudFunctionsFetchException.ts @@ -1,12 +1,12 @@ -import { BaseException } from '../../common/exception/BaseException'; +import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class CloudFunctionsFetchException extends BaseException { constructor(error: Error) { super({ - code: 777, - message: `cloud function 과 fetch 중 오류가 발생했습니다. : ${error.message}`, - status: HttpStatus.CONFLICT, + code: 711, + message: `Cloud function 과 Fetch 중 오류가 발생했습니다. : ${error.message}`, + status: HttpStatus.INTERNAL_SERVER_ERROR, }); } } diff --git a/backend/src/user/exception/UserNotFoundException.ts b/backend/src/user/exception/UserNotFoundException.ts index d2f9c13f..fc35e4bd 100644 --- a/backend/src/user/exception/UserNotFoundException.ts +++ b/backend/src/user/exception/UserNotFoundException.ts @@ -3,7 +3,7 @@ import { BaseException } from '@src/common/exception/BaseException'; export class UserNotFoundException extends BaseException { constructor(id: number) { super({ - code: 2001, + code: 101, message: `id:${id} 사용자를 찾을 수 없습니다.`, status: 404, }); From 803b0bac7f39955a0e9e4d2e348a4000507fabce Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 11:59:02 +0900 Subject: [PATCH 049/139] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/admin/banner/banner.service.ts | 11 ++++++----- .../src/banner/exception/BannerNotFoundException.ts | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 backend/src/banner/exception/BannerNotFoundException.ts diff --git a/backend/src/admin/banner/banner.service.ts b/backend/src/admin/banner/banner.service.ts index 0e6a4a42..a2ff3706 100644 --- a/backend/src/admin/banner/banner.service.ts +++ b/backend/src/admin/banner/banner.service.ts @@ -1,8 +1,9 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { AdminBannerRepository } from './banner.repository'; import { CreateBannerRequest } from '@src/admin/banner/dto/CreateBannerRequest'; import { UpdateBannerPeriodRequest } from '@src/admin/banner/dto/UpdateBannerPeriodRequest'; import { UpdateBannerDetailsRequest } from '@src/admin/banner/dto/UpdateBannerDetailsRequest'; +import { BannerNotFoundException } from '@src/banner/exception/BannerNotFoundException'; @Injectable() export class AdminBannerService { @@ -26,7 +27,7 @@ export class AdminBannerService { }); if (result.affected === 0) { - throw new NotFoundException(`Banner with id ${id} not found`); + throw new BannerNotFoundException(id); } return this.bannerRepository.findOne({ where: { id } }); @@ -42,7 +43,7 @@ export class AdminBannerService { }); if (result.affected === 0) { - throw new NotFoundException(`Banner with id ${id} not found`); + throw new BannerNotFoundException(id); } return this.bannerRepository.findOne({ where: { id } }); @@ -52,9 +53,9 @@ export class AdminBannerService { const result = await this.bannerRepository.softDelete(id); if (result.affected === 0) { - throw new NotFoundException(`Banner with id ${id} not found`); + throw new BannerNotFoundException(id); } - return { message: `Banner with id ${id} successfully deleted` }; + return { deleted: id }; } } diff --git a/backend/src/banner/exception/BannerNotFoundException.ts b/backend/src/banner/exception/BannerNotFoundException.ts new file mode 100644 index 00000000..518bbe30 --- /dev/null +++ b/backend/src/banner/exception/BannerNotFoundException.ts @@ -0,0 +1,11 @@ +import { BaseException } from '@src/common/exception/BaseException'; + +export class BannerNotFoundException extends BaseException { + constructor(id: number) { + super({ + code: 661, + message: `[${id}] 배너가 존재하지 않습니다.`, + status: 404, + }); + } +} From 9da2c6d05f40db3b61698eed2f6e32290f02924a Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 18:07:53 +0900 Subject: [PATCH 050/139] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=98=95=EC=8B=9D=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `[id] 메시지` 형식 --- backend/src/course/exception/CourseNotFoundException.ts | 2 +- backend/src/course/exception/CoursePermissionException.ts | 2 +- backend/src/map/exception/MapNotFoundException.ts | 2 +- backend/src/map/exception/MapPermissionException.ts | 4 ++-- backend/src/place/exception/PlaceNotFoundException.ts | 4 +--- backend/src/search/exception/ElasticSearchSaveException.ts | 2 +- backend/src/user/exception/UserNotFoundException.ts | 2 +- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/src/course/exception/CourseNotFoundException.ts b/backend/src/course/exception/CourseNotFoundException.ts index bea00438..0d9cc4d7 100644 --- a/backend/src/course/exception/CourseNotFoundException.ts +++ b/backend/src/course/exception/CourseNotFoundException.ts @@ -5,7 +5,7 @@ export class CourseNotFoundException extends BaseException { constructor(id: number) { super({ code: 401, - message: `id:${id} 코스가 존재하지 않거나 삭제되었습니다.`, + message: `[${id}] 코스가 존재하지 않거나 삭제되었습니다.`, status: HttpStatus.NOT_FOUND, }); } diff --git a/backend/src/course/exception/CoursePermissionException.ts b/backend/src/course/exception/CoursePermissionException.ts index 998f257b..173a51db 100644 --- a/backend/src/course/exception/CoursePermissionException.ts +++ b/backend/src/course/exception/CoursePermissionException.ts @@ -5,7 +5,7 @@ export class CoursePermissionException extends BaseException { constructor(id: number) { super({ code: 406, - message: `id:${id} 코스에 대한 권한이 없습니다.`, + message: `[${id}] 코스에 대한 권한이 없습니다.`, status: HttpStatus.FORBIDDEN, }); } diff --git a/backend/src/map/exception/MapNotFoundException.ts b/backend/src/map/exception/MapNotFoundException.ts index 511d59ab..8df4050e 100644 --- a/backend/src/map/exception/MapNotFoundException.ts +++ b/backend/src/map/exception/MapNotFoundException.ts @@ -5,7 +5,7 @@ export class MapNotFoundException extends BaseException { constructor(id: number) { super({ code: 301, - message: `id:${id} 지도가 존재하지 않거나 삭제되었습니다.`, + message: `[${id}] 지도가 존재하지 않거나 삭제되었습니다.`, status: HttpStatus.NOT_FOUND, }); } diff --git a/backend/src/map/exception/MapPermissionException.ts b/backend/src/map/exception/MapPermissionException.ts index 8fc8a414..5d91a072 100644 --- a/backend/src/map/exception/MapPermissionException.ts +++ b/backend/src/map/exception/MapPermissionException.ts @@ -4,8 +4,8 @@ import { HttpStatus } from '@nestjs/common'; export class MapPermissionException extends BaseException { constructor(id: number) { super({ - code: 803, - message: `지도 ${id} 에 대한 권한이 없습니다.`, + code: 306, + message: `[${id}] 지도에 대한 권한이 없습니다.`, status: HttpStatus.FORBIDDEN, }); } diff --git a/backend/src/place/exception/PlaceNotFoundException.ts b/backend/src/place/exception/PlaceNotFoundException.ts index aa29ba79..1fcef37f 100644 --- a/backend/src/place/exception/PlaceNotFoundException.ts +++ b/backend/src/place/exception/PlaceNotFoundException.ts @@ -3,9 +3,7 @@ import { HttpStatus } from '@nestjs/common'; export class PlaceNotFoundException extends BaseException { constructor(id?: number) { - const message = id - ? `id:${id} 장소가 존재하지 않습니다.` - : '장소가 존재하지 않습니다.'; + const message = `${id && `[${id}]`} 장소가 존재하지 않습니다.`; super({ code: 201, message, diff --git a/backend/src/search/exception/ElasticSearchSaveException.ts b/backend/src/search/exception/ElasticSearchSaveException.ts index 13d3e295..eed88cd5 100644 --- a/backend/src/search/exception/ElasticSearchSaveException.ts +++ b/backend/src/search/exception/ElasticSearchSaveException.ts @@ -3,7 +3,7 @@ import { HttpStatus } from '@nestjs/common'; export class ElasticSearchSaveException extends BaseException { constructor(id?: number) { - const message = `${id ? `id:${id}` : ''} ElasticSearch에 데이터를 저장하는데 실패했습니다.`; + const message = `${id && `[${id}]`} ElasticSearch에 데이터를 저장하는데 실패했습니다.`; super({ code: 3001, message, diff --git a/backend/src/user/exception/UserNotFoundException.ts b/backend/src/user/exception/UserNotFoundException.ts index fc35e4bd..4e5c57f1 100644 --- a/backend/src/user/exception/UserNotFoundException.ts +++ b/backend/src/user/exception/UserNotFoundException.ts @@ -4,7 +4,7 @@ export class UserNotFoundException extends BaseException { constructor(id: number) { super({ code: 101, - message: `id:${id} 사용자를 찾을 수 없습니다.`, + message: `[${id}] 사용자가 존재하지 않습니다.`, status: 404, }); } From 1c50507138781dfb21244a03fb6ba8acf6d50018 Mon Sep 17 00:00:00 2001 From: Soap Date: Tue, 26 Nov 2024 18:10:09 +0900 Subject: [PATCH 051/139] =?UTF-8?q?test:=20=EC=98=88=EC=99=B8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/integration-test/map.integration.expectExcptions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/test/map/integration-test/map.integration.expectExcptions.ts b/backend/test/map/integration-test/map.integration.expectExcptions.ts index 7db954c3..84a8b869 100644 --- a/backend/test/map/integration-test/map.integration.expectExcptions.ts +++ b/backend/test/map/integration-test/map.integration.expectExcptions.ts @@ -16,13 +16,13 @@ export const EMPTY_TOKEN_EXCEPTION = { export const MAP_NOT_FOUND_EXCEPTION = (id: number) => { return { statusCode: 404, - message: `id:${id} 지도가 존재하지 않거나 삭제되었습니다.`, + message: `[${id}] 지도가 존재하지 않거나 삭제되었습니다.`, }; }; export const MAP_PERMISSION_EXCEPTION = (id: number) => { return { statusCode: 403, - message: `지도 ${id} 에 대한 권한이 없습니다.`, + message: `[${id}] 지도에 대한 권한이 없습니다.`, }; }; From 730079b38a708ba5ff5aaa317030642c877d52c7 Mon Sep 17 00:00:00 2001 From: Soap Date: Wed, 27 Nov 2024 00:43:28 +0900 Subject: [PATCH 052/139] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=EB=8A=94=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=EB=A7=8C=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BaseException 에 에러 필드를 추가해 상세 로깅 가능하도록 수정 --- backend/src/common/exception/BaseException.ts | 5 ++++- .../exception/filter/GlobalExceptionFilter.ts | 19 +++++++++++++------ .../exception/ElasticSearchSaveException.ts | 17 ++++++++++------- .../exception/CloudFunctionsFetchException.ts | 13 ++++++++----- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/backend/src/common/exception/BaseException.ts b/backend/src/common/exception/BaseException.ts index 02ac6fa5..b5ac742d 100644 --- a/backend/src/common/exception/BaseException.ts +++ b/backend/src/common/exception/BaseException.ts @@ -4,7 +4,10 @@ import { ExceptionType } from './ExceptionType'; export class BaseException extends HttpException { readonly code: number; - constructor(exceptionType: ExceptionType) { + constructor( + readonly exceptionType: ExceptionType, + readonly error?: Error, + ) { super(exceptionType.message, exceptionType.status); this.code = exceptionType.code; } diff --git a/backend/src/common/exception/filter/GlobalExceptionFilter.ts b/backend/src/common/exception/filter/GlobalExceptionFilter.ts index 945d0c35..e9a623a3 100644 --- a/backend/src/common/exception/filter/GlobalExceptionFilter.ts +++ b/backend/src/common/exception/filter/GlobalExceptionFilter.ts @@ -18,7 +18,7 @@ export class BaseExceptionFilter implements ExceptionFilter { const response = ctx.getResponse(); const status = exception.getStatus(); - logException(this.logger, exception.getMessage(), status); + logException(this.logger, exception.getMessage(), status, exception.error); return response.status(status).json({ code: exception.getCode(), @@ -59,14 +59,23 @@ export class HttpExceptionFilter implements ExceptionFilter { } } -function logException(logger: PinoLogger, message: string, status: number) { +function logException( + logger: PinoLogger, + message: string, + status: number, + error?: Error, +) { const WARN = 400; const ERROR = 500; if (status < WARN) return; void (status < ERROR - ? logger.warn(`${message}`) - : logger.error(`${message}`)); + ? logger.warn(message) + : logger.error({ + message, + stack: error?.stack, + name: error?.name, + })); } @Catch() @@ -90,8 +99,6 @@ export class UnknownExceptionFilter implements ExceptionFilter { }); } - console.error(exception); - return response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ code: -1, message: 'Internal server error', diff --git a/backend/src/search/exception/ElasticSearchSaveException.ts b/backend/src/search/exception/ElasticSearchSaveException.ts index eed88cd5..589fecc9 100644 --- a/backend/src/search/exception/ElasticSearchSaveException.ts +++ b/backend/src/search/exception/ElasticSearchSaveException.ts @@ -2,12 +2,15 @@ import { BaseException } from '@src/common/exception/BaseException'; import { HttpStatus } from '@nestjs/common'; export class ElasticSearchSaveException extends BaseException { - constructor(id?: number) { - const message = `${id && `[${id}]`} ElasticSearch에 데이터를 저장하는데 실패했습니다.`; - super({ - code: 3001, - message, - status: HttpStatus.INTERNAL_SERVER_ERROR, - }); + constructor(id?: number, error?: Error) { + const message = `${id && `[${id}]`} ElasticSearch 데이터를 저장하는데 실패했습니다.`; + super( + { + code: 3001, + message, + status: HttpStatus.INTERNAL_SERVER_ERROR, + }, + error, + ); } } diff --git a/backend/src/storage/exception/CloudFunctionsFetchException.ts b/backend/src/storage/exception/CloudFunctionsFetchException.ts index 579ffb18..d1062728 100644 --- a/backend/src/storage/exception/CloudFunctionsFetchException.ts +++ b/backend/src/storage/exception/CloudFunctionsFetchException.ts @@ -3,10 +3,13 @@ import { HttpStatus } from '@nestjs/common'; export class CloudFunctionsFetchException extends BaseException { constructor(error: Error) { - super({ - code: 711, - message: `Cloud function 과 Fetch 중 오류가 발생했습니다. : ${error.message}`, - status: HttpStatus.INTERNAL_SERVER_ERROR, - }); + super( + { + code: 711, + message: `Cloud function 통신 중 오류가 발생했습니다.`, + status: HttpStatus.INTERNAL_SERVER_ERROR, + }, + error, + ); } } From 65ed581a0aa98632a1035c19a08c3761a4229d76 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:26:47 +0900 Subject: [PATCH 053/139] =?UTF-8?q?feat:=20=EC=A0=95=EB=A0=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80/=20map=EA=B4=B4=20user,=20place=20=EB=A5=BC=20?= =?UTF-8?q?=EC=A1=B0=EC=9D=B8=ED=95=98=EC=97=AC=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B3=A0=20place=EA=B0=80=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EC=A7=80=EB=8F=84=EC=9D=98=20=EA=B0=9C=EC=88=98=20?= =?UTF-8?q?=EC=84=B8=EB=8A=94=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.repository.ts | 41 ++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/backend/src/map/map.repository.ts b/backend/src/map/map.repository.ts index 417d21ab..a7414815 100644 --- a/backend/src/map/map.repository.ts +++ b/backend/src/map/map.repository.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { ILike, DataSource } from 'typeorm'; +import { DataSource, ILike } from 'typeorm'; import { Map } from './entity/map.entity'; import { SoftDeleteRepository } from '../common/SoftDeleteRepository'; +import { sortOrder } from '@src/map/map.type'; @Injectable() export class MapRepository extends SoftDeleteRepository { @@ -9,27 +10,61 @@ export class MapRepository extends SoftDeleteRepository { super(Map, dataSource.createEntityManager()); } - findAll(page: number, pageSize: number) { + findAll(page: number, pageSize: number, orderBy: sortOrder) { return this.find({ where: { isPublic: true }, skip: (page - 1) * pageSize, take: pageSize, + order: { + createdAt: orderBy, + }, }); } - searchByTitleQuery(title: string, page: number, pageSize: number) { + searchByTitleQuery( + title: string, + page: number, + pageSize: number, + orderBy: sortOrder, + ) { return this.find({ where: { title: ILike(`%${title}%`), isPublic: true }, skip: (page - 1) * pageSize, take: pageSize, + order: { + createdAt: orderBy, + }, }); } + async findMapWithPlace(page: number, pageSize: number, orderBy: sortOrder) { + return await this.createQueryBuilder('map') + .leftJoinAndSelect('map.mapPlaces', 'mapPlace') + .leftJoinAndSelect('map.user', 'user') + .where('map.isPublic = :isPublic', { isPublic: true }) + .andWhere('mapPlace.id IS NOT NULL') + .orderBy('map.createdAt', orderBy) + .skip((page - 1) * pageSize) + .take(pageSize) + .getMany(); + } + + async countMapsWithPlace() { + return await this.createQueryBuilder('map') + .leftJoinAndSelect('map.mapPlaces', 'mapPlace') + .where('map.isPublic = :isPublic', { isPublic: true }) + .andWhere('mapPlace.id IS NOT NULL') + .getCount(); + } + findByUserId(userId: number, page: number, pageSize: number) { return this.find({ where: { user: { id: userId } }, skip: (page - 1) * pageSize, take: pageSize, + order: { + createdAt: 'DESC', + }, }); } } From de9a9ffc92c1e47ee1670cb65a99b59a3018a158 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:41:51 +0900 Subject: [PATCH 054/139] =?UTF-8?q?feat:=20=EC=A7=80=EB=8F=84=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C,=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=A1=B0=ED=9A=8C=20=20order=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.controller.ts | 18 ++++++++++++++++-- backend/src/map/map.type.ts | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 backend/src/map/map.type.ts diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 4b76ab95..075084a9 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -21,6 +21,7 @@ import { AuthUser } from '@src/auth/AuthUser.decorator'; import { JwtAuthGuard } from '@src/auth/JwtAuthGuard'; import { MapPermissionGuard } from '@src/map/guards/MapPermissionGuard'; import { UpdateMapVisibilityRequest } from '@src/map/dto/UpdateMapVisibilityRequest'; +import { sortOrder } from '@src/map/map.type'; @Controller('/maps') export class MapController { @@ -31,13 +32,26 @@ export class MapController { @Query('query') query?: string, @Query('page', new ParseOptionalNumberPipe(1)) page?: number, @Query('limit', new ParseOptionalNumberPipe(15)) limit?: number, + @Query('order') order?: string, ) { - return await this.mapService.searchMap(query, page, limit); + if (!order) order = 'DESC'; + if (query) { + return await this.mapService.searchMap( + query, + page, + limit, + order as sortOrder, + ); + } + return await this.mapService.getAllMaps(page, limit, order as sortOrder); } @UseGuards(JwtAuthGuard) @Get('/my') - async getMyMapList(@AuthUser() user: AuthUser) { + async getMyMapList( + @AuthUser() user: AuthUser, + @Query('order') order?: string, + ) { return await this.mapService.getOwnMaps(user.userId); } diff --git a/backend/src/map/map.type.ts b/backend/src/map/map.type.ts new file mode 100644 index 00000000..f061cc51 --- /dev/null +++ b/backend/src/map/map.type.ts @@ -0,0 +1 @@ +export type sortOrder = 'ASC' | 'DESC'; From 3e0096eb1bf1183a5d99f852509fa43cee32ad34 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:43:15 +0900 Subject: [PATCH 055/139] =?UTF-8?q?feat:=20mapService=20searchMap=20?= =?UTF-8?q?=EA=B3=BC=20getAllMaps=20=EB=B6=84=EB=A6=AC/=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95/?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EC=A7=80=EB=8F=84=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?,=20=EC=A7=80=EB=8F=84=20=EA=B2=80=EC=83=89=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.service.ts | 38 ++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index af20791e..1e9f4388 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -13,6 +13,7 @@ import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMap import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { Transactional } from 'typeorm-transactional'; +import { sortOrder } from '@src/map/map.type'; @Injectable() export class MapService { @@ -24,17 +25,23 @@ export class MapService { // Todo. 작성자명 등 ... 검색 조건 추가 // Todo. fix : public 으로 조회해서 페이지마다 수 일정하게. (현재는 한 페이지에 10개 미만인 경우 존재) - async searchMap(query?: string, page: number = 1, pageSize: number = 10) { - const maps = query - ? await this.mapRepository.searchByTitleQuery(query, page, pageSize) - : await this.mapRepository.findAll(page, pageSize); - + async searchMap( + query?: string, + page: number = 1, + pageSize: number = 15, + orderBy: sortOrder = 'DESC', + ) { + const maps = await this.mapRepository.searchByTitleQuery( + query, + page, + pageSize, + orderBy, + ); const totalCount = await this.mapRepository.count({ where: { title: query, isPublic: true }, }); const publicMaps = maps.filter((map) => map.isPublic); - return { maps: await Promise.all(publicMaps.map(MapListResponse.from)), totalPages: Math.ceil(totalCount / pageSize), @@ -42,6 +49,25 @@ export class MapService { }; } + async getAllMaps( + page: number = 1, + pageSize: number = 15, + orderBy: sortOrder = 'DESC', + ) { + const totalCount = await this.mapRepository.countMapsWithPlace(); + const maps = await this.mapRepository.findMapWithPlace( + page, + pageSize, + orderBy, + ); + + return { + maps: await Promise.all(maps.map(MapListResponse.from)), + totalPages: Math.ceil(totalCount / pageSize), + currentPage: page, + }; + } + async getOwnMaps(userId: number, page: number = 1, pageSize: number = 10) { // Todo. 그룹 기능 추가 const totalCount = await this.mapRepository.count({ From 185d279b79156c1f4e4574e6f967ae686f1ee123 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:13:06 +0900 Subject: [PATCH 056/139] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=82=AD=EC=A0=9C=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.controller.ts | 17 +++-------------- backend/src/map/map.repository.ts | 18 ++++++------------ backend/src/map/map.service.ts | 22 ++++------------------ backend/src/map/map.type.ts | 1 - 4 files changed, 13 insertions(+), 45 deletions(-) delete mode 100644 backend/src/map/map.type.ts diff --git a/backend/src/map/map.controller.ts b/backend/src/map/map.controller.ts index 075084a9..6253b6eb 100644 --- a/backend/src/map/map.controller.ts +++ b/backend/src/map/map.controller.ts @@ -21,7 +21,6 @@ import { AuthUser } from '@src/auth/AuthUser.decorator'; import { JwtAuthGuard } from '@src/auth/JwtAuthGuard'; import { MapPermissionGuard } from '@src/map/guards/MapPermissionGuard'; import { UpdateMapVisibilityRequest } from '@src/map/dto/UpdateMapVisibilityRequest'; -import { sortOrder } from '@src/map/map.type'; @Controller('/maps') export class MapController { @@ -32,26 +31,16 @@ export class MapController { @Query('query') query?: string, @Query('page', new ParseOptionalNumberPipe(1)) page?: number, @Query('limit', new ParseOptionalNumberPipe(15)) limit?: number, - @Query('order') order?: string, ) { - if (!order) order = 'DESC'; if (query) { - return await this.mapService.searchMap( - query, - page, - limit, - order as sortOrder, - ); + return await this.mapService.searchMap(query, page, limit); } - return await this.mapService.getAllMaps(page, limit, order as sortOrder); + return await this.mapService.getAllMaps(page, limit); } @UseGuards(JwtAuthGuard) @Get('/my') - async getMyMapList( - @AuthUser() user: AuthUser, - @Query('order') order?: string, - ) { + async getMyMapList(@AuthUser() user: AuthUser) { return await this.mapService.getOwnMaps(user.userId); } diff --git a/backend/src/map/map.repository.ts b/backend/src/map/map.repository.ts index a7414815..b7e9dc35 100644 --- a/backend/src/map/map.repository.ts +++ b/backend/src/map/map.repository.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { DataSource, ILike } from 'typeorm'; import { Map } from './entity/map.entity'; import { SoftDeleteRepository } from '../common/SoftDeleteRepository'; -import { sortOrder } from '@src/map/map.type'; @Injectable() export class MapRepository extends SoftDeleteRepository { @@ -10,40 +9,35 @@ export class MapRepository extends SoftDeleteRepository { super(Map, dataSource.createEntityManager()); } - findAll(page: number, pageSize: number, orderBy: sortOrder) { + findAll(page: number, pageSize: number) { return this.find({ where: { isPublic: true }, skip: (page - 1) * pageSize, take: pageSize, order: { - createdAt: orderBy, + createdAt: 'DESC', }, }); } - searchByTitleQuery( - title: string, - page: number, - pageSize: number, - orderBy: sortOrder, - ) { + searchByTitleQuery(title: string, page: number, pageSize: number) { return this.find({ where: { title: ILike(`%${title}%`), isPublic: true }, skip: (page - 1) * pageSize, take: pageSize, order: { - createdAt: orderBy, + createdAt: 'DESC', }, }); } - async findMapWithPlace(page: number, pageSize: number, orderBy: sortOrder) { + async findMapsWithPlace(page: number, pageSize: number) { return await this.createQueryBuilder('map') .leftJoinAndSelect('map.mapPlaces', 'mapPlace') .leftJoinAndSelect('map.user', 'user') .where('map.isPublic = :isPublic', { isPublic: true }) .andWhere('mapPlace.id IS NOT NULL') - .orderBy('map.createdAt', orderBy) + .orderBy('map.createdAt', 'DESC') .skip((page - 1) * pageSize) .take(pageSize) .getMany(); diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 1e9f4388..9fca3f3e 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -13,7 +13,6 @@ import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMap import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { Transactional } from 'typeorm-transactional'; -import { sortOrder } from '@src/map/map.type'; @Injectable() export class MapService { @@ -25,17 +24,11 @@ export class MapService { // Todo. 작성자명 등 ... 검색 조건 추가 // Todo. fix : public 으로 조회해서 페이지마다 수 일정하게. (현재는 한 페이지에 10개 미만인 경우 존재) - async searchMap( - query?: string, - page: number = 1, - pageSize: number = 15, - orderBy: sortOrder = 'DESC', - ) { + async searchMap(query?: string, page: number = 1, pageSize: number = 15) { const maps = await this.mapRepository.searchByTitleQuery( query, page, pageSize, - orderBy, ); const totalCount = await this.mapRepository.count({ where: { title: query, isPublic: true }, @@ -49,17 +42,9 @@ export class MapService { }; } - async getAllMaps( - page: number = 1, - pageSize: number = 15, - orderBy: sortOrder = 'DESC', - ) { + async getAllMaps(page: number = 1, pageSize: number = 15) { const totalCount = await this.mapRepository.countMapsWithPlace(); - const maps = await this.mapRepository.findMapWithPlace( - page, - pageSize, - orderBy, - ); + const maps = await this.mapRepository.findMapsWithPlace(page, pageSize); return { maps: await Promise.all(maps.map(MapListResponse.from)), @@ -72,6 +57,7 @@ export class MapService { // Todo. 그룹 기능 추가 const totalCount = await this.mapRepository.count({ where: { user: { id: userId } }, + order: { createdAt: 'DESC' }, }); const ownMaps = await this.mapRepository.findByUserId( diff --git a/backend/src/map/map.type.ts b/backend/src/map/map.type.ts deleted file mode 100644 index f061cc51..00000000 --- a/backend/src/map/map.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type sortOrder = 'ASC' | 'DESC'; From fe301c14449dc810d77559ce9c45dce735241f9d Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:58:47 +0900 Subject: [PATCH 057/139] =?UTF-8?q?test:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20(=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=ED=95=80?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20=EC=A7=80=EB=8F=84=20=EB=B0=98=ED=99=98?= =?UTF-8?q?)=20=EC=97=90=20=EB=A7=9E=EA=B2=8C=20map=20service=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.service.test.ts | 58 ++++++++++++++++------------ 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/backend/test/map/map.service.test.ts b/backend/test/map/map.service.test.ts index d76b0c28..aa31fecb 100644 --- a/backend/test/map/map.service.test.ts +++ b/backend/test/map/map.service.test.ts @@ -30,6 +30,7 @@ import { ConfigModule } from '@nestjs/config'; import { JWTHelper } from '@src/auth/JWTHelper'; import { UpdateMapInfoRequest } from '@src/map/dto/UpdateMapInfoRequest'; import { initMapUserPlaceTable } from '@test/map/integration-test/map.integration.util'; +import { MapPlace } from '@src/map/entity/map-place.entity'; describe('MapService 테스트', () => { let app: INestApplication; @@ -116,31 +117,6 @@ describe('MapService 테스트', () => { }); describe('searchMap 메소드 테스트', () => { - it('파라미터 중 query 가 없을 경우 공개된 모든 지도를 반환한다.', async () => { - const publicMaps: Map[] = createPublicMaps(5, fakeUser1); - const privateMaps = createPrivateMaps(5, fakeUser1); - const publicMapEntities = await mapRepository.save([...publicMaps]); - await mapRepository.save([...privateMaps]); - publicMapEntities.forEach((publicMapEntity) => { - publicMapEntity.mapPlaces = []; - }); - const expected = await Promise.all( - publicMapEntities.map(MapListResponse.from), - ); - - const result = await mapService.searchMap(undefined, 1, 10); - - expect(result.maps).toEqual( - expect.arrayContaining( - expected.map((response) => expect.objectContaining(response)), - ), - ); - expect(result.currentPage).toEqual(page); - expect(result.totalPages).toEqual( - Math.ceil(publicMapEntities.length / pageSize), - ); - }); - it('파라미터 중 쿼리가 있을 경우 해당 제목을 가진 지도들을 반환한다', async () => { const searchTitle = 'cool'; const coolMaps: Map[] = createPublicMapsWithTitle( @@ -165,7 +141,39 @@ describe('MapService 테스트', () => { ); }); }); + describe('getAllMaps 메소드 테스트', () => { + it('장소를 가지고 있고 공개된 모든 지도를 반환한다.', async () => { + const publicMaps: Map[] = createPublicMaps(3, fakeUser1); + const publicMapsWithPlaces = createPublicMaps(2, fakeUser1); + const privateMaps = createPrivateMaps(5, fakeUser1); + publicMapsWithPlaces.forEach((publicMapWithPlaces) => { + publicMapWithPlaces.mapPlaces = [ + MapPlace.of(1, publicMapWithPlaces, 'RED' as Color, 'test'), + ]; + }); + await mapRepository.save([ + ...publicMaps, + ...publicMapsWithPlaces, + ...privateMaps, + ]); + await mapRepository.save([...privateMaps]); + const expected = await Promise.all( + publicMapsWithPlaces.map(MapListResponse.from), + ); + + const result = await mapService.getAllMaps(1, 10); + expect(result.maps).toEqual( + expect.arrayContaining( + expected.map((response) => expect.objectContaining(response)), + ), + ); + expect(result.currentPage).toEqual(page); + expect(result.totalPages).toEqual( + Math.ceil(publicMapsWithPlaces.length / pageSize), + ); + }); + }); describe('getOwnMaps 메소드 테스트', () => { it('유저 아이디를 파라미터로 받아서 해당 유저의 지도를 반환한다.', async () => { const fakeUserMaps = createPublicMaps(5, fakeUser1); From a870233dbd938c1af1b78ce4b4827a72a443ea84 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:59:04 +0900 Subject: [PATCH 058/139] =?UTF-8?q?test:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20(=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=ED=95=80?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20=EC=A7=80=EB=8F=84=20=EB=B0=98=ED=99=98?= =?UTF-8?q?)=20=EC=97=90=20=EB=A7=9E=EA=B2=8C=20map=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration-test/map.integration.test.ts | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index 7ac37ea2..a15e5b6a 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -18,6 +18,7 @@ import { createPlace, createPrivateMaps, createPublicMaps, + createPublicMapsWithTitle, } from '@test/map/map.test.util'; import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; @@ -33,6 +34,7 @@ import { initMapUserPlaceTable, } from '@test/map/integration-test/map.integration.util'; import { initializeTransactionalContext } from 'typeorm-transactional'; +import { MapPlace } from '@src/map/entity/map-place.entity'; describe('MapController 통합 테스트', () => { let app: INestApplication; @@ -232,18 +234,74 @@ describe('MapController 통합 테스트', () => { }); describe('getMapList 메소드 테스트', () => { - it('GET maps/ 에 대한 요청으로 공개 되어있는 지도 모두 반환한다.', async () => { - const publicMaps = createPublicMaps(5, fakeUser1); + it('GET maps/ 에 대한 요청으로 공개 되어있고, 장소를 가지고 있는 지도 모두 반환한다.', async () => { + const publicMaps = createPublicMaps(2, fakeUser1); + const publicMapsWithPlace = createPublicMaps(3, fakeUser1); const privateMaps = createPrivateMaps(5, fakeUser1); - await mapRepository.save([...publicMaps, ...privateMaps]); + publicMapsWithPlace.forEach((publicMapWithPlace) => { + publicMapWithPlace.mapPlaces = []; + publicMapWithPlace.mapPlaces.push( + MapPlace.of(1, publicMapWithPlace, 'RED' as Color, 'test'), + ); + }); + await mapRepository.save([ + ...publicMaps, + ...publicMapsWithPlace, + ...privateMaps, + ]); return request(app.getHttpServer()) .get('/maps') + .expect((response) => { + const gotMaps = response.body.maps; + expect(gotMaps.length).toEqual(publicMapsWithPlace.length); + gotMaps.forEach((gotMap: Map, index: number) => { + expect(gotMap.title).toEqual(publicMapsWithPlace[index].title); + expect(gotMap.description).toEqual( + publicMapsWithPlace[index].description, + ); + expect(gotMap.isPublic).toEqual( + publicMapsWithPlace[index].isPublic, + ); + expect(gotMap.thumbnailUrl).toEqual( + publicMapsWithPlace[index].thumbnailUrl, + ); + }); + }); + }); + + it('GET /maps?query 에 대한 요청으로 공개 되어있고 query 와 유사한 title을 가지는 지도들을 반환한다.', async () => { + const coolMaps = createPublicMapsWithTitle( + 2, + fakeUser1, + 'cool test title', + ); + const coolMapsWithPlace = createPublicMapsWithTitle( + 3, + fakeUser1, + 'cool title', + ); + const privateMaps = createPrivateMaps(5, fakeUser1); + coolMapsWithPlace.forEach((publicMapWithPlace) => { + publicMapWithPlace.mapPlaces = []; + publicMapWithPlace.mapPlaces.push( + MapPlace.of(1, publicMapWithPlace, 'RED' as Color, 'test'), + ); + }); + coolMaps.forEach((publicMap) => { + publicMap.mapPlaces = []; + }); + const publicMaps = [...coolMaps, ...coolMapsWithPlace]; + await mapRepository.save([...publicMaps, ...privateMaps]); + + return request(app.getHttpServer()) + .get('/maps?query=cool') + .expect((response) => { const gotMaps = response.body.maps; expect(gotMaps.length).toEqual(publicMaps.length); - gotMaps.forEach((gotMap: Map, index) => { + gotMaps.forEach((gotMap: Map, index: number) => { expect(gotMap.title).toEqual(publicMaps[index].title); expect(gotMap.description).toEqual(publicMaps[index].description); expect(gotMap.isPublic).toEqual(publicMaps[index].isPublic); From eac0707b3395cee6e51b9c07f167b8670ee3fcfb Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:18:31 +0900 Subject: [PATCH 059/139] =?UTF-8?q?test:=20mapRespository=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=90=9C=20=EB=A9=94=EC=86=8C=EB=93=9C(countMapsWithP?= =?UTF-8?q?lace,=20findMapsWithPlace)=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.repository.test.ts | 208 +++++++++++++++++------- 1 file changed, 146 insertions(+), 62 deletions(-) diff --git a/backend/test/map/map.repository.test.ts b/backend/test/map/map.repository.test.ts index e1fa243c..fe422016 100644 --- a/backend/test/map/map.repository.test.ts +++ b/backend/test/map/map.repository.test.ts @@ -6,28 +6,44 @@ import { initDataSource } from '@test/config/datasource.config'; import { UserFixture } from '@test/user/fixture/user.fixture'; import { MapFixture } from '@test/map/fixture/map.fixture'; import { initializeTransactionalContext } from 'typeorm-transactional'; +import { + createPlace, + createPrivateMaps, + createPublicMaps, +} from '@test/map/map.test.util'; +import { PlaceRepository } from '@src/place/place.repository'; +import { MapPlace } from '@src/map/entity/map-place.entity'; +import { Color } from '@src/place/color.enum'; describe('MapRepository', () => { let container: StartedMySqlContainer; let mapRepository: MapRepository; + let placeRepository: PlaceRepository; let dataSource: DataSource; let fakeUser1: User; let fakeUser2: User; beforeAll(async () => { + container = await new MySqlContainer().withReuse().start(); + dataSource = await initDataSource(container); initializeTransactionalContext(); + fakeUser1 = UserFixture.createUser({ oauthId: 'abc' }); fakeUser2 = UserFixture.createUser({ oauthId: 'def' }); - container = await new MySqlContainer().withReuse().start(); - dataSource = await initDataSource(container); + mapRepository = new MapRepository(dataSource); + placeRepository = new PlaceRepository(dataSource); + await dataSource.getRepository(User).delete({}); await dataSource.getRepository(User).save([fakeUser1, fakeUser2]); + const places = createPlace(5); + await placeRepository.save(places); }); beforeEach(async () => { await mapRepository.delete({}); }); afterAll(async () => { await mapRepository.delete({}); + await placeRepository.delete({}); await dataSource.getRepository(User).delete({}); await dataSource.destroy(); }); @@ -37,7 +53,9 @@ describe('MapRepository', () => { MapFixture.createMap({ user: fakeUser1, title, isPublic }), ); await mapRepository.save(maps); + const findMaps = await mapRepository.findAll(1, 10); + expect(findMaps).toHaveLength(publicMaps.length); expect(findMaps).toEqual( expect.arrayContaining( @@ -49,8 +67,10 @@ describe('MapRepository', () => { const fiftyPublicMaps = createPublicMaps(50, fakeUser1); await mapRepository.save(fiftyPublicMaps); const [page, pageSize] = [5, 5]; - const fifthPageMaps = await mapRepository.findAll(page, pageSize); const expectedMaps = fiftyPublicMaps.slice(20, 25); + + const fifthPageMaps = await mapRepository.findAll(page, pageSize); + expect(fifthPageMaps).toEqual( expect.arrayContaining( expectedMaps.map((expectedMap) => expect.objectContaining(expectedMap)), @@ -75,11 +95,13 @@ describe('MapRepository', () => { ); await mapRepository.save(maps); const [title, page, pageSize] = ['test', 1, 10]; + const MapsWithTestTitle = await mapRepository.searchByTitleQuery( title, page, pageSize, ); + expect(MapsWithTestTitle).toEqual( expect.arrayContaining( searchedMaps.map((searchedMap) => @@ -95,11 +117,13 @@ describe('MapRepository', () => { ); await mapRepository.save(maps); const [title, page, pageSize] = ['test', 1, 5]; + const mapsWithTest = await mapRepository.searchByTitleQuery( title, page, pageSize, ); + expect(mapsWithTest).toEqual( expect.arrayContaining( publicMaps.map((publicMap) => expect.objectContaining(publicMap)), @@ -111,11 +135,13 @@ describe('MapRepository', () => { await mapRepository.save(fiftyMaps); const [title, page, pageSize] = ['test', 5, 5]; const expected = fiftyMaps.slice(20, 25); + const received = await mapRepository.searchByTitleQuery( title, page, pageSize, ); + expect(received).toEqual( expect.arrayContaining( expected.map((map) => expect.objectContaining(map)), @@ -123,47 +149,129 @@ describe('MapRepository', () => { ); }); }); - describe('findByUserId 메소드 테스트', () => { - it('유저의 아이디로 유저가 만든 공개/비공개 모든 지도를 검색할 수 있다', async () => { - const publicFirstFakeUsers = createPublicMaps(5, fakeUser1); - const privateFirstUsers = createPrivateMaps(3, fakeUser1); - const secondFakeUsers = createPublicMaps(3, fakeUser2); - await mapRepository.save([ - ...publicFirstFakeUsers, - ...secondFakeUsers, - ...privateFirstUsers, + describe('findMapsWithPlace 메소드 테스트', () => { + it('지도와 장소를 조인하여 공개되고 장소를 가지고 있는 지도들을 반환한다.', async () => { + const publicMaps = createPublicMaps(5, fakeUser1); + const publicMapsWithPlaces = createPublicMaps(5, fakeUser1); + const privateMapsWithPlaces = createPrivateMaps(5, fakeUser1); + + publicMapsWithPlaces.forEach((publicMapWithPlaces) => { + publicMapWithPlaces.mapPlaces = [ + MapPlace.of(1, publicMapWithPlaces, 'RED' as Color, 'test'), + ]; + }); + privateMapsWithPlaces.forEach((privateMapsWithPlaces) => { + privateMapsWithPlaces.mapPlaces = [ + MapPlace.of(1, privateMapsWithPlaces, 'RED' as Color, 'test'), + ]; + }); + + const mapEntities = await mapRepository.save([ + ...publicMaps, + ...publicMapsWithPlaces, + ...privateMapsWithPlaces, ]); - const [userId, page, pageSize] = [fakeUser1.id, 1, 10]; - const findFirstFakeUsers = await mapRepository.findByUserId( - userId, - page, - pageSize, - ); - expect(findFirstFakeUsers).toEqual( + const expectedMaps = mapEntities.slice(5, 10).map((map) => { + return { + ...map, + mapPlaces: map.mapPlaces.map((place) => { + return { + color: place.color, + createdAt: place.createdAt, + deletedAt: place.deletedAt, + updatedAt: place.updatedAt, + description: place.description, + id: place.id, + placeId: place.placeId, + }; + }), + }; + }); + + const result = await mapRepository.findMapsWithPlace(1, 15); + + expect(result).toEqual( expect.arrayContaining( - [...publicFirstFakeUsers, ...privateFirstUsers].map( - (firstFakeUserMap) => expect.objectContaining(firstFakeUserMap), - ), + expectedMaps.map((map) => expect.objectContaining(map)), ), ); }); - it('유저의 아이디로 검색했을 때 정확한 페이지의 내용이 전달되어야 한다', async () => { - const publicFirstUsers = createPublicMaps(50, fakeUser1); - const [userId, page, pageSize] = [fakeUser1.id, 5, 5]; - await mapRepository.save(publicFirstUsers); - const firstUsersFifthPage = await mapRepository.findByUserId( - userId, - page, - pageSize, - ); - const expectedMaps = publicFirstUsers.slice(20, 25); - expect(firstUsersFifthPage).toEqual( - expect.arrayContaining( - expectedMaps.map((expectedMap) => - expect.objectContaining(expectedMap), + describe('countMapsWithPlace 메소드 테스트', () => { + it('지도와 장소를 조인하여 공개되고 장소를 가지고 있는 지도의 개수를 반환한다.', async () => { + const publicMaps = createPublicMaps(5, fakeUser1); + const publicMapsWithPlaces = createPublicMaps(5, fakeUser1); + const privateMapsWithPlaces = createPrivateMaps(5, fakeUser1); + + publicMapsWithPlaces.forEach((publicMapWithPlaces) => { + publicMapWithPlaces.mapPlaces = [ + MapPlace.of(1, publicMapWithPlaces, 'RED' as Color, 'test'), + ]; + }); + privateMapsWithPlaces.forEach((privateMapsWithPlaces) => { + privateMapsWithPlaces.mapPlaces = [ + MapPlace.of(1, privateMapsWithPlaces, 'RED' as Color, 'test'), + ]; + }); + await mapRepository.save([ + ...publicMaps, + ...publicMapsWithPlaces, + ...privateMapsWithPlaces, + ]); + + const result = await mapRepository.countMapsWithPlace(); + + expect(result).toBe(publicMapsWithPlaces.length); + }); + }); + describe('findByUserId 메소드 테스트', () => { + it('유저의 아이디로 유저가 만든 공개/비공개 모든 지도를 검색할 수 있다', async () => { + const publicFirstFakeUsers = createPublicMaps(5, fakeUser1); + const privateFirstUsers = createPrivateMaps(3, fakeUser1); + const secondFakeUsers = createPublicMaps(3, fakeUser2); + await mapRepository.save([ + ...publicFirstFakeUsers, + ...secondFakeUsers, + ...privateFirstUsers, + ]); + const [userId, page, pageSize] = [fakeUser1.id, 1, 10]; + + const findFirstFakeUsers = await mapRepository.findByUserId( + userId, + page, + pageSize, + ); + + expect(findFirstFakeUsers).toEqual( + expect.arrayContaining( + [...publicFirstFakeUsers, ...privateFirstUsers].map( + (firstFakeUserMap) => expect.objectContaining(firstFakeUserMap), + ), ), - ), - ); + ); + }); + it('유저의 아이디로 검색했을 때 정확한 페이지의 내용이 전달되어야 한다', async () => { + const publicFirstUsers = createPublicMaps(50, fakeUser1); + const [userId, page, pageSize] = [fakeUser1.id, 5, 5]; + await mapRepository.save(publicFirstUsers); + const expectedMaps = publicFirstUsers.slice(20, 25); + expectedMaps.forEach((expectedMap) => { + expectedMap.mapPlaces = []; + }); + + const firstUsersFifthPage = await mapRepository.findByUserId( + userId, + page, + pageSize, + ); + + expect(firstUsersFifthPage).toEqual( + expect.arrayContaining( + expectedMaps.map((expectedMap) => + expect.objectContaining(expectedMap), + ), + ), + ); + }); }); }); }); @@ -182,27 +290,3 @@ function createPublicPrivateMaps() { ], }; } - -function createPublicMaps(count: number, user: User) { - const maps = []; - for (let i = 1; i <= count + 1; i++) { - const map = MapFixture.createMap({ - user: user, - title: `public test map ${i}`, - }); - maps.push(map); - } - return maps; -} - -function createPrivateMaps(count: number, user: User) { - const maps = []; - for (let i = 1; i <= count + 1; i++) { - const map = MapFixture.createMap({ - user: user, - title: `private test map ${i}`, - }); - maps.push(map); - } - return maps; -} From b1997d3d46915668dd2da19d5c3f5855b08eecfc Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:22:41 +0900 Subject: [PATCH 060/139] =?UTF-8?q?fix:=20color=20enum=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.repository.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/test/map/map.repository.test.ts b/backend/test/map/map.repository.test.ts index fe422016..a9980633 100644 --- a/backend/test/map/map.repository.test.ts +++ b/backend/test/map/map.repository.test.ts @@ -13,7 +13,7 @@ import { } from '@test/map/map.test.util'; import { PlaceRepository } from '@src/place/place.repository'; import { MapPlace } from '@src/map/entity/map-place.entity'; -import { Color } from '@src/place/color.enum'; +import { Color } from '@src/place/place.color.enum'; describe('MapRepository', () => { let container: StartedMySqlContainer; From 3f14182e6ec14ddd4cccded891884471753f9d35 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:32:51 +0900 Subject: [PATCH 061/139] =?UTF-8?q?fix:=20count=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC/=20PageMapResponse=20=EB=A1=9C=20=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/dto/PagedMapResponse.ts | 13 +++++++++++++ backend/src/map/map.repository.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 backend/src/map/dto/PagedMapResponse.ts diff --git a/backend/src/map/dto/PagedMapResponse.ts b/backend/src/map/dto/PagedMapResponse.ts new file mode 100644 index 00000000..0b337da1 --- /dev/null +++ b/backend/src/map/dto/PagedMapResponse.ts @@ -0,0 +1,13 @@ +import { PaginationResponse } from '@src/common/dto/PaginationResponse'; +import { MapListResponse } from '@src/map/dto/MapListResponse'; + +export class PagedMapResponse extends PaginationResponse { + constructor( + readonly maps: MapListResponse[], + totalCount: number, + currentPage: number, + pageSize: number, + ) { + super(totalCount, pageSize, currentPage); + } +} diff --git a/backend/src/map/map.repository.ts b/backend/src/map/map.repository.ts index b7e9dc35..52420616 100644 --- a/backend/src/map/map.repository.ts +++ b/backend/src/map/map.repository.ts @@ -31,6 +31,18 @@ export class MapRepository extends SoftDeleteRepository { }); } + countByTitle(title: string) { + return this.count({ + where: { title: ILike(`%${title}%`), isPublic: true }, + }); + } + + countByUserId(userId: number) { + return this.count({ + where: { id: userId }, + }); + } + async findMapsWithPlace(page: number, pageSize: number) { return await this.createQueryBuilder('map') .leftJoinAndSelect('map.mapPlaces', 'mapPlace') From 69116cceb85e4eda12a646f918921447db0ffbc3 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:33:57 +0900 Subject: [PATCH 062/139] =?UTF-8?q?fix:=20mapRepository=20=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=ED=95=9C=20countBy=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20service=20=EC=9D=98=20count=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20/=20PagedMapResponse=20=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20mapService=20=EB=B3=80=EA=B2=BD=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/map/map.service.ts | 48 ++++++++++++++++------------------ 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/backend/src/map/map.service.ts b/backend/src/map/map.service.ts index 9fca3f3e..89eb486d 100644 --- a/backend/src/map/map.service.ts +++ b/backend/src/map/map.service.ts @@ -13,6 +13,7 @@ import { InvalidPlaceToMapException } from '@src/map/exception/InvalidPlaceToMap import { Map } from '@src/map/entity/map.entity'; import { Color } from '@src/place/place.color.enum'; import { Transactional } from 'typeorm-transactional'; +import { PagedMapResponse } from '@src/map/dto/PagedMapResponse'; @Injectable() export class MapService { @@ -22,43 +23,37 @@ export class MapService { private readonly placeRepository: PlaceRepository, ) {} - // Todo. 작성자명 등 ... 검색 조건 추가 - // Todo. fix : public 으로 조회해서 페이지마다 수 일정하게. (현재는 한 페이지에 10개 미만인 경우 존재) async searchMap(query?: string, page: number = 1, pageSize: number = 15) { const maps = await this.mapRepository.searchByTitleQuery( query, page, pageSize, ); - const totalCount = await this.mapRepository.count({ - where: { title: query, isPublic: true }, - }); - const publicMaps = maps.filter((map) => map.isPublic); - return { - maps: await Promise.all(publicMaps.map(MapListResponse.from)), - totalPages: Math.ceil(totalCount / pageSize), - currentPage: page, - }; + const totalCount = await this.mapRepository.countByTitle(query); + + return new PagedMapResponse( + await Promise.all(maps.map(MapListResponse.from)), + totalCount, + page, + pageSize, + ); } async getAllMaps(page: number = 1, pageSize: number = 15) { const totalCount = await this.mapRepository.countMapsWithPlace(); const maps = await this.mapRepository.findMapsWithPlace(page, pageSize); - return { - maps: await Promise.all(maps.map(MapListResponse.from)), - totalPages: Math.ceil(totalCount / pageSize), - currentPage: page, - }; + return new PagedMapResponse( + await Promise.all(maps.map(MapListResponse.from)), + totalCount, + page, + pageSize, + ); } async getOwnMaps(userId: number, page: number = 1, pageSize: number = 10) { - // Todo. 그룹 기능 추가 - const totalCount = await this.mapRepository.count({ - where: { user: { id: userId } }, - order: { createdAt: 'DESC' }, - }); + const totalCount = await this.mapRepository.countByUserId(userId); const ownMaps = await this.mapRepository.findByUserId( userId, @@ -66,11 +61,12 @@ export class MapService { pageSize, ); - return { - maps: await Promise.all(ownMaps.map(MapListResponse.from)), - totalPages: Math.ceil(totalCount / pageSize), - currentPage: page, - }; + return new PagedMapResponse( + await Promise.all(ownMaps.map(MapListResponse.from)), + totalCount, + page, + pageSize, + ); } async getMapById(id: number) { From 9538eef643efa2a02550c462b9dfed3341498fa7 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:34:19 +0900 Subject: [PATCH 063/139] =?UTF-8?q?refactor:=20color=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.repository.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/test/map/map.repository.test.ts b/backend/test/map/map.repository.test.ts index a9980633..e82ddf9e 100644 --- a/backend/test/map/map.repository.test.ts +++ b/backend/test/map/map.repository.test.ts @@ -157,12 +157,12 @@ describe('MapRepository', () => { publicMapsWithPlaces.forEach((publicMapWithPlaces) => { publicMapWithPlaces.mapPlaces = [ - MapPlace.of(1, publicMapWithPlaces, 'RED' as Color, 'test'), + MapPlace.of(1, publicMapWithPlaces, Color.RED, 'test'), ]; }); privateMapsWithPlaces.forEach((privateMapsWithPlaces) => { privateMapsWithPlaces.mapPlaces = [ - MapPlace.of(1, privateMapsWithPlaces, 'RED' as Color, 'test'), + MapPlace.of(1, privateMapsWithPlaces, Color.RED, 'test'), ]; }); @@ -204,12 +204,12 @@ describe('MapRepository', () => { publicMapsWithPlaces.forEach((publicMapWithPlaces) => { publicMapWithPlaces.mapPlaces = [ - MapPlace.of(1, publicMapWithPlaces, 'RED' as Color, 'test'), + MapPlace.of(1, publicMapWithPlaces, Color.RED, 'test'), ]; }); privateMapsWithPlaces.forEach((privateMapsWithPlaces) => { privateMapsWithPlaces.mapPlaces = [ - MapPlace.of(1, privateMapsWithPlaces, 'RED' as Color, 'test'), + MapPlace.of(1, privateMapsWithPlaces, Color.RED, 'test'), ]; }); await mapRepository.save([ From f017d7770944cd1abc4de8ca5fc4791ec584ca2c Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:34:41 +0900 Subject: [PATCH 064/139] =?UTF-8?q?refactort:=20=EC=A2=80=20=EB=8D=94=20?= =?UTF-8?q?=EC=A7=81=EA=B4=80=EC=A0=81=EC=9D=B4=EA=B3=A0=20=EC=9D=BC?= =?UTF-8?q?=EA=B4=80=EC=84=B1=20=EC=9E=88=EB=8A=94=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/map/map.service.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/test/map/map.service.test.ts b/backend/test/map/map.service.test.ts index aa31fecb..a02bbcd7 100644 --- a/backend/test/map/map.service.test.ts +++ b/backend/test/map/map.service.test.ts @@ -119,12 +119,12 @@ describe('MapService 테스트', () => { describe('searchMap 메소드 테스트', () => { it('파라미터 중 쿼리가 있을 경우 해당 제목을 가진 지도들을 반환한다', async () => { const searchTitle = 'cool'; - const coolMaps: Map[] = createPublicMapsWithTitle( + const publicMapsWithTitle: Map[] = createPublicMapsWithTitle( 5, fakeUser1, 'cool map', ); - const savedMaps = await mapRepository.save([...coolMaps]); + const savedMaps = await mapRepository.save([...publicMapsWithTitle]); savedMaps.forEach((mapEntity) => { mapEntity.mapPlaces = []; }); @@ -148,7 +148,7 @@ describe('MapService 테스트', () => { const privateMaps = createPrivateMaps(5, fakeUser1); publicMapsWithPlaces.forEach((publicMapWithPlaces) => { publicMapWithPlaces.mapPlaces = [ - MapPlace.of(1, publicMapWithPlaces, 'RED' as Color, 'test'), + MapPlace.of(1, publicMapWithPlaces, Color.RED, 'test'), ]; }); await mapRepository.save([ @@ -303,7 +303,7 @@ describe('MapService 테스트', () => { await mapRepository.save(publicMap); await expect( - mapService.addPlace(1, 777777, 'RED' as Color, 'test'), + mapService.addPlace(1, 777777, Color.RED, 'test'), ).rejects.toThrow(InvalidPlaceToMapException); }); @@ -314,11 +314,11 @@ describe('MapService 테스트', () => { publicMapEntity.id, ); publicMapEntity.mapPlaces = []; - publicMapEntity.addPlace(alreadyAddPlace.id, 'RED' as Color, 'test'); + publicMapEntity.addPlace(alreadyAddPlace.id, Color.RED, 'test'); await mapRepository.save(publicMapEntity); await expect( - mapService.addPlace(1, 1, 'RED' as Color, 'test'), + mapService.addPlace(1, 1, Color.RED, 'test'), ).rejects.toThrow(DuplicatePlaceToMapException); }); @@ -329,7 +329,7 @@ describe('MapService 테스트', () => { const expectedResult = { placeId: addPlace.id, comment: 'test', - color: 'RED' as Color, + color: Color.RED, }; const result = await mapService.addPlace( From 0ee7f5d64a4a826901c47e2144355ce12f918658 Mon Sep 17 00:00:00 2001 From: hyohyo12 <129946082+hyohyo12@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:35:16 +0900 Subject: [PATCH 065/139] =?UTF-8?q?fix:=20beforeAll=20=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94/=20Color=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=A9=EB=B2=95=20=EB=B3=80=EA=B2=BD=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration-test/map.integration.test.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/test/map/integration-test/map.integration.test.ts b/backend/test/map/integration-test/map.integration.test.ts index a15e5b6a..02eca8fb 100644 --- a/backend/test/map/integration-test/map.integration.test.ts +++ b/backend/test/map/integration-test/map.integration.test.ts @@ -69,8 +69,7 @@ describe('MapController 통합 테스트', () => { placeRepository = new PlaceRepository(dataSource); userRepository = new UserRepository(dataSource); - await userRepository.query(`ALTER TABLE USER AUTO_INCREMENT = 1`); - await userRepository.delete({}); + await initMapUserPlaceTable(mapRepository, userRepository, placeRepository); const [fakeUser1Entity, fakeUser2Entity] = await userRepository.save([ fakeUser1, @@ -241,7 +240,7 @@ describe('MapController 통합 테스트', () => { publicMapsWithPlace.forEach((publicMapWithPlace) => { publicMapWithPlace.mapPlaces = []; publicMapWithPlace.mapPlaces.push( - MapPlace.of(1, publicMapWithPlace, 'RED' as Color, 'test'), + MapPlace.of(1, publicMapWithPlace, Color.RED, 'test'), ); }); await mapRepository.save([ @@ -272,27 +271,30 @@ describe('MapController 통합 테스트', () => { }); it('GET /maps?query 에 대한 요청으로 공개 되어있고 query 와 유사한 title을 가지는 지도들을 반환한다.', async () => { - const coolMaps = createPublicMapsWithTitle( + const publicMapsWithCoolTitle = createPublicMapsWithTitle( 2, fakeUser1, 'cool test title', ); - const coolMapsWithPlace = createPublicMapsWithTitle( + const publicMapsWithCoolTitleAndPlace = createPublicMapsWithTitle( 3, fakeUser1, 'cool title', ); const privateMaps = createPrivateMaps(5, fakeUser1); - coolMapsWithPlace.forEach((publicMapWithPlace) => { + publicMapsWithCoolTitleAndPlace.forEach((publicMapWithPlace) => { publicMapWithPlace.mapPlaces = []; publicMapWithPlace.mapPlaces.push( - MapPlace.of(1, publicMapWithPlace, 'RED' as Color, 'test'), + MapPlace.of(1, publicMapWithPlace, Color.RED, 'test'), ); }); - coolMaps.forEach((publicMap) => { + publicMapsWithCoolTitle.forEach((publicMap) => { publicMap.mapPlaces = []; }); - const publicMaps = [...coolMaps, ...coolMapsWithPlace]; + const publicMaps = [ + ...publicMapsWithCoolTitle, + ...publicMapsWithCoolTitleAndPlace, + ]; await mapRepository.save([...publicMaps, ...privateMaps]); return request(app.getHttpServer()) From 38ee08079c41a45250cf0e4c9d8db1c21902fffd Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sat, 23 Nov 2024 18:18:03 +0900 Subject: [PATCH 066/139] =?UTF-8?q?feat:=20user=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #preview --- frontend/src/api/axiosInstance.ts | 10 +++----- frontend/src/hooks/api/useLoginMutation.ts | 1 - frontend/src/hooks/api/useUserInfoQuery.ts | 11 ++++++++ frontend/src/pages/HomePage/Header.tsx | 29 ++++++++++++++++++++-- 4 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 frontend/src/hooks/api/useUserInfoQuery.ts diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts index 64f8e6ff..7c9b2842 100644 --- a/frontend/src/api/axiosInstance.ts +++ b/frontend/src/api/axiosInstance.ts @@ -1,19 +1,15 @@ import axios from 'axios'; import { AXIOS_BASE_URL, NETWORK } from '@/constants/api'; -import { handleAPIError } from './interceptors'; - -const accessToken = import.meta.env.VITE_TEST_ACCESS_TOKEN as string; +import { checkAndSetToken, handleAPIError } from './interceptors'; export const axiosInstance = axios.create({ baseURL: AXIOS_BASE_URL, timeout: NETWORK.TIMEOUT, - headers: { - Authorization: `Bearer ${accessToken}`, - }, withCredentials: true, useAuth: true, }); -//axiosInstance.interceptors.request.use(checkAndSetToken); + +axiosInstance.interceptors.request.use(checkAndSetToken); axiosInstance.interceptors.response.use((response) => response, handleAPIError); diff --git a/frontend/src/hooks/api/useLoginMutation.ts b/frontend/src/hooks/api/useLoginMutation.ts index 1f42fb69..81bc79fe 100644 --- a/frontend/src/hooks/api/useLoginMutation.ts +++ b/frontend/src/hooks/api/useLoginMutation.ts @@ -12,7 +12,6 @@ export const useLogInMutation = () => { const logInMutation = useMutation({ mutationFn: postLogIn, onSuccess: ({ accessToken }) => { - console.log('token', accessToken); localStorage.setItem(`ACCESS_TOKEN_KEY`, accessToken); axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`; logIn(); diff --git a/frontend/src/hooks/api/useUserInfoQuery.ts b/frontend/src/hooks/api/useUserInfoQuery.ts new file mode 100644 index 00000000..52ac424e --- /dev/null +++ b/frontend/src/hooks/api/useUserInfoQuery.ts @@ -0,0 +1,11 @@ +import { getUserInfo } from '@/api/auth'; +import { User } from '@/types'; +import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; + +export const useUserInfoQuery = () => { + const { data: userInfo } = useSuspenseQuery({ + queryKey: ['user'], + queryFn: getUserInfo, + }); + return userInfo; +}; diff --git a/frontend/src/pages/HomePage/Header.tsx b/frontend/src/pages/HomePage/Header.tsx index e9c933fb..b47a2a01 100644 --- a/frontend/src/pages/HomePage/Header.tsx +++ b/frontend/src/pages/HomePage/Header.tsx @@ -1,17 +1,42 @@ +import { useUserInfoQuery } from '@/hooks/api/useUserInfoQuery'; import { useStore } from '@/store/useStore'; +import { useEffect } from 'react'; const Header = () => { const user = useStore((state) => state.user); const isLogged = useStore((state) => state.isLogged); + const setUser = useStore((state) => state.setUser); const handleLogin = () => { - window.location.href = import.meta.env.VITE_GOOGLE_AUTH_URL; + window.location.href = import.meta.env.VITE_GOOGLE_AUTH_URL_TEST; }; + const handleAdminLogin = () => { + localStorage.setItem( + 'ACCESS_TOKEN_KEY', + import.meta.env.VITE_TEST_ACCESS_TOKEN, + ); + console.log(localStorage.getItem('ACCESS_TOKEN_KEY')); + }; + + useEffect(() => { + if (isLogged) { + const data = useUserInfoQuery(); + setUser(data); + console.log('user', data); + } + }, [isLogged]); + return (

Logo

+
From e4f76b1a754507735b800a9cbe41cf1857749912 Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sat, 23 Nov 2024 20:05:29 +0900 Subject: [PATCH 067/139] =?UTF-8?q?feat:=20=EB=B0=B0=ED=8F=AC=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #preview --- frontend/src/api/interceptors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/api/interceptors.ts b/frontend/src/api/interceptors.ts index 01296af1..4271c4cf 100644 --- a/frontend/src/api/interceptors.ts +++ b/frontend/src/api/interceptors.ts @@ -7,6 +7,7 @@ interface ErrorResponseData { export const checkAndSetToken = (config: InternalAxiosRequestConfig) => { if (!config.useAuth || !config.headers || config.headers.Authorization) { + console.log(config); return config; } From 662a3aefa8c244b628ca3316d19fb5f9e9b7bf8c Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sat, 23 Nov 2024 20:16:34 +0900 Subject: [PATCH 068/139] =?UTF-8?q?fix:=20useAuth=20=EB=B3=80=EA=B2=BD=20#?= =?UTF-8?q?124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #preview --- frontend/src/api/interceptors.ts | 1 - frontend/src/api/map/index.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/api/interceptors.ts b/frontend/src/api/interceptors.ts index 4271c4cf..655d0ab9 100644 --- a/frontend/src/api/interceptors.ts +++ b/frontend/src/api/interceptors.ts @@ -10,7 +10,6 @@ export const checkAndSetToken = (config: InternalAxiosRequestConfig) => { console.log(config); return config; } - const accessToken = localStorage.getItem(`ACCESS_TOKEN_KEY`); if (!accessToken) { window.location.href = ROUTES.ROOT; diff --git a/frontend/src/api/map/index.ts b/frontend/src/api/map/index.ts index 1c2ae3d8..503c4b44 100644 --- a/frontend/src/api/map/index.ts +++ b/frontend/src/api/map/index.ts @@ -34,8 +34,8 @@ export const getMapList = async (pageParam: number) => { const { data } = await axiosInstance.get(END_POINTS.MAPS, { params: { page: pageParam, - useAuth: false, }, + useAuth: false, }); return data; }; From bcc8c63ecd8fc150dcc15a04cb585f29e4a42aaf Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sat, 23 Nov 2024 20:19:48 +0900 Subject: [PATCH 069/139] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B3=80=EC=88=98=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #preview --- frontend/src/pages/HomePage/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/HomePage/Header.tsx b/frontend/src/pages/HomePage/Header.tsx index b47a2a01..158f438e 100644 --- a/frontend/src/pages/HomePage/Header.tsx +++ b/frontend/src/pages/HomePage/Header.tsx @@ -7,7 +7,7 @@ const Header = () => { const isLogged = useStore((state) => state.isLogged); const setUser = useStore((state) => state.setUser); const handleLogin = () => { - window.location.href = import.meta.env.VITE_GOOGLE_AUTH_URL_TEST; + window.location.href = import.meta.env.VITE_GOOGLE_AUTH_URL; }; const handleAdminLogin = () => { localStorage.setItem( From e6fe62888aaec4ad9ca790d83d6b2d90eb6bc33b Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sun, 24 Nov 2024 00:15:45 +0900 Subject: [PATCH 070/139] =?UTF-8?q?feat:=20=EC=83=88=20=EC=9E=A5=EC=86=8C?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20UI=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Place/SearchGoogleResults.tsx | 20 +++++++++++++ .../src/components/Place/SearchResults.tsx | 28 ++++++++++++------- 2 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/Place/SearchGoogleResults.tsx diff --git a/frontend/src/components/Place/SearchGoogleResults.tsx b/frontend/src/components/Place/SearchGoogleResults.tsx new file mode 100644 index 00000000..185fce91 --- /dev/null +++ b/frontend/src/components/Place/SearchGoogleResults.tsx @@ -0,0 +1,20 @@ +import { useGooglePlaceQuery } from '@/hooks/api/useGooglePlaceQuery'; +import GooglePlaceItem from './GooglePlaceItem'; + +type SearchGoogleResultsProps = { + query: string; +}; + +const SearchGoogleResults = ({ query }: SearchGoogleResultsProps) => { + const places = useGooglePlaceQuery(query); + + return ( +
+ {places.map((place) => ( + + ))} +
+ ); +}; + +export default SearchGoogleResults; diff --git a/frontend/src/components/Place/SearchResults.tsx b/frontend/src/components/Place/SearchResults.tsx index 6b4339f6..4f3572d4 100644 --- a/frontend/src/components/Place/SearchResults.tsx +++ b/frontend/src/components/Place/SearchResults.tsx @@ -1,15 +1,16 @@ import { getPlace } from '@/api/place'; -import React from 'react'; -import { Place } from '@/types'; +import React, { useMemo } from 'react'; +import { CustomPlace, Place } from '@/types'; import PlaceItem from './PlaceItem'; import Marker from '@/components/Marker/Marker'; import { useInfiniteScroll } from '@/hooks/useInfiniteScroll'; type SearchResultsProps = { query: string; + places: (Place & CustomPlace)[]; }; -const SearchResults = ({ query }: SearchResultsProps) => { +const SearchResults = ({ query, places }: SearchResultsProps) => { const { ref, data, isFetchingNextPage, hasNextPage } = useInfiniteScroll< Place[] >({ @@ -25,6 +26,11 @@ const SearchResults = ({ query }: SearchResultsProps) => { const isEmpty = isEmptyResults(data); + const placesSet = useMemo( + () => new Set(places.map((place) => place.id)), + [places], + ); + return (
{query &&

"{query}"에 대한 검색결과

} @@ -35,13 +41,15 @@ const SearchResults = ({ query }: SearchResultsProps) => { {page.map((place: Place) => ( - + {!placesSet.has(place.id) && ( + + )} ))} From 699c9af5efafac214bdb6c32055c09fee6110840 Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sun, 24 Nov 2024 00:16:15 +0900 Subject: [PATCH 071/139] =?UTF-8?q?feat:=20=EC=BD=94=EC=8A=A4=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20UI=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Marker/Polyline.tsx | 13 +++++++++++++ .../src/components/Place/GooglePlaceItem.tsx | 16 ++++++++++++++++ frontend/src/components/Place/PlaceListPanel.tsx | 16 ++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Marker/Polyline.tsx create mode 100644 frontend/src/components/Place/GooglePlaceItem.tsx diff --git a/frontend/src/components/Marker/Polyline.tsx b/frontend/src/components/Marker/Polyline.tsx new file mode 100644 index 00000000..cfa93ff3 --- /dev/null +++ b/frontend/src/components/Marker/Polyline.tsx @@ -0,0 +1,13 @@ +import { usePoyline } from '@/hooks/usePolyline'; + +type PolylineProps = { + points: google.maps.LatLngLiteral[]; + color: string; +}; + +const Polyline = ({ points, color }: PolylineProps) => { + const polyline = usePoyline(points, color); + return <>; +}; + +export default Polyline; diff --git a/frontend/src/components/Place/GooglePlaceItem.tsx b/frontend/src/components/Place/GooglePlaceItem.tsx new file mode 100644 index 00000000..0267cb78 --- /dev/null +++ b/frontend/src/components/Place/GooglePlaceItem.tsx @@ -0,0 +1,16 @@ +import { GooglePlaceResponse } from '@/api/place'; + +type GooglePlaceItemProps = { + place: GooglePlaceResponse; +}; + +const GooglePlaceItem = ({ place }: GooglePlaceItemProps) => { + return ( +
+
{place.name}
+
{place.formed_address}
+
+ ); +}; + +export default GooglePlaceItem; diff --git a/frontend/src/components/Place/PlaceListPanel.tsx b/frontend/src/components/Place/PlaceListPanel.tsx index 4b9b8584..023cad4e 100644 --- a/frontend/src/components/Place/PlaceListPanel.tsx +++ b/frontend/src/components/Place/PlaceListPanel.tsx @@ -2,12 +2,13 @@ import { CustomPlace, Place } from '@/types'; import BaseWrapper from '../common/BaseWrapper'; import Box from '../common/Box'; import PlaceItem from './PlaceItem'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import Marker from '../Marker/Marker'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { useAddPlaceToCourseMutation } from '@/hooks/api/useAddPlaceToCourseMutation'; import { useParams } from 'react-router-dom'; import { useStore } from '@/store/useStore'; +import Polyline from '../Marker/Polyline'; type PlaceListPanelProps = { places: (Place & CustomPlace)[]; @@ -25,6 +26,13 @@ const PlaceListPanel = ({ const addPlaceToCourseMutation = useAddPlaceToCourseMutation(); const addToast = useStore((state) => state.addToast); + const points = useMemo(() => { + return places.map((place) => ({ + lat: place.location.latitude, + lng: place.location.longitude, + })); + }, [places]); + const handleDragend = (result: any) => { if (!result.destination) { return; @@ -101,15 +109,19 @@ const PlaceListPanel = ({ )} - {places.map((place) => ( + {places.map((place, index) => ( ))} + {isDraggable && } ); }; From b41754851fadd597cdeedc672d01ea488263122a Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sun, 24 Nov 2024 00:16:45 +0900 Subject: [PATCH 072/139] =?UTF-8?q?feat:=20=EC=83=88=20=EC=9E=A5=EC=86=8C?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20api=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #preview --- frontend/src/api/place/index.ts | 16 ++++++++++++++++ frontend/src/constants/api.ts | 2 +- frontend/src/hooks/api/useGooglePlaceQuery.ts | 11 +++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 frontend/src/hooks/api/useGooglePlaceQuery.ts diff --git a/frontend/src/api/place/index.ts b/frontend/src/api/place/index.ts index 3534e898..7aeb964f 100644 --- a/frontend/src/api/place/index.ts +++ b/frontend/src/api/place/index.ts @@ -14,6 +14,22 @@ export const getPlace = async (queryString: string, pageParam: number) => { return data; }; +export type GooglePlaceResponse = { + photoReference: string; +} & Place; + +export const getGooglePlace = async (query: string) => { + const { data } = await axiosInstance.get( + END_POINTS.GOOGLE_PLACE_SEARCH, + { + params: { + query, + }, + }, + ); + return data; +}; + type AddPlaceParams = { id: number; } & CustomPlace; diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index dc3ab64e..dffa6a5a 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -26,7 +26,7 @@ export const END_POINTS = { `/courses/${courseId}/visibility`, DELETE_PLACE_TO_COURSE: (courseId: number, placeId: number) => `/courses/${courseId}/places/${placeId}`, - + GOOGLE_PLACE_SEARCH: '/places/search', GOOGLE_LOGIN: '/oauth/google/signIn', LOGOUT: 'oauth/signOut', MY_MAP: '/maps/my', diff --git a/frontend/src/hooks/api/useGooglePlaceQuery.ts b/frontend/src/hooks/api/useGooglePlaceQuery.ts new file mode 100644 index 00000000..567d9d00 --- /dev/null +++ b/frontend/src/hooks/api/useGooglePlaceQuery.ts @@ -0,0 +1,11 @@ +import { getGooglePlace, GooglePlaceResponse } from '@/api/place'; +import { useSuspenseQuery } from '@tanstack/react-query'; + +export const useGooglePlaceQuery = (query: string) => { + if (!query) return []; + const { data } = useSuspenseQuery({ + queryKey: ['googlePlace'], + queryFn: () => getGooglePlace(query), + }); + return data; +}; From 9f1d8e497ea3d3fc206413a38fb26ba857a3f92f Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Sun, 24 Nov 2024 00:19:30 +0900 Subject: [PATCH 073/139] =?UTF-8?q?feat:=20usePolyline=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=EB=A7=88=EC=BB=A4=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #preview --- frontend/src/hooks/useMarker.ts | 39 ++++++++++++++++++++++++------- frontend/src/hooks/usePolyline.ts | 31 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 frontend/src/hooks/usePolyline.ts diff --git a/frontend/src/hooks/useMarker.ts b/frontend/src/hooks/useMarker.ts index fe2aa622..d4eff0df 100644 --- a/frontend/src/hooks/useMarker.ts +++ b/frontend/src/hooks/useMarker.ts @@ -2,30 +2,40 @@ import { ICONS } from '@/constants/icon'; import { useStore } from '@/store/useStore'; import { useEffect, useState } from 'react'; -type MarkerEventProps = { +type MarkerCustomProps = { onClick?: (e: google.maps.MapMouseEvent) => void; + color?: string; + category?: string; + order?: number; }; export type MarkerProps = Omit< google.maps.marker.AdvancedMarkerElementOptions, 'map' > & - MarkerEventProps; + MarkerCustomProps; export const useMarker = (props: MarkerProps) => { const [marker, setMarker] = useState(null); const map = useStore((state) => state.googleMap); - const { onClick, ...markerOptions } = props; + const { onClick, category, color, order, ...markerOptions } = props; + const contentDiv = document.createElement('div'); useEffect(() => { if (!map) { return; } - const contentDiv = document.createElement('div'); - contentDiv.innerHTML = ICONS.RED_PIN(); - contentDiv.style.borderRadius = '50%'; - contentDiv.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.3)'; + const categoryCode = + categoryObj[(category as keyof typeof categoryObj) ?? '기본']; + console.log(color?.toLocaleLowerCase(), category); + console.log(categoryCode ?? 'pin', color?.toLocaleLowerCase() ?? 'defualt'); + contentDiv.innerHTML = order + ? ` +
${order}
+ ` + : ``; + const newMarker = new google.maps.marker.AdvancedMarkerElement({ ...markerOptions, content: contentDiv, @@ -41,9 +51,13 @@ export const useMarker = (props: MarkerProps) => { useEffect(() => { if (!marker) return; - + google.maps.event.addListener(marker, 'click', () => { + console.log(1); + }); if (onClick) { - google.maps.event.addListener(marker, 'click', onClick); + google.maps.event.addListener(marker, 'click', () => { + console.log(1); + }); } return () => { @@ -52,3 +66,10 @@ export const useMarker = (props: MarkerProps) => { }, [marker, onClick]); return marker; }; + +const categoryObj = { + 명소: 'camera', + 맛집: 'restaurant', + 카페: 'cafe', + 기본: 'pin', +}; diff --git a/frontend/src/hooks/usePolyline.ts b/frontend/src/hooks/usePolyline.ts new file mode 100644 index 00000000..19e4f38b --- /dev/null +++ b/frontend/src/hooks/usePolyline.ts @@ -0,0 +1,31 @@ +import { useStore } from '@/store/useStore'; +import { useEffect, useState } from 'react'; + +export const usePoyline = ( + path: google.maps.LatLngLiteral[], + color: string, +) => { + const [polyline, setPolyline] = useState(null); + const map = useStore((state) => state.googleMap); + + useEffect(() => { + if (!map) { + return; + } + const newPolyline = new google.maps.Polyline({ + path, + strokeColor: color, + strokeOpacity: 1.0, + strokeWeight: 2, + }); + newPolyline.setMap(map); + setPolyline(newPolyline); + console.log('polyline', newPolyline); + return () => { + newPolyline.setMap(null); + setPolyline(null); + }; + }, [map, path]); + + return polyline; +}; From a55afe3b6433afc4c4a3c106fb172b79d97fe079 Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Mon, 25 Nov 2024 12:32:24 +0900 Subject: [PATCH 074/139] =?UTF-8?q?feat:=20=EC=BD=94=EC=8A=A4=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20UI,=20=ED=8E=98=EC=9D=B4=EC=A7=80=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Form/EditCourseForm.tsx | 61 ++++++++++++ .../src/components/Map/CourseDetailBoard.tsx | 99 +++++++++++++++++++ frontend/src/components/Map/CourseItem.tsx | 40 ++++++++ .../src/components/Map/CourseListPanel.tsx | 39 ++++++++ frontend/src/pages/CourseEditPage.tsx | 18 ++++ .../src/pages/MapDetail/CourseDetailPage.tsx | 13 +++ frontend/src/types/index.ts | 13 +-- 7 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/Form/EditCourseForm.tsx create mode 100644 frontend/src/components/Map/CourseDetailBoard.tsx create mode 100644 frontend/src/components/Map/CourseItem.tsx create mode 100644 frontend/src/components/Map/CourseListPanel.tsx create mode 100644 frontend/src/pages/CourseEditPage.tsx create mode 100644 frontend/src/pages/MapDetail/CourseDetailPage.tsx diff --git a/frontend/src/components/Form/EditCourseForm.tsx b/frontend/src/components/Form/EditCourseForm.tsx new file mode 100644 index 00000000..8d278ed7 --- /dev/null +++ b/frontend/src/components/Form/EditCourseForm.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import BaseWrapper from '../common/BaseWrapper'; +import FormWrapper from './FormWrapper'; +import { BaseMap, Course } from '@/types'; +import { useEditCourseMutation } from '@/hooks/api/useEditCourseMutation'; +import { useMapForm } from '@/hooks/useMapForm'; + +import { useNavigate } from 'react-router-dom'; +import { useStore } from '@/store/useStore'; + +type EditCourseFormProps = { + courseData: Course; +}; + +const EditCourseForm = ({ courseData }: EditCourseFormProps) => { + const editCourseMutation = useEditCourseMutation(); + const navigate = useNavigate(); + const addToast = useStore((state) => state.addToast); + + const initialCourseData: BaseMap = { + title: courseData.title, + description: courseData.description, + isPublic: courseData.isPublic, + thumbnailUrl: courseData.thumbnailUrl, + mode: 'COURSE', + }; + + const { mapInfo, updateMapInfo, isMapInfoValid } = + useMapForm(initialCourseData); + + const onSubmitHandler = (e: React.FormEvent) => { + e.preventDefault(); + + editCourseMutation.mutate( + { courseId: courseData.id, ...mapInfo }, + { + onSuccess: () => { + addToast('코스가 수정되었습니다.', '', 'success'); + navigate(`/course/${courseData.id}`); + }, + }, + ); + }; + + return ( + <> + + + + + ); +}; + +export default EditCourseForm; diff --git a/frontend/src/components/Map/CourseDetailBoard.tsx b/frontend/src/components/Map/CourseDetailBoard.tsx new file mode 100644 index 00000000..c88e1f31 --- /dev/null +++ b/frontend/src/components/Map/CourseDetailBoard.tsx @@ -0,0 +1,99 @@ +import BaseWrapper from '@/components/common/BaseWrapper'; +import Box from '@/components/common/Box'; +import DashBoardHeader from '@/components/common/DashBoardHeader'; +import { Course, Map } from '@/types'; +import PlaceItem from '@/components/Place/PlaceItem'; +import { useMemo, useState } from 'react'; +import PlaceDetailPanel from '@/components/Place/PlaceDetailPanel'; +import { useStore } from '@/store/useStore'; +import SideContainer from '@/components/common/SideContainer'; +import Marker from '@/components/Marker/Marker'; +import DeleteMapButton from './DeleteMapButton'; +import EditMapButton from './EditMapButton'; +import Polyline from '../Marker/Polyline'; + +type MapDetailBoardProps = { + courseData: Course; +}; + +const CourseDetailBoard = ({ courseData }: MapDetailBoardProps) => { + const { title, description, isPublic, thumbnailUrl, pinCount, places } = + courseData; + const [isSidePanelOpen, setIsSidePanelOpen] = useState(false); + const activePlace = useStore((state) => state.place); + + const customPlace = useMemo( + () => places.find((place) => place.id === activePlace.id), + [places, activePlace.id], + ); + + const points = useMemo(() => { + return places.map((place) => ({ + lat: place.location.latitude, + lng: place.location.longitude, + })); + }, [places]); + console.log(points, 'points'); + return ( + + + +
+ +
+ +

|

+ +
+
+ {thumbnailUrl ? ( + thumbnail + ) : ( + map + )} +

지도 소개

+
+ {description} +
+
+ + + {places && places.length > 0 ? ( + <> + {places.map((place) => ( +
setIsSidePanelOpen(true)}> + + +
+ ))} + + ) : ( +
+

등록된 장소가 없습니다.

+
+ )} +
+
+ {isSidePanelOpen && customPlace && ( + setIsSidePanelOpen(false)} + /> + )} + +
+ ); +}; + +export default CourseDetailBoard; diff --git a/frontend/src/components/Map/CourseItem.tsx b/frontend/src/components/Map/CourseItem.tsx new file mode 100644 index 00000000..b0b688b8 --- /dev/null +++ b/frontend/src/components/Map/CourseItem.tsx @@ -0,0 +1,40 @@ +import { MapItemType } from '@/types'; +import { Link } from 'react-router-dom'; +import PinIcon from '@/components/PinIcon'; +import MapThumbnail from './MapThumbnail'; + +type CourseItemProps = { + courseItem: MapItemType; +}; + +const CourseItem = ({ courseItem }: CourseItemProps) => { + return ( + +
+
+ {courseItem.thumbnailUrl.startsWith('https://example') ? ( + + ) : ( + + )} +
+

{courseItem.title}

+
+ +

{courseItem.user.nickname}

+ +
+

{courseItem.pinCount}

+
+
+
+ + ); +}; + +export default CourseItem; diff --git a/frontend/src/components/Map/CourseListPanel.tsx b/frontend/src/components/Map/CourseListPanel.tsx new file mode 100644 index 00000000..4a93ce29 --- /dev/null +++ b/frontend/src/components/Map/CourseListPanel.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { getCourseList } from '@/api/course'; +import { useInfiniteScroll } from '@/hooks/useInfiniteScroll'; +import { CourseList, MapItemType } from '@/types'; + +import MapItem from '@/components/Map/MapItem'; +import CourseItem from './CourseItem'; + +const CourseListPanel = () => { + const { data, isFetchingNextPage, hasNextPage, ref } = + useInfiniteScroll({ + queryKey: ['courseList'], + queryFn: ({ pageParam }) => getCourseList(pageParam), + getNextPageParam: (lastPage) => { + return lastPage.currentPage < lastPage.totalPages + ? lastPage.currentPage + 1 + : undefined; + }, + fetchWithoutQuery: true, + }); + + return ( + <> +
+ {data?.pages.map((page, index) => ( + + {page.courses.map((map: MapItemType) => ( + + ))} + + ))} +
+
+ + ); +}; + +export default CourseListPanel; diff --git a/frontend/src/pages/CourseEditPage.tsx b/frontend/src/pages/CourseEditPage.tsx new file mode 100644 index 00000000..3776405a --- /dev/null +++ b/frontend/src/pages/CourseEditPage.tsx @@ -0,0 +1,18 @@ +import SideContainer from '@/components/common/SideContainer'; +import EditCourseForm from '@/components/Form/EditCourseForm'; + +import { useCourseQuery } from '@/hooks/api/useCourseQuery'; + +import { useParams } from 'react-router-dom'; + +const CourseEditPage = () => { + const { id } = useParams(); + const courseData = useCourseQuery(Number(id)); + return ( + + + + ); +}; + +export default CourseEditPage; diff --git a/frontend/src/pages/MapDetail/CourseDetailPage.tsx b/frontend/src/pages/MapDetail/CourseDetailPage.tsx new file mode 100644 index 00000000..4f3d6c58 --- /dev/null +++ b/frontend/src/pages/MapDetail/CourseDetailPage.tsx @@ -0,0 +1,13 @@ +import { useParams } from 'react-router-dom'; + +import CourseDetailBoard from '@/components/Map/CourseDetailBoard'; +import { useCourseQuery } from '@/hooks/api/useCourseQuery'; + +const CourseDetailPage = () => { + const { id } = useParams(); + const courseData = useCourseQuery(Number(id)); + + return ; +}; + +export default CourseDetailPage; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f08b3958..2b6431b6 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -58,6 +58,12 @@ export type MapList = { currentPage: number; }; +export type CourseList = { + courses: MapItemType[]; + totalPages: number; + currentPage: number; +}; + export type CreateMapType = 'MAP' | 'COURSE'; export type BaseMap = { @@ -92,12 +98,7 @@ export type MarkerColor = | 'GREEN' | 'BLUE' | 'PURPLE'; -export type MarkerCategory = - | 'restaurant' - | 'cafe' - | 'attraction' - | string - | undefined; +export type MarkerCategory = 'restaurant' | 'cafe' | 'attraction' | string; export type PlaceMarker = { placeId: number; From 6c6d948041f3be4539def7efac12fce315892d78 Mon Sep 17 00:00:00 2001 From: 1119wj <1119wj@naver.com> Date: Mon, 25 Nov 2024 12:33:31 +0900 Subject: [PATCH 075/139] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EB=B3=80=EA=B2=BD=20#124?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/HomePage/HomePage.tsx | 14 +++++++-- frontend/src/pages/HomePage/MainListPanel.tsx | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/HomePage/MainListPanel.tsx diff --git a/frontend/src/pages/HomePage/HomePage.tsx b/frontend/src/pages/HomePage/HomePage.tsx index 5aa720ce..a739ebfe 100644 --- a/frontend/src/pages/HomePage/HomePage.tsx +++ b/frontend/src/pages/HomePage/HomePage.tsx @@ -1,12 +1,22 @@ import Footer from '@/pages/HomePage/Footer'; import Header from '@/pages/HomePage/Header'; -import MapListPanel from '@/components/Map/MapListPanel'; + +import NavigateButton from '@/components/common/NavigateButton'; + +import MainListPanel from './MainListPanel'; const Homepage = () => { return ( <>
- +
+ +
+