Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

페이징 적용, 장소 카테고리 추가 #74

Merged
merged 7 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ pids

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

**.csv
7 changes: 4 additions & 3 deletions backend/resources/sql/DDL.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ CREATE TABLE PLACE
google_place_id CHAR(50) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
thumbnail_url VARCHAR(255),
rating FLOAT,
rating DECIMAL(3, 2),
longitude DECIMAL(10, 7), -- 경도
latitude DECIMAL(10, 7), -- 위도
formatted_address VARCHAR(255),
category VARCHAR(50),
description TEXT,
detail_page_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
Expand Down Expand Up @@ -64,7 +65,7 @@ CREATE TABLE MAP_PLACE
(
id INT PRIMARY KEY AUTO_INCREMENT,
place_id INT NOT NULL,
map_id INT,
map_id INT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
Expand Down Expand Up @@ -94,7 +95,7 @@ CREATE TABLE COURSE_PLACE
id INT PRIMARY KEY AUTO_INCREMENT,
`order` INT NOT NULL,
place_id INT NOT NULL,
course_id INT,
course_id INT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
Expand Down
16 changes: 8 additions & 8 deletions backend/resources/sql/Mock.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,27 @@ VALUES ('place1', 'Place 1', 'https://example.com/place1.jpg', 4.5, 127.001, 37.
-- MAP 데이터 삽입
INSERT INTO MAP (user_id, thumbnail_url, title, is_public, description)
VALUES (1, 'https://example.com/map1.jpg', 'Map 1', TRUE, 'Description for Map 1'),
(2, 'https://example.com/map2.jpg', 'Map 2', FALSE, 'Description for Map 2'),
(2, 'https://example.com/map2.jpg', 'Map 2', TRUE, 'Description for Map 2'),
(3, 'https://example.com/map3.jpg', 'Map 3', TRUE, 'Description for Map 3'),
(4, 'https://example.com/map4.jpg', 'Map 4', FALSE, 'Description for Map 4'),
(4, 'https://example.com/map4.jpg', 'Map 4', TRUE, 'Description for Map 4'),
(5, 'https://example.com/map5.jpg', 'Map 5', TRUE, 'Description for Map 5'),
(1, 'https://example.com/map6.jpg', 'Map 6', TRUE, 'Description for Map 6'),
(2, 'https://example.com/map7.jpg', 'Map 7', FALSE, 'Description for Map 7'),
(2, 'https://example.com/map7.jpg', 'Map 7', TRUE, 'Description for Map 7'),
(3, 'https://example.com/map8.jpg', 'Map 8', TRUE, 'Description for Map 8'),
(4, 'https://example.com/map9.jpg', 'Map 9', FALSE, 'Description for Map 9'),
(4, 'https://example.com/map9.jpg', 'Map 9', TRUE, 'Description for Map 9'),
(5, 'https://example.com/map10.jpg', 'Map 10', TRUE, 'Description for Map 10');

-- COURSE 데이터 삽입
INSERT INTO COURSE (user_id, thumbnail_url, title, is_public, description)
VALUES (1, 'https://example.com/course1.jpg', 'Course 1', TRUE, 'Description for Course 1'),
(2, 'https://example.com/course2.jpg', 'Course 2', FALSE, 'Description for Course 2'),
(2, 'https://example.com/course2.jpg', 'Course 2', TRUE, 'Description for Course 2'),
(3, 'https://example.com/course3.jpg', 'Course 3', TRUE, 'Description for Course 3'),
(4, 'https://example.com/course4.jpg', 'Course 4', FALSE, 'Description for Course 4'),
(4, 'https://example.com/course4.jpg', 'Course 4', TRUE, 'Description for Course 4'),
(5, 'https://example.com/course5.jpg', 'Course 5', TRUE, 'Description for Course 5'),
(1, 'https://example.com/course6.jpg', 'Course 6', TRUE, 'Description for Course 6'),
(2, 'https://example.com/course7.jpg', 'Course 7', FALSE, 'Description for Course 7'),
(2, 'https://example.com/course7.jpg', 'Course 7', TRUE, 'Description for Course 7'),
(3, 'https://example.com/course8.jpg', 'Course 8', TRUE, 'Description for Course 8'),
(4, 'https://example.com/course9.jpg', 'Course 9', FALSE, 'Description for Course 9'),
(4, 'https://example.com/course9.jpg', 'Course 9', TRUE, 'Description for Course 9'),
(5, 'https://example.com/course10.jpg', 'Course 10', TRUE, 'Description for Course 10');

-- MAP_PLACE 데이터 삽입
Expand Down
11 changes: 9 additions & 2 deletions backend/src/course/course.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ export class CourseController {
constructor(private readonly courseService: CourseService) {}

@Get()
async getCourseList(@Query('query') query?: string) {
return await this.courseService.searchCourse(query);
async getCourseList(
@Query('query') query?: string,
@Query('page') page?: number,
@Query('limit') limit?: number,
) {
if (isNaN(page)) page = 1; // Todo. number 타입 선택적 매개변수일 때 NaN 으로 처리되어 추가. 다른 방법?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: 아래와 같은 방법으로는 안 걸러질까요?

const pageNumber = page ?? 1;
const limitNumber = limit ?? 10;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? 키워드가 undefined, null 을 처리해주는데,
number타입인 페이지와 리미트 인자가 들어오지 않았을 때 NaN 으로 매핑되더라구요..

저도 이 부분 마음에 들지 않아 Todo 로 남겨두었습니다 ㅎㅎ;;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2: page를 적어두지 않았으면 보통 1페이지일테니 page: number = 1로 하는건 어떨가요?

Copy link
Collaborator Author

@Miensoap Miensoap Nov 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재밌는걸 찾았는데, 따끈따끈한 이슈인가봐요!
ValidationPipe 의 transform 옵션이 문제를 일으키고 있다고 해요.

nestjs/nest#12864 ( 기본값 두었을 때 문제 )
nestjs/nest#10246

11.0.0 버전에 수정될 예정이라고 합니다 ㅎㅎ
nestjs/nest#12893

이건 개발 일지에 남겨도 좋겠네요

if (isNaN(limit)) limit = 10;

return await this.courseService.searchCourse(query, page, limit);
}

@Get('/my')
Expand Down
1 change: 1 addition & 0 deletions backend/src/course/course.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class CourseRepository extends SoftDeleteRepository<Course, number> {

findAll(page: number, pageSize: number) {
return this.find({
where: { isPublic: true },
skip: (page - 1) * pageSize,
take: pageSize,
});
Expand Down
6 changes: 3 additions & 3 deletions backend/src/course/dto/CourseDetailResponse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UserIconResponse } from '../../user/dto/UserIconResponse';
import { PlaceResponse } from '../../place/dto/PlaceResponse';
import { PlaceListResponse } from '../../place/dto/PlaceListResponse';
import { DEFAULT_THUMBNAIL_URL } from './CourseListResponse';
import { Course } from '../entity/course.entity';

Expand All @@ -12,7 +12,7 @@ export class CourseDetailResponse {
readonly thumbnailUrl: string,
readonly description: string,
readonly pinCount: number,
readonly places: PlaceResponse[],
readonly places: PlaceListResponse[],
readonly createdAt: Date,
readonly updatedAt: Date,
) {}
Expand All @@ -38,7 +38,7 @@ export class CourseDetailResponse {
export async function getPlacesResponseOfCourseWithOrder(course: Course) {
return (await course.getPlacesWithComment()).map((place, index) => {
return {
...PlaceResponse.from(place.place),
...PlaceListResponse.from(place.place),
comment: place.comment,
order: index + 1,
};
Expand Down
6 changes: 3 additions & 3 deletions backend/src/map/dto/MapDetailResponse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Map } from '../entity/map.entity';
import { UserIconResponse } from '../../user/dto/UserIconResponse';
import { PlaceResponse } from '../../place/dto/PlaceResponse';
import { PlaceListResponse } from '../../place/dto/PlaceListResponse';
import { DEFAULT_THUMBNAIL_URL } from './MapListResponse';

export class MapDetailResponse {
Expand All @@ -12,15 +12,15 @@ export class MapDetailResponse {
readonly thumbnailUrl: string,
readonly description: string,
readonly pinCount: number,
readonly places: PlaceResponse[],
readonly places: PlaceListResponse[],
readonly createdAt: Date,
readonly updatedAt: Date,
) {}

static async from(map: Map) {
const places = (await map.getPlacesWithComment()).map((place) => {
return {
...PlaceResponse.from(place.place),
...PlaceListResponse.from(place.place),
comment: place.comment,
};
});
Expand Down
11 changes: 9 additions & 2 deletions backend/src/map/map.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ export class MapController {
constructor(private readonly mapService: MapService) {}

@Get()
async getMapList(@Query('query') query?: string) {
return await this.mapService.searchMap(query);
async getMapList(
@Query('query') query?: string,
@Query('page') page?: number,
@Query('limit') limit?: number,
) {
if (isNaN(page)) page = 1; // Todo. number 타입 선택적 매개변수일 때 NaN 으로 처리되어 추가. 다른 방법?
if (isNaN(limit)) limit = 10;

return await this.mapService.searchMap(query, page, limit);
}

@Get('/my')
Expand Down
1 change: 1 addition & 0 deletions backend/src/map/map.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class MapRepository extends SoftDeleteRepository<Map, number> {

findAll(page: number, pageSize: number) {
return this.find({
where: { isPublic: true },
skip: (page - 1) * pageSize,
take: pageSize,
});
Expand Down
12 changes: 10 additions & 2 deletions backend/src/place/dto/CreatePlaceRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {
IsNumber,
IsString,
ValidateNested,
Min,
Max,
IsUrl,
} from 'class-validator';
import { Place } from '../entity/place.entity';

Expand All @@ -15,10 +18,12 @@ export class CreatePlaceRequest {
@IsNotEmpty()
name: string;

@IsString()
@IsUrl()
thumbnailUrl?: string;

@IsNumber()
@Min(0)
@Max(5)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2.5: 별점같은 경우는 DB에서 5점만점이라도 소수점을 지원할 경우, 0부터 10의 정수로 저장하는 경우를 봤었는데요! gpt는 정수형 저장이 평균계산 등에서 성능적으로 좋다고는 하는데, 확인해보셔도 좋을 것 같네요 😀

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 필드는 구글에서 가져온 4.33 형태의 값인 평점입니다!

말씀하신 부분이 성능상의 유의미한 차이를 만들게 된다면,
우리 서비스에서 평점의 정확도가 가지는 가치와 비교해 보고 결정하면 될 것 같아요!

정확도가 중요하지 않다고 판단되었을 때, 4.33 이면 9로 저장한다던지?
저는 개인적으로 그럴거면 저장하지 않는 것이 나을 수도 있다고 생각합니다!

아마 말씀해주신 부분은 개인 리뷰에 달린 4 , 4.5 형태의 값인 별점에 대한 이야기 아닐까요? ㅎㅎ

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 ! 제가 착각했네요 하하
저의 경우는 0.5 단위로 리뷰를 제공하는 케이스에만 적용됩니다.
소수점 둘째자리까지 표기해야한다면 정확도가 더 중요할 것 같아요.

rating?: number;

@ValidateNested()
Expand All @@ -32,9 +37,12 @@ export class CreatePlaceRequest {
formattedAddress?: string;

@IsString()
description?: string;
category?: string;

@IsString()
description?: string;

@IsUrl()
detailPageUrl?: string;

toEntity() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Place } from '../entity/place.entity';

export class PlaceResponse {
export class PlaceListResponse {
constructor(
readonly id: number,
readonly name: string,
Expand All @@ -9,14 +9,16 @@ export class PlaceResponse {
readonly lng: number;
},
readonly google_place_id: string,
readonly category?: string,
readonly description?: string,
readonly detail_page_url?: string,
readonly thumbnail_url?: string,
readonly rating?: number,
readonly formed_address?: string,
) {}

static from(place: Place): PlaceResponse {
return new PlaceResponse(
static from(place: Place): PlaceListResponse {
return new PlaceListResponse(
place.id,
place.name,
{
Expand All @@ -26,6 +28,8 @@ export class PlaceResponse {
place.googlePlaceId,
place.detailPageUrl,
place.thumbnailUrl,
place.category,
place.description,
place.rating,
place.formattedAddress,
);
Expand Down
37 changes: 37 additions & 0 deletions backend/src/place/dto/PlaceSearchResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Place } from '../entity/place.entity';

export class PlaceSearchResponse {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: PlaceListResponse 와 내용이 같은 것 같은데 구분한 이유가 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경 전에 장소나 코스의 리스트로 담아 보내는
PlaceListResponse 가 임시로 PlaceResponse 이름을 쓰고 있었고,

검색 API 에서는이 DTO 대신 엔티티 자체가 응답되고 있었어요!

장소 특성상 문제가 생기지 않았지만,
지도나 코스의 경우
즉시 로딩하는 지도_장소 와 순환 참조 문제가 생겨
jsonignore 같은 별도의 설정을 해 주어야 해요.

서로 다른 API 에서 같은 DTO 를 사용하다 보면,
한 쪽에만 변경이 있을 때에 유연하게 대처할 수 없다고 생각해,
응답 DTO를 용도별로 분리하는 편입니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검색 API 에서는이 DTO 대신 엔티티 자체가 응답되고 있었어요!

헉 그러네요 제 실수..

서로 다른 API 에서 같은 DTO 를 사용하다 보면,
한 쪽에만 변경이 있을 때에 유연하게 대처할 수 없다고 생각해,
응답 DTO를 용도별로 분리하는 편입니다!

좋습니다!

constructor(
readonly id: number,
readonly name: string,
readonly location: {
readonly lat: number;
readonly lng: number;
},
readonly google_place_id: string,
readonly category?: string,
readonly description?: string,
readonly detail_page_url?: string,
readonly thumbnail_url?: string,
readonly rating?: number,
readonly formed_address?: string,
) {}

static from(place: Place): PlaceSearchResponse {
return new PlaceSearchResponse(
place.id,
place.name,
{
lat: place.latitude,
lng: place.longitude,
},
place.googlePlaceId,
place.detailPageUrl,
place.thumbnailUrl,
place.category,
place.description,
place.rating,
place.formattedAddress,
);
}
}
3 changes: 3 additions & 0 deletions backend/src/place/entity/place.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export class Place extends BaseEntity {
@Column({ nullable: true })
formattedAddress?: string;

@Column({ nullable: true })
category?: string;

@Column('text', { nullable: true })
description?: string;

Expand Down
11 changes: 9 additions & 2 deletions backend/src/place/place.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@ export class PlaceController {
}

@Get()
async getPlaces(@Query('query') query?: string) {
return this.placeService.getPlaces(query);
async getPlaces(
@Query('query') query?: string,
@Query('page') page?: number,
@Query('limit') limit?: number,
) {
if (isNaN(page)) page = 1; // Todo. number 타입 선택적 매개변수일 때 NaN 으로 처리되어 추가. 다른 방법?
if (isNaN(limit)) limit = 5;

return this.placeService.getPlaces(query, page, limit);
}

@Get('/:id')
Expand Down
5 changes: 3 additions & 2 deletions backend/src/place/place.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PlaceRepository } from './place.repository';
import { CreatePlaceRequest } from './dto/CreatePlaceRequest';
import { PlaceNotFoundException } from './exception/PlaceNotFoundException';
import { PlaceAlreadyExistsException } from './exception/PlaceAlreadyExistsException';
import { PlaceSearchResponse } from './dto/PlaceSearchResponse';

@Injectable()
export class PlaceService {
Expand Down Expand Up @@ -31,14 +32,14 @@ export class PlaceService {
if (!result.length) {
throw new PlaceNotFoundException();
}
return result;
return result.map(PlaceSearchResponse.from);
}

async getPlace(id: number) {
const place = await this.placeRepository.findById(id);
if (!place) {
throw new PlaceNotFoundException(id);
}
return place;
return PlaceSearchResponse.from(place);
}
}
Loading