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

지도/코스에 핀 추가 API 구현 #67

Merged
merged 17 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
4 changes: 2 additions & 2 deletions backend/resources/sql/DDL.sql
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ CREATE TABLE MAP_PLACE
(
id INT PRIMARY KEY AUTO_INCREMENT,
place_id INT NOT NULL,
map_id INT NOT NULL,
map_id INT,
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: 나중에 바뀌는거죠?

Copy link
Collaborator Author

@Miensoap Miensoap Nov 7, 2024

Choose a reason for hiding this comment

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

NOT NULL 로 바뀌냐는 질문인가요?

아니오! ORM 으로 관계 해제했을 때 null 로 업데이트 되는 과정에서
NOT NULL 제약조건으로 오류가 발생해 삭제했습니다.

orphanremoval 옵션을 사용해서 참조가 null 이 되었을 경우 레코드를 삭제하도록
어플리케이션 레벨에서 제어할 것 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@koomchang 지금보니 이거 다시 추가해도 될 것 같아요!
update set null 쿼리가 안나가게 할 수 있겠네요.

제가 cascade , orphanedRowAction, onDelete 관련 정리해서 개발 일지에 공유할게요!

description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
Expand Down Expand Up @@ -94,7 +94,7 @@ CREATE TABLE COURSE_PLACE
id INT PRIMARY KEY AUTO_INCREMENT,
`order` INT NOT NULL,
place_id INT NOT NULL,
course_id INT NOT NULL,
course_id INT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
Expand Down
89 changes: 89 additions & 0 deletions backend/resources/sql/Mock.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
-- USER 데이터 삽입
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: csv 만들어둬도 괜찮을 듯합니다! 나중에 몇 백만건 테스트하려면!

INSERT INTO USER (provider, nickname, oauth_id, role, profile_image_url)
VALUES ('google', 'user1', 'oauth1', 'USER', 'https://example.com/profile1.jpg'),
('google', 'user2', 'oauth2', 'USER', 'https://example.com/profile2.jpg'),
('google', 'user3', 'oauth3', 'USER', 'https://example.com/profile3.jpg'),
('google', 'user4', 'oauth4', 'USER', 'https://example.com/profile4.jpg'),
('google', 'user5', 'oauth5', 'USER', 'https://example.com/profile5.jpg');

-- PLACE 데이터 삽입
INSERT INTO PLACE (google_place_id, name, thumbnail_url, rating, longitude, latitude, formatted_address, description,
detail_page_url)
VALUES ('place1', 'Place 1', 'https://example.com/place1.jpg', 4.5, 127.001, 37.501, 'Seoul, South Korea',
'Beautiful place 1', 'https://example.com/detail1'),
('place2', 'Place 2', 'https://example.com/place2.jpg', 4.0, 127.002, 37.502, 'Seoul, South Korea',
'Beautiful place 2', 'https://example.com/detail2'),
('place3', 'Place 3', 'https://example.com/place3.jpg', 4.2, 127.003, 37.503, 'Seoul, South Korea',
'Beautiful place 3', 'https://example.com/detail3'),
('place4', 'Place 4', 'https://example.com/place4.jpg', 4.1, 127.004, 37.504, 'Seoul, South Korea',
'Beautiful place 4', 'https://example.com/detail4'),
('place5', 'Place 5', 'https://example.com/place5.jpg', 3.9, 127.005, 37.505, 'Seoul, South Korea',
'Beautiful place 5', 'https://example.com/detail5'),
('place6', 'Place 6', 'https://example.com/place6.jpg', 4.3, 127.006, 37.506, 'Seoul, South Korea',
'Beautiful place 6', 'https://example.com/detail6'),
('place7', 'Place 7', 'https://example.com/place7.jpg', 4.4, 127.007, 37.507, 'Seoul, South Korea',
'Beautiful place 7', 'https://example.com/detail7'),
('place8', 'Place 8', 'https://example.com/place8.jpg', 4.6, 127.008, 37.508, 'Seoul, South Korea',
'Beautiful place 8', 'https://example.com/detail8'),
('place9', 'Place 9', 'https://example.com/place9.jpg', 4.7, 127.009, 37.509, 'Seoul, South Korea',
'Beautiful place 9', 'https://example.com/detail9'),
('place10', 'Place 10', 'https://example.com/place10.jpg', 4.8, 127.010, 37.510, 'Seoul, South Korea',
'Beautiful place 10', 'https://example.com/detail10');

-- 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'),
(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'),
(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'),
(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'),
(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'),
(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'),
(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'),
(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'),
(5, 'https://example.com/course10.jpg', 'Course 10', TRUE, 'Description for Course 10');

-- MAP_PLACE 데이터 삽입
INSERT INTO MAP_PLACE (place_id, map_id, description)
VALUES (1, 1, 'Place 1 in Map 1'),
(2, 2, 'Place 2 in Map 2'),
(3, 3, 'Place 3 in Map 3'),
(4, 4, 'Place 4 in Map 4'),
(5, 5, 'Place 5 in Map 5'),
(6, 6, 'Place 6 in Map 6'),
(7, 7, 'Place 7 in Map 7'),
(8, 8, 'Place 8 in Map 8'),
(9, 9, 'Place 9 in Map 9'),
(10, 10, 'Place 10 in Map 10'),
(1, 2, 'Place 1 in Map 2'),
(2, 3, 'Place 2 in Map 3'),
(3, 4, 'Place 3 in Map 4');

-- COURSE_PLACE 데이터 삽입
INSERT INTO COURSE_PLACE (`order`, place_id, course_id, description)
VALUES (1, 1, 1, 'Place 1 in Course 1'),
(2, 2, 2, 'Place 2 in Course 2'),
(3, 3, 3, 'Place 3 in Course 3'),
(4, 4, 4, 'Place 4 in Course 4'),
(5, 5, 5, 'Place 5 in Course 5'),
(6, 6, 6, 'Place 6 in Course 6'),
(7, 7, 7, 'Place 7 in Course 7'),
(8, 8, 8, 'Place 8 in Course 8'),
(9, 9, 9, 'Place 9 in Course 9'),
(10, 10, 10, 'Place 10 in Course 10'),
(2, 1, 3, 'Place 1 in Course 3'),
(3, 2, 4, 'Place 2 in Course 4'),
(4, 3, 5, 'Place 3 in Course 5');
19 changes: 16 additions & 3 deletions backend/src/course/course.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
Delete,
Param,
Patch,
Put,
} from '@nestjs/common';
import { CreateCourseRequest } from './dto/CreateCourseRequest';
import { UpdateCourseInfoRequest } from './dto/UpdateCourseInfoRequest';
import { CourseService } from './course.service';
import { SetPlacesOfCourseRequest } from './dto/AddPlaceToCourseRequest';

@Controller('/courses')
export class CourseController {
Expand Down Expand Up @@ -38,9 +40,15 @@ export class CourseController {
return await this.courseService.createCourse(userId, createCourseRequest);
}

@Delete('/:id')
async deleteCourse(@Param('id') id: number) {
return await this.courseService.deleteCourse(id);
@Put('/:id/places')
async SetPlacesOfCourse(
Copy link
Collaborator

Choose a reason for hiding this comment

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

p1: 네이밍 수정바랍니다!

Suggested change
async SetPlacesOfCourse(
async setPlacesOfCourse(

@Param('id') id: number,
@Body() setPlacesOfCourseRequest: SetPlacesOfCourseRequest,
) {
return await this.courseService.setPlacesOfCourse(
id,
setPlacesOfCourseRequest,
);
}

@Patch('/:id/info')
Expand All @@ -60,4 +68,9 @@ export class CourseController {
await this.courseService.updateCourseVisibility(id, isPublic);
return { id, isPublic };
}

@Delete('/:id')
async deleteCourse(@Param('id') id: number) {
return await this.courseService.deleteCourse(id);
}
}
3 changes: 2 additions & 1 deletion backend/src/course/course.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { CourseService } from './course.service';
import { CourseController } from './course.controller';
import { UserModule } from '../user/user.module';
import { CourseRepository } from './course.repository';
import { PlaceModule } from '../place/place.module';

@Module({
imports: [UserModule],
imports: [UserModule, PlaceModule],
controllers: [CourseController],
providers: [CourseService, CourseRepository],
})
Expand Down
52 changes: 46 additions & 6 deletions backend/src/course/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ import { Injectable } from '@nestjs/common';
import { CourseRepository } from './course.repository';
import { User } from '../user/entity/user.entity';
import { CreateCourseRequest } from './dto/CreateCourseRequest';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CourseListResponse } from './dto/CourseListResponse';
import { CourseDetailResponse } from './dto/CourseDetailResponse';
import {
CourseDetailResponse,
getPlacesResponseOfCourseWithOrder,
} from './dto/CourseDetailResponse';
import { CourseNotFoundException } from './exception/CourseNotFoundException';
import { UpdateCourseInfoRequest } from './dto/UpdateCourseInfoRequest';
import { SetPlacesOfCourseRequest } from './dto/AddPlaceToCourseRequest';
import { PlaceRepository } from '../place/place.repository';
import { UserRepository } from '../user/user.repository';
import { InvalidPlaceToCourseException } from './exception/InvalidPlaceToCourseException';

@Injectable()
export class CourseService {
constructor(
private readonly courseRepository: CourseRepository,
@InjectRepository(User) private readonly userRepository: Repository<User>,
private readonly placeRepository: PlaceRepository,
private readonly userRepository: UserRepository,
) {
// Todo. 로그인 기능 완성 후 제거
const testUser = new User('test', 'test', 'test', 'test');
Expand All @@ -34,7 +40,7 @@ export class CourseService {
const publicMaps = maps.filter((map) => map.isPublic);

return {
maps: await Promise.all(publicMaps.map(CourseListResponse.from)),
courses: await Promise.all(publicMaps.map(CourseListResponse.from)),
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: 제가 지식이 부족해서.. from 은 메소드인데 () 없이 쓸 수 있는게 정적 메서드라서 그런건가요?

Copy link
Collaborator Author

@Miensoap Miensoap Nov 7, 2024

Choose a reason for hiding this comment

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

사실 이거 저도
java 에서 CourseListResponse::from 으로 메서드 참조가 되는 거 생각하고
해보니까 이것도 되네? 하고 알게된건데요,

단일 인자고, static 메서드면 되는 것 같습니다

totalPages: Math.ceil(totalCount / pageSize),
currentPage: page,
};
Expand All @@ -53,7 +59,7 @@ export class CourseService {
);

return {
maps: await Promise.all(ownMaps.map(CourseListResponse.from)),
courses: await Promise.all(ownMaps.map(CourseListResponse.from)),
totalPages: Math.ceil(totalCount / pageSize),
currentPage: page,
};
Expand Down Expand Up @@ -97,4 +103,38 @@ export class CourseService {
if (!(await this.courseRepository.existById(id)))
throw new CourseNotFoundException(id);
}

async setPlacesOfCourse(
id: number,
setPlacesOfCourseRequest: SetPlacesOfCourseRequest,
) {
const course = await this.courseRepository.findById(id);
if (!course) throw new CourseNotFoundException(id);

await this.checkPlacesExist(
setPlacesOfCourseRequest.places.map((p) => p.placeId),
);
Comment on lines +114 to +116
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: 반환값은 어디서 쓰이나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

어떤 함수의 반환값인가요?
checkPlacesExist() 함수는 조건부로 예외를 던지고, 값을 반환하지 않습니다.


course.setPlaces(setPlacesOfCourseRequest.places);
await this.courseRepository.save(course); // Todo. Q.바로 장소 조회하면 장소 정보가 없음.. (장소 참조만 객체에 저장했기 때문)
const reloadedCourse = await this.courseRepository.findById(course.id);

return {
places: await getPlacesResponseOfCourseWithOrder(reloadedCourse),
};
}
Comment on lines +119 to +125
Copy link
Collaborator

Choose a reason for hiding this comment

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

q: save 하고 다시 findById 하는거에 대해서 설명 부탁드립니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

place 를 실제로 조회해서 넣은 것이 아니라, id 만 존재하는지 확인하고
{id : placeId} as place 형태로 참조만 추가해 COURSE_PLACE 테이블에 추가하도록 구현했는데요,

그 이후 place의 온전한 정보를 응답하려니, 위의 참조로 충분하지 않고,
실제 조회가 이루어져야 하더라구요.

이후에
place 상세정보 까진 응답하지 않는다 vs 미리 조회해서 응답한다
결정해서 수정할 것 같습니다!


private async checkPlacesExist(placeIds: number[]) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: validateIfPlacesExist 는 어떤가요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아래의 것과 비슷한 리뷰 같은데요,

validatePlacesExist 로 충분하지 않을까요?

const notExistsPlaceIds = await Promise.all(
placeIds.map(async (placeId) => {
const exists = await this.placeRepository.existById(placeId);
return exists ? null : placeId;
}),
);

const invalidIds = notExistsPlaceIds.filter((placeId) => placeId !== null);
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: invalidPlaceIds는 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

사실 그렇게 작성했다가,
길어져서 줄바꿈되는게 더 안좋아보여 수정했습니다 ㅎㅎ;;
어떻게 생각하시나요?

if (invalidIds.length > 0) {
throw new InvalidPlaceToCourseException(invalidIds);
}
}
}
27 changes: 27 additions & 0 deletions backend/src/course/dto/AddPlaceToCourseRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
IsNumber,
IsNotEmpty,
IsString,
IsArray,
ValidateNested,
Validate,
} from 'class-validator';
import { Type } from 'class-transformer';
import { IsNotConsecutiveDuplicatePlace } from '../pipes/IsNotConsecutiveDuplicatePlace';

export class SetPlacesOfCourseRequestItem {
@IsNumber()
@IsNotEmpty()
placeId: number;

@IsString()
comment?: string;
}

export class SetPlacesOfCourseRequest {
@IsArray()
@ValidateNested({ each: true })
@Type(() => SetPlacesOfCourseRequestItem)
@Validate(IsNotConsecutiveDuplicatePlace)
places: SetPlacesOfCourseRequestItem[];
}
18 changes: 11 additions & 7 deletions backend/src/course/dto/CourseDetailResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ export class CourseDetailResponse {
) {}

static async from(course: Course) {
const places = (await course.getPlacesWithComment()).map((place, index) => {
return {
...PlaceResponse.from(place.place),
comment: place.comment,
order: index + 1,
};
});
const places = await getPlacesResponseOfCourseWithOrder(course);

return new CourseDetailResponse(
course.id,
Expand All @@ -40,3 +34,13 @@ export class CourseDetailResponse {
);
}
}

export async function getPlacesResponseOfCourseWithOrder(course: Course) {
return (await course.getPlacesWithComment()).map((place, index) => {
return {
...PlaceResponse.from(place.place),
comment: place.comment,
order: index + 1,
};
});
}
21 changes: 19 additions & 2 deletions backend/src/course/entity/course-place.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,27 @@ export class CoursePlace extends BaseEntity {
@JoinColumn({ name: 'place_id' })
place: Promise<Place>;

@ManyToOne(() => Course, { onDelete: 'CASCADE' })
@ManyToOne(() => Course, {
onDelete: 'CASCADE',
orphanedRowAction: 'delete',
})
@JoinColumn({ name: 'course_id' })
course: Promise<Course>;
course: Course;

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

static of(
order: number,
placeId: number,
course: Course,
description?: string,
) {
const place = new CoursePlace();
place.course = course;
place.order = order;
place.place = Promise.resolve({ id: placeId } as Place);
place.description = description;
return place;
}
}
11 changes: 9 additions & 2 deletions backend/src/course/entity/course.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Entity, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
import { BaseEntity } from '../../common/BaseEntity';
import { User } from '../../user/entity/user.entity';
import { CoursePlace } from './course-place.entity';
import { SetPlacesOfCourseRequestItem } from '../dto/AddPlaceToCourseRequest';

@Entity()
export class Course extends BaseEntity {
Expand All @@ -23,8 +24,9 @@ export class Course extends BaseEntity {

@OneToMany(() => CoursePlace, (coursePlace) => coursePlace.course, {
eager: true,
cascade: true,
})
private coursePlaces: CoursePlace[];
coursePlaces: CoursePlace[];

constructor(
user: User,
Expand All @@ -45,9 +47,14 @@ export class Course extends BaseEntity {
return this.coursePlaces.length;
}

setPlaces(coursePlaces: SetPlacesOfCourseRequestItem[]) {
this.coursePlaces = coursePlaces.map((item, index) => {
return CoursePlace.of(index + 1, item.placeId, this, item.comment);
});
}

async getPlacesWithComment() {
const coursePlaces = this.coursePlaces.sort((a, b) => a.order - b.order);

return await Promise.all(
coursePlaces.map(async (coursePlace) => ({
place: await coursePlace.place,
Expand Down
12 changes: 12 additions & 0 deletions backend/src/course/exception/ConsecutivePlaceException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BaseException } from '../../common/exception/BaseException';
import { HttpStatus } from '@nestjs/common';

export class ConsecutivePlaceException extends BaseException {
constructor() {
super({
code: 904,
message: '동일한 장소는 연속된 순서로 추가할 수 없습니다.',
status: HttpStatus.BAD_REQUEST,
});
}
}
2 changes: 1 addition & 1 deletion backend/src/course/exception/CourseNotFoundException.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { HttpStatus } from '@nestjs/common';
export class CourseNotFoundException extends BaseException {
constructor(id: number) {
super({
code: 802,
code: 902,
message: `id:${id} 코스가 존재하지 않거나 삭제되었습니다.`,
status: HttpStatus.NOT_FOUND,
});
Expand Down
Loading
Loading