From 0f22b49a5ac533bf38cac6b9ec5270be300095d4 Mon Sep 17 00:00:00 2001 From: mirlee0304 Date: Tue, 24 Oct 2023 15:32:04 +0900 Subject: [PATCH 1/3] my review, random review --- .../review/repositories/review.repository.ts | 18 ++- backend/src/review/review.controller.ts | 18 +++ backend/src/test/review/get-my-review.spec.ts | 124 ++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 backend/src/test/review/get-my-review.spec.ts diff --git a/backend/src/review/repositories/review.repository.ts b/backend/src/review/repositories/review.repository.ts index 0cb2762..1df7fde 100644 --- a/backend/src/review/repositories/review.repository.ts +++ b/backend/src/review/repositories/review.repository.ts @@ -35,7 +35,7 @@ export class ReviewRepository extends Repository { return find[0]; } - private findFull(options: FindOptionsWhere) { + findFull(options: FindOptionsWhere) { return this.find({ where: options, relations: { @@ -44,4 +44,20 @@ export class ReviewRepository extends Repository { }, }); } + + findOfUserId(username: string) { + return this.findFull({ + user: { + username: username, + }, + }); + } + + findRandomReviews(limit: number) { + return this.createQueryBuilder() + .select() + .orderBy('RAND()') // for MySQL. Change this according to your database + .take(limit) + .getMany(); + } } diff --git a/backend/src/review/review.controller.ts b/backend/src/review/review.controller.ts index 1d178e4..3bbdca1 100644 --- a/backend/src/review/review.controller.ts +++ b/backend/src/review/review.controller.ts @@ -24,6 +24,8 @@ import { ReviewRepository } from './repositories/review.repository'; import { ReviewListDto } from './dtos/out-dtos/reviewList.dto'; import { ImageUploadDto } from './dtos/out-dtos/imageUpload.dto'; import { ReviewDetailDto } from './dtos/out-dtos/reviewDetail.dto'; +import { UserEntity } from '../user/models/user.entity'; +import { UserRepository } from '../user/repostiories/user.repository'; @ApiTags('reviews') @Controller('reviews') @@ -86,4 +88,20 @@ export class ReviewController { return new ImageUploadDto(image); } + + @UseGuards(JwtAccessGuard) + @Get('/my') + async getMyReview(@Req() { user }: UserRequest) { + console.log(user); + const reviews = user.reviews; + return new ReviewListDto(reviews); + } + + @UseGuards(JwtAccessGuard) + @Get('/random') + async getReviewRandom() { + const limit = 5; // Define the number of random reviews you want to retrieve + const randomReviews = await this.reviewRepository.findRandomReviews(limit); + return new ReviewListDto(randomReviews); + } } diff --git a/backend/src/test/review/get-my-review.spec.ts b/backend/src/test/review/get-my-review.spec.ts new file mode 100644 index 0000000..8883389 --- /dev/null +++ b/backend/src/test/review/get-my-review.spec.ts @@ -0,0 +1,124 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { AppModule } from '../../app.module'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; +import { appSetting } from '../../main'; +import * as supertest from 'supertest'; +import { UserEntity } from '../../user/models/user.entity'; +import { UserFixture } from '../fixture/user.fixture'; +import { HttpStatus } from '@nestjs/common'; +import { RestaurantEntity } from '../../review/models/restaurant.entity'; +import { RestaurantFixture } from '../fixture/restaurant.fixture'; +import { ReviewEntity } from '../../review/models/review.entity'; +import { ReviewFixture } from '../fixture/review.fixture'; +import { ImageFixture } from '../fixture/image.fixture'; +import { validateReview, validateReviewList } from './validateReviewList'; + +describe('Review test', () => { + let testServer: NestExpressApplication; + let dataSource: DataSource; + let user: UserEntity; + let accessToken: string; + let restaurant: RestaurantEntity; + let review: ReviewEntity; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + testServer = module.createNestApplication(); + dataSource = testServer.get(DataSource); + await dataSource.synchronize(true); + appSetting(testServer); + + await testServer.init(); + }); + + beforeEach(async () => { + await dataSource.synchronize(true); + + user = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const { body } = await supertest(testServer.getHttpServer()) + .post('/auth/login') + .send({ + username: 'hello', + password: 'world', + }) + .expect(HttpStatus.CREATED); + + accessToken = body.accessToken; + + restaurant = await RestaurantFixture.create({}); + const image = await ImageFixture.create({}); + review = await ReviewFixture.create({ + restaurant, + images: [image], + user, + }); + }); + + it('unauthorized', async () => { + await supertest(testServer.getHttpServer()) + .get(`/reviews/my`) + .expect(HttpStatus.UNAUTHORIZED); + }); + + it('OK', async () => { + await supertest(testServer.getHttpServer()) + .get(`/reviews/my`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + }); + + it('DTO check', async () => { + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/my`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + validateReview(body); + }); + + it('리뷰 없으면 빈 배열 준다', async () => { + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/my`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + expect(body.reviewList).toStrictEqual([]); + }); + + it('should fetch user reviews in time order', async () => { + // Create multiple reviews for the user with different timestamps + const review1 = await ReviewFixture.create({ + restaurant, + images: [await ImageFixture.create({})], + user, + }); + const review2 = await ReviewFixture.create({ + restaurant, + images: [await ImageFixture.create({})], + user, + }); + + // Fetch the user's reviews + const { body } = await supertest(testServer.getHttpServer()) + .get('/reviews/my') + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + // Assuming the reviews are ordered by the most recent time, verify if the retrieved reviews are in the correct order + const reviewList = body.reviewList; + for (let i = 0; i < reviewList.length - 1; i++) { + const currentReview = new Date(reviewList[i].issuedAt); + const nextReview = new Date(reviewList[i + 1].issuedAt); + expect(currentReview >= nextReview).toBe(true); + } + }); +}); From ef8edcf94c961ecdbfffcaee399777cb6d26fd9d Mon Sep 17 00:00:00 2001 From: eldpswp99 Date: Tue, 24 Oct 2023 16:06:06 +0900 Subject: [PATCH 2/3] fix test and feature --- .../review/repositories/review.repository.ts | 19 ++- backend/src/review/review.controller.ts | 31 ++-- backend/src/test/review/create-review.spec.ts | 2 +- backend/src/test/review/get-my-review.spec.ts | 31 ++-- .../src/test/review/get-review-detail.spec.ts | 2 +- .../test/review/get-review-restaurant.spec.ts | 2 +- backend/src/test/review/random-review.spec.ts | 148 ++++++++++++++++++ backend/src/test/review/upload-image.spec.ts | 2 +- 8 files changed, 202 insertions(+), 35 deletions(-) create mode 100644 backend/src/test/review/random-review.spec.ts diff --git a/backend/src/review/repositories/review.repository.ts b/backend/src/review/repositories/review.repository.ts index 1df7fde..e06fd36 100644 --- a/backend/src/review/repositories/review.repository.ts +++ b/backend/src/review/repositories/review.repository.ts @@ -3,6 +3,7 @@ import { ReviewEntity } from '../models/review.entity'; import { Repository } from 'typeorm'; import { FindManyOptions } from 'typeorm/find-options/FindManyOptions'; import { FindOptionsWhere } from 'typeorm/find-options/FindOptionsWhere'; +import { UserEntity } from '../../user/models/user.entity'; @CustomRepository(ReviewEntity) export class ReviewRepository extends Repository { @@ -41,23 +42,29 @@ export class ReviewRepository extends Repository { relations: { images: true, user: true, + restaurant: true, + }, + order: { + id: 'DESC', }, }); } - findOfUserId(username: string) { + findOfUser(user: UserEntity) { return this.findFull({ user: { - username: username, + id: user.id, }, }); } findRandomReviews(limit: number) { - return this.createQueryBuilder() - .select() - .orderBy('RAND()') // for MySQL. Change this according to your database - .take(limit) + return this.createQueryBuilder('review') + .leftJoinAndSelect('review.images', 'image') + .leftJoinAndSelect('review.user', 'user') + .leftJoinAndSelect('review.restaurant', 'restaurant') + .orderBy('RANDOM()') + .limit(limit) .getMany(); } } diff --git a/backend/src/review/review.controller.ts b/backend/src/review/review.controller.ts index 3bbdca1..68702dc 100644 --- a/backend/src/review/review.controller.ts +++ b/backend/src/review/review.controller.ts @@ -57,6 +57,21 @@ export class ReviewController { return new ReviewListDto(reviews); } + @UseGuards(JwtAccessGuard) + @Get('/my') + async getMyReview(@Req() { user }: UserRequest) { + const reviews = await this.reviewRepository.findOfUser(user); + return new ReviewListDto(reviews); + } + + @UseGuards(JwtAccessGuard) + @Get('/random') + async getReviewRandom() { + const limit = 5; // Define the number of random reviews you want to retrieve + const randomReviews = await this.reviewRepository.findRandomReviews(limit); + return new ReviewListDto(randomReviews); + } + @UseGuards(JwtAccessGuard) @Get('/:reviewId') async getReviewDetail( @@ -88,20 +103,4 @@ export class ReviewController { return new ImageUploadDto(image); } - - @UseGuards(JwtAccessGuard) - @Get('/my') - async getMyReview(@Req() { user }: UserRequest) { - console.log(user); - const reviews = user.reviews; - return new ReviewListDto(reviews); - } - - @UseGuards(JwtAccessGuard) - @Get('/random') - async getReviewRandom() { - const limit = 5; // Define the number of random reviews you want to retrieve - const randomReviews = await this.reviewRepository.findRandomReviews(limit); - return new ReviewListDto(randomReviews); - } } diff --git a/backend/src/test/review/create-review.spec.ts b/backend/src/test/review/create-review.spec.ts index 033654b..f7c514b 100644 --- a/backend/src/test/review/create-review.spec.ts +++ b/backend/src/test/review/create-review.spec.ts @@ -12,7 +12,7 @@ import { RestaurantEntity } from '../../review/models/restaurant.entity'; import { RestaurantFixture } from '../fixture/restaurant.fixture'; import { ReviewEntity } from '../../review/models/review.entity'; -describe('Review test', () => { +describe('Create Review test', () => { let testServer: NestExpressApplication; let dataSource: DataSource; let user: UserEntity; diff --git a/backend/src/test/review/get-my-review.spec.ts b/backend/src/test/review/get-my-review.spec.ts index 8883389..39abef4 100644 --- a/backend/src/test/review/get-my-review.spec.ts +++ b/backend/src/test/review/get-my-review.spec.ts @@ -14,7 +14,7 @@ import { ReviewFixture } from '../fixture/review.fixture'; import { ImageFixture } from '../fixture/image.fixture'; import { validateReview, validateReviewList } from './validateReviewList'; -describe('Review test', () => { +describe('My Review test', () => { let testServer: NestExpressApplication; let dataSource: DataSource; let user: UserEntity; @@ -82,10 +82,26 @@ describe('Review test', () => { .set('Authorization', `Bearer ${accessToken}`) .expect(HttpStatus.OK); - validateReview(body); + validateReviewList(body); }); it('리뷰 없으면 빈 배열 준다', async () => { + const newUser = await UserFixture.create({ + name: 'hinew', + username: 'hellonew', + password: 'world', + }); + + const { body: authBody } = await supertest(testServer.getHttpServer()) + .post('/auth/login') + .send({ + username: 'hellonew', + password: 'world', + }) + .expect(HttpStatus.CREATED); + + accessToken = authBody.accessToken; + const { body } = await supertest(testServer.getHttpServer()) .get(`/reviews/my`) .set('Authorization', `Bearer ${accessToken}`) @@ -113,12 +129,9 @@ describe('Review test', () => { .set('Authorization', `Bearer ${accessToken}`) .expect(HttpStatus.OK); - // Assuming the reviews are ordered by the most recent time, verify if the retrieved reviews are in the correct order - const reviewList = body.reviewList; - for (let i = 0; i < reviewList.length - 1; i++) { - const currentReview = new Date(reviewList[i].issuedAt); - const nextReview = new Date(reviewList[i + 1].issuedAt); - expect(currentReview >= nextReview).toBe(true); - } + // Check that the reviews are in time order + expect(body.reviewList[0].id).toBe(review2.id); + expect(body.reviewList[1].id).toBe(review1.id); + expect(body.reviewList[2].id).toBe(review.id); }); }); diff --git a/backend/src/test/review/get-review-detail.spec.ts b/backend/src/test/review/get-review-detail.spec.ts index 7ae007f..6f9ca9a 100644 --- a/backend/src/test/review/get-review-detail.spec.ts +++ b/backend/src/test/review/get-review-detail.spec.ts @@ -14,7 +14,7 @@ import { ReviewFixture } from '../fixture/review.fixture'; import { ImageFixture } from '../fixture/image.fixture'; import { validateReview, validateReviewList } from './validateReviewList'; -describe('Review test', () => { +describe('Get review detail test', () => { let testServer: NestExpressApplication; let dataSource: DataSource; let user: UserEntity; diff --git a/backend/src/test/review/get-review-restaurant.spec.ts b/backend/src/test/review/get-review-restaurant.spec.ts index 678952a..7cfa111 100644 --- a/backend/src/test/review/get-review-restaurant.spec.ts +++ b/backend/src/test/review/get-review-restaurant.spec.ts @@ -14,7 +14,7 @@ import { ReviewFixture } from '../fixture/review.fixture'; import { ImageFixture } from '../fixture/image.fixture'; import { validateReviewList } from './validateReviewList'; -describe('Review test', () => { +describe('get review restaurant test', () => { let testServer: NestExpressApplication; let dataSource: DataSource; let user: UserEntity; diff --git a/backend/src/test/review/random-review.spec.ts b/backend/src/test/review/random-review.spec.ts new file mode 100644 index 0000000..4a9ceaa --- /dev/null +++ b/backend/src/test/review/random-review.spec.ts @@ -0,0 +1,148 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { AppModule } from '../../app.module'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; +import { appSetting } from '../../main'; +import * as supertest from 'supertest'; +import { UserEntity } from '../../user/models/user.entity'; +import { UserFixture } from '../fixture/user.fixture'; +import { HttpStatus } from '@nestjs/common'; +import { RestaurantEntity } from '../../review/models/restaurant.entity'; +import { RestaurantFixture } from '../fixture/restaurant.fixture'; +import { ReviewEntity } from '../../review/models/review.entity'; +import { ReviewFixture } from '../fixture/review.fixture'; +import { ImageFixture } from '../fixture/image.fixture'; +import { validateReview, validateReviewList } from './validateReviewList'; + +describe('Random Review test', () => { + let testServer: NestExpressApplication; + let dataSource: DataSource; + let user: UserEntity; + let accessToken: string; + let restaurant: RestaurantEntity; + let review: ReviewEntity; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + testServer = module.createNestApplication(); + dataSource = testServer.get(DataSource); + await dataSource.synchronize(true); + appSetting(testServer); + + await testServer.init(); + }); + + beforeEach(async () => { + await dataSource.synchronize(true); + + user = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const { body } = await supertest(testServer.getHttpServer()) + .post('/auth/login') + .send({ + username: 'hello', + password: 'world', + }) + .expect(HttpStatus.CREATED); + + accessToken = body.accessToken; + + restaurant = await RestaurantFixture.create({}); + const image = await ImageFixture.create({}); + review = await ReviewFixture.create({ + restaurant, + images: [image], + user, + }); + }); + + it('unauthorized', async () => { + await supertest(testServer.getHttpServer()) + .get(`/reviews/random`) + .expect(HttpStatus.UNAUTHORIZED); + }); + + it('OK', async () => { + await supertest(testServer.getHttpServer()) + .get(`/reviews/random`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + }); + + it('DTO check', async () => { + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/random`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + validateReviewList(body); + }); + + it('리뷰 없으면 빈 배열 준다', async () => { + await dataSource.synchronize(true); + + user = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const { body: authBody } = await supertest(testServer.getHttpServer()) + .post('/auth/login') + .send({ + username: 'hello', + password: 'world', + }) + .expect(HttpStatus.CREATED); + + accessToken = authBody.accessToken; + + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/random`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + expect(body.reviewList).toStrictEqual([]); + }); + + it('내가 쓰지 않은 리뷰도 잘 준다', async () => { + // Create multiple reviews for the user with different timestamps + const review1 = await ReviewFixture.create({ + restaurant, + images: [await ImageFixture.create({})], + user, + }); + const review2 = await ReviewFixture.create({ + restaurant, + images: [await ImageFixture.create({})], + user, + }); + + const anotherUser = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const review3 = await ReviewFixture.create({ + restaurant, + images: [await ImageFixture.create({})], + user: anotherUser, + }); + + // Fetch the user's reviews + const { body } = await supertest(testServer.getHttpServer()) + .get('/reviews/random') + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + expect(body.reviewList.length).toBe(4); + }); +}); diff --git a/backend/src/test/review/upload-image.spec.ts b/backend/src/test/review/upload-image.spec.ts index 6d35fe5..af358df 100644 --- a/backend/src/test/review/upload-image.spec.ts +++ b/backend/src/test/review/upload-image.spec.ts @@ -16,7 +16,7 @@ import { validateReviewList } from './validateReviewList'; import { ImageEntity } from '../../review/models/image.entity'; import { validateDtoKeys } from '../utils'; -describe('Review test', () => { +describe('upload-image test', () => { let testServer: NestExpressApplication; let dataSource: DataSource; let user: UserEntity; From adb3d2e1f45cb95194aa12579d86c26f1cb4717f Mon Sep 17 00:00:00 2001 From: eldpswp99 Date: Tue, 24 Oct 2023 16:44:53 +0900 Subject: [PATCH 3/3] review restaurant adjcent --- backend/package-lock.json | 6 + backend/package.json | 1 + .../dtos/in-dtos/review-adjacent-query.dto.ts | 16 +++ .../dtos/out-dtos/restaurantList.dto.ts | 12 ++ backend/src/review/review.controller.ts | 14 ++ backend/src/review/review.service.ts | 17 +++ .../test/review/adjacent-restaurant.spec.ts | 126 ++++++++++++++++++ backend/src/test/review/validateReviewList.ts | 7 + .../src/user/repostiories/user.repository.ts | 9 -- 9 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 backend/src/review/dtos/in-dtos/review-adjacent-query.dto.ts create mode 100644 backend/src/review/dtos/out-dtos/restaurantList.dto.ts create mode 100644 backend/src/test/review/adjacent-restaurant.spec.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index dc5f39b..f2b28f3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,6 +25,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "dotenv": "^16.3.1", + "geolib": "^3.3.4", "jsonwebtoken": "^9.0.2", "luxon": "^3.4.3", "multer": "^1.4.5-lts.1", @@ -6503,6 +6504,11 @@ "node": ">=6.9.0" } }, + "node_modules/geolib": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.4.tgz", + "integrity": "sha512-EicrlLLL3S42gE9/wde+11uiaYAaeSVDwCUIv2uMIoRBfNJCn8EsSI+6nS3r4TCKDO6+RQNM9ayLq2at+oZQWQ==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", diff --git a/backend/package.json b/backend/package.json index ed5f920..b40168b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -36,6 +36,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "dotenv": "^16.3.1", + "geolib": "^3.3.4", "jsonwebtoken": "^9.0.2", "luxon": "^3.4.3", "multer": "^1.4.5-lts.1", diff --git a/backend/src/review/dtos/in-dtos/review-adjacent-query.dto.ts b/backend/src/review/dtos/in-dtos/review-adjacent-query.dto.ts new file mode 100644 index 0000000..d768ef2 --- /dev/null +++ b/backend/src/review/dtos/in-dtos/review-adjacent-query.dto.ts @@ -0,0 +1,16 @@ +import { IsNumber } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; + +export class ReviewAdjacentQueryDto { + @IsNumber() + @Type(() => Number) + latitude: number; + + @IsNumber() + @Type(() => Number) + longitude: number; + + @IsNumber() + @Type(() => Number) + distance: number; +} diff --git a/backend/src/review/dtos/out-dtos/restaurantList.dto.ts b/backend/src/review/dtos/out-dtos/restaurantList.dto.ts new file mode 100644 index 0000000..ad402c2 --- /dev/null +++ b/backend/src/review/dtos/out-dtos/restaurantList.dto.ts @@ -0,0 +1,12 @@ +import { RestaurantDto } from './restaurant.dto'; +import { RestaurantEntity } from '../../models/restaurant.entity'; + +export class RestaurantListDto { + private restaurantList: RestaurantDto[]; + + constructor(restaurantList: RestaurantEntity[]) { + this.restaurantList = restaurantList.map( + (restaurant) => new RestaurantDto(restaurant), + ); + } +} diff --git a/backend/src/review/review.controller.ts b/backend/src/review/review.controller.ts index 68702dc..3f0fc9c 100644 --- a/backend/src/review/review.controller.ts +++ b/backend/src/review/review.controller.ts @@ -5,6 +5,7 @@ import { NotFoundException, Param, Post, + Query, Req, UploadedFile, UseGuards, @@ -26,6 +27,8 @@ import { ImageUploadDto } from './dtos/out-dtos/imageUpload.dto'; import { ReviewDetailDto } from './dtos/out-dtos/reviewDetail.dto'; import { UserEntity } from '../user/models/user.entity'; import { UserRepository } from '../user/repostiories/user.repository'; +import { ReviewAdjacentQueryDto } from './dtos/in-dtos/review-adjacent-query.dto'; +import { RestaurantListDto } from './dtos/out-dtos/restaurantList.dto'; @ApiTags('reviews') @Controller('reviews') @@ -72,6 +75,17 @@ export class ReviewController { return new ReviewListDto(randomReviews); } + @UseGuards(JwtAccessGuard) + @Get('/adjacent/restaurants') + async getReviewAdjacent( + @Req() { user }: UserRequest, + @Query() data: ReviewAdjacentQueryDto, + ) { + const restaurants = await this.reviewService.getAdjacentRestaurant(data); + + return new RestaurantListDto(restaurants); + } + @UseGuards(JwtAccessGuard) @Get('/:reviewId') async getReviewDetail( diff --git a/backend/src/review/review.service.ts b/backend/src/review/review.service.ts index 14f6a4c..38695c3 100644 --- a/backend/src/review/review.service.ts +++ b/backend/src/review/review.service.ts @@ -5,6 +5,8 @@ import { RestaurantRepository } from './repositories/restaurant.repository'; import { ImageRepository } from './repositories/image.repository'; import { In } from 'typeorm'; import { ReviewEntity } from './models/review.entity'; +import { ReviewAdjacentQueryDto } from './dtos/in-dtos/review-adjacent-query.dto'; +import { getDistance } from 'geolib'; @Injectable() export class ReviewService { @@ -37,4 +39,19 @@ export class ReviewService { images, }).save(); } + + async getAdjacentRestaurant(data: ReviewAdjacentQueryDto) { + const { longitude, latitude, distance } = data; + const restaurants = await this.restaurantRepository.find({}); + const KILOMETER = 1000; + + return restaurants.filter( + (restaurant) => + getDistance( + { latitude, longitude }, + { latitude: restaurant.latitude, longitude: restaurant.longitude }, + ) <= + distance * KILOMETER, + ); + } } diff --git a/backend/src/test/review/adjacent-restaurant.spec.ts b/backend/src/test/review/adjacent-restaurant.spec.ts new file mode 100644 index 0000000..7a44cb8 --- /dev/null +++ b/backend/src/test/review/adjacent-restaurant.spec.ts @@ -0,0 +1,126 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { AppModule } from '../../app.module'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; +import { appSetting } from '../../main'; +import * as supertest from 'supertest'; +import { UserEntity } from '../../user/models/user.entity'; +import { UserFixture } from '../fixture/user.fixture'; +import { HttpStatus } from '@nestjs/common'; +import { RestaurantEntity } from '../../review/models/restaurant.entity'; +import { RestaurantFixture } from '../fixture/restaurant.fixture'; +import { ReviewEntity } from '../../review/models/review.entity'; +import { ReviewFixture } from '../fixture/review.fixture'; +import { ImageFixture } from '../fixture/image.fixture'; +import { + validateRestaurantList, + validateReview, + validateReviewList, +} from './validateReviewList'; + +describe('Get adjacent restaurant test', () => { + let testServer: NestExpressApplication; + let dataSource: DataSource; + let user: UserEntity; + let accessToken: string; + let restaurant: RestaurantEntity; + let review: ReviewEntity; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + testServer = module.createNestApplication(); + dataSource = testServer.get(DataSource); + await dataSource.synchronize(true); + appSetting(testServer); + + await testServer.init(); + }); + + beforeEach(async () => { + await dataSource.synchronize(true); + + user = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const { body } = await supertest(testServer.getHttpServer()) + .post('/auth/login') + .send({ + username: 'hello', + password: 'world', + }) + .expect(HttpStatus.CREATED); + + accessToken = body.accessToken; + + restaurant = await RestaurantFixture.create({}); + const image = await ImageFixture.create({}); + review = await ReviewFixture.create({ + restaurant, + images: [image], + user, + }); + }); + + it('unauthorized', async () => { + await supertest(testServer.getHttpServer()) + .get( + `/reviews/adjacent/restaurants?longitude=127.0&latitude=37.0&distance=1`, + ) + .expect(HttpStatus.UNAUTHORIZED); + }); + + it('OK', async () => { + await supertest(testServer.getHttpServer()) + .get( + `/reviews/adjacent/restaurants?longitude=127.0&latitude=37.0&distance=1`, + ) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + }); + + it('DTO check', async () => { + restaurant.longitude = 127.0; + restaurant.latitude = 37.0; + await restaurant.save(); + + const { body } = await supertest(testServer.getHttpServer()) + .get( + `/reviews/adjacent/restaurants?longitude=127.0&latitude=37.0&distance=1`, + ) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + validateRestaurantList(body); + }); + + // TODO : unittest + it('멀리 떨어지면 안잡한다', async () => { + restaurant.longitude = 127.0; + restaurant.latitude = 37.018018; + await restaurant.save(); + + const { body } = await supertest(testServer.getHttpServer()) + .get( + `/reviews/adjacent/restaurants?longitude=127.0&latitude=37.0&distance=1`, + ) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + expect(body.restaurantList.length).toEqual(0); + + const { body: body2 } = await supertest(testServer.getHttpServer()) + .get( + `/reviews/adjacent/restaurants?longitude=127.0&latitude=37.0&distance=3`, + ) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + expect(body2.restaurantList.length).toEqual(1); + }); +}); diff --git a/backend/src/test/review/validateReviewList.ts b/backend/src/test/review/validateReviewList.ts index ac99cc7..f50482c 100644 --- a/backend/src/test/review/validateReviewList.ts +++ b/backend/src/test/review/validateReviewList.ts @@ -23,6 +23,13 @@ export function validateReview(body: any) { validateImages(body.images); } +export function validateRestaurantList(body: any) { + validateDtoKeys(body, ['restaurantList']); + for (const restaurant of body.restaurantList) { + validateRestaurant(restaurant); + } +} + export function validateRestaurant(body: any) { validateDtoKeys(body, ['id', 'googleMapPlaceId', 'longitude', 'latitude']); } diff --git a/backend/src/user/repostiories/user.repository.ts b/backend/src/user/repostiories/user.repository.ts index a5fa674..d9b3fc7 100644 --- a/backend/src/user/repostiories/user.repository.ts +++ b/backend/src/user/repostiories/user.repository.ts @@ -10,15 +10,6 @@ export class UserRepository extends Repository { return user ? user : undefined; // Return undefined when no user is found } - async validateUserByToken(payload: JwtPayload) { - const { username } = payload; - const user = await this.findByUsername(username); - if (!user) { - return null; - } - return user; - } - async searchUserByUsernameSorted(name: string) { return await this.createQueryBuilder('user') .where('user.name ILIKE :name', { name: `%${name}%` })