Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,46 @@ public class CourseController {
)
@Operation(summary = "추천 코스 등록", description = "추천 코스를 등록합니다.")
@PostMapping
public ApiResponse<CourseResDTO.CourseCreateRes> createCourse(
public ApiResponse<CourseResDTO.CourseIdRes> createCourse(
@Valid @RequestBody CourseReqDTO.CourseCreateReq request
) {
CourseResDTO.CourseCreateRes response = courseService.createCourse(request);
CourseResDTO.CourseIdRes response = courseService.createCourse(request);

return ApiResponse.onSuccess(SuccessCode.CREATED, response);
}

@io.swagger.v3.oas.annotations.parameters.RequestBody(
required = true,
content = @Content(
examples = @ExampleObject(
name = "추천 코스 수정 예시",
value = SwaggerExamples.COURSE_UPDATE
)
)
)
@Operation(summary = "추천 코스 수정", description = "추천 코스를 수정합니다.")
@PatchMapping("/{courseId}")
public ApiResponse<CourseResDTO.CourseCreateRes> updateCourse(
public ApiResponse<CourseResDTO.CourseIdRes> updateCourse(
@Parameter(description = "코스 ID", example = "15")
@PathVariable Long courseId,
@Valid @RequestBody CourseReqDTO.CourseCreateReq request
@Valid @RequestBody CourseReqDTO.CourseUpdateReq request
) {
CourseResDTO.CourseCreateRes response = courseService.updateCourse(courseId, request);
CourseResDTO.CourseIdRes response = courseService.updateCourse(courseId, request);

return ApiResponse.onSuccess(SuccessCode.OK, response);
}

@Operation(summary = "추천 코스 삭제", description = "추천 코스를 삭제합니다.")
@DeleteMapping("/{courseId}")
public ApiResponse<Void> deleteCourse(
@Parameter(description = "코스 ID", example = "15")
@PathVariable Long courseId
) {
courseService.deleteCourse(courseId);

return ApiResponse.onSuccess(SuccessCode.OK);
}

@Operation(summary = "추천 코스 목록 조회", description = "추천 코스 목록을 조회합니다.")
@GetMapping
public ApiResponse<CursorResponse<CourseResDTO.CoursePreview>> getCourses(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public static Course toCourse(CourseReqDTO.CourseCreateReq request, User user, R
.durationType(request.durationType())
.transportType(request.transportType())
.companionType(request.companionType())
.monthStart(request.monthStart())
.monthEnd(request.monthEnd())
.thumbnailKey(request.thumbnailKey())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
Expand Down Expand Up @@ -47,6 +49,18 @@ public record CourseCreateReq(
@Schema(description = "동행 유형", example = "FRIEND")
CompanionType companionType,

@NotNull(message = "여행 시작 월은 필수입니다")
@Min(value = 1, message = "여행 시작 월은 1 이상이어야 합니다")
@Max(value = 12, message = "여행 시작 월은 12 이하이어야 합니다")
@Schema(description = "여행 시작 월", example = "4")
Integer monthStart,

@NotNull(message = "여행 종료 월은 필수입니다")
@Min(value = 1, message = "여행 종료 월은 1 이상이어야 합니다")
@Max(value = 12, message = "여행 종료 월은 12 이하이어야 합니다")
@Schema(description = "여행 종료 월", example = "10")
Integer monthEnd,

@Schema(description = "대표 이미지 S3 key", example = "courses/thumbnail/abcd1234.jpg")
@NotBlank(message = "대표 이미지는 필수입니다")
String thumbnailKey,
Expand All @@ -63,7 +77,50 @@ public record CourseCreateReq(
@Valid
@NotEmpty(message = "코스 구성 항목은 최소 1개 이상 필요합니다.")
@Schema(description = "코스 구성 항목")
List<CourseItemCreateReq> courseItems
List<@NotNull(message = "코스 구성 항목이 올바르지 않습니다.") CourseItemCreateReq> courseItems
) { }

@Schema(name = "CourseUpdateRequest", description = "추천 코스 수정 요청")
public record CourseUpdateReq(

@Schema(description = "코스 제목", example = "부산 야경 여행")
String title,

@Schema(description = "코스 설명", example = "부산의 야경과 축제를 함께 즐길 수 있는 코스입니다.")
String description,

@Schema(description = "여행 기간", example = "DAY_TRIP")
DurationType durationType,

@Schema(description = "이동 수단", example = "CAR")
TransportType transportType,

@Schema(description = "동행 유형", example = "FRIEND")
CompanionType companionType,

@Min(value = 1, message = "여행 시작 월은 1 이상이어야 합니다")
@Max(value = 12, message = "여행 시작 월은 12 이하이어야 합니다")
@Schema(description = "여행 시작 월", example = "5")
Integer monthStart,

@Min(value = 1, message = "여행 종료 월은 1 이상이어야 합니다")
@Max(value = 12, message = "여행 종료 월은 12 이하이어야 합니다")
@Schema(description = "여행 종료 월", example = "9")
Integer monthEnd,

@Schema(description = "대표 이미지 S3 key", example = "courses/thumbnail/abcd1234.jpg")
String thumbnailKey,

@Schema(description = "해시태그 ID 목록", example = "[1, 3, 5]")
@Size(
max = 5,
message = "해시태그는 5개 이하로 선택할 수 있습니다."
)
List<@NotNull(message = "유효하지 않은 해시태그입니다.") Long> hashtagIds,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

등록 요청에서는 해시태그를 최소 1개 이상 선택하도록 검증하고있어서,
수정 dto에도 min = 1 를 적용하는게 좋을것같습니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

해시태그를 전달하지 않으면 해시태그는 수정하지 않는 걸로 하였기 때문에 최소 개수를 검증하지 않고 최대 개수만 검증하도록 했습니다. 확인부탁드립니다!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

넵 확인했습니다 수고하셨습니다!

@Valid
@Schema(description = "코스 구성 항목")
List<@NotNull(message = "코스 구성 항목이 올바르지 않습니다.") CourseItemCreateReq> courseItems
) { }

@Schema(name = "CourseItemCreateRequest", description = "추천 코스 구성 항목 등록 요청")
Expand All @@ -74,12 +131,9 @@ public record CourseItemCreateReq(
Integer order,

@NotNull(message = "코스 항목 타입은 필수입니다")
@Schema(description = "코스 항목 타입. PLACE인 경우 placeId와 imageKey를 사용하고, CONTENT인 경우 contentId를 사용합니다.", example = "PLACE")
@Schema(description = "PLACE인 경우 externalPlaceId, categoryGroupCode, name, roadAddress(또는 lotAddress), latitude, longitude, imageKey를 사용하고, CONTENT인 경우 contentId를 사용합니다.", example = "PLACE")
CourseItemType type,

@Schema(description = "장소 ID. type=PLACE인 경우 사용합니다.", example = "1")
Long placeId,

@Schema(description = "콘텐츠 ID. type=CONTENT인 경우 사용합니다.", example = "10")
Long contentId,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

public class CourseResDTO {

@Schema(name = "CourseCreateResponse", description = "추천 코스 등록 응답")
public record CourseCreateRes(
@Schema(name = "CourseIdResponse", description = "추천 코스 ID 응답")
public record CourseIdRes(

@Schema(description = "코스 ID", example = "15")
Long courseId
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/yeogido/backend/domain/course/entity/Course.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,44 @@ public class Course extends BaseEntity {

@Column(name = "deleted_at")
private LocalDateTime deletedAt;

public void update(
String title,
String description,
DurationType durationType,
TransportType transportType,
CompanionType companionType,
Integer monthStart,
Integer monthEnd,
String thumbnailKey
) {
if (title != null) {
this.title = title;
}
if (description != null) {
this.description = description;
}
if (durationType != null) {
this.durationType = durationType;
}
if (transportType != null) {
this.transportType = transportType;
}
if (companionType != null) {
this.companionType = companionType;
}
if (monthStart != null) {
this.monthStart = monthStart;
}
if (monthEnd != null) {
this.monthEnd = monthEnd;
}
if (thumbnailKey != null) {
this.thumbnailKey = thumbnailKey;
}
}

public void delete() {
this.deletedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

import com.yeogido.backend.domain.course.entity.CourseHashtag;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface CourseHashtagRepository extends JpaRepository<CourseHashtag, Long> {

@Modifying
@Query("delete from CourseHashtag ch where ch.course.id = :courseId")
void deleteAllByCourseId(@Param("courseId") Long courseId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

import com.yeogido.backend.domain.course.entity.CourseItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface CourseItemRepository extends JpaRepository<CourseItem, Long> {

@Modifying
@Query("delete from CourseItem ci where ci.course.id = :courseId")
void deleteAllByCourseId(@Param("courseId") Long courseId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

public interface CourseService {

CourseResDTO.CourseCreateRes createCourse(CourseReqDTO.CourseCreateReq request);
CourseResDTO.CourseIdRes createCourse(CourseReqDTO.CourseCreateReq request);

CourseResDTO.CourseCreateRes updateCourse(Long courseId, CourseReqDTO.CourseCreateReq request);
CourseResDTO.CourseIdRes updateCourse(Long courseId, CourseReqDTO.CourseUpdateReq request);

void deleteCourse(Long courseId);

CursorResponse<CourseResDTO.CoursePreview> getCourses(CourseReqDTO.CourseListReq request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
import com.yeogido.backend.domain.region.entity.Region;
import com.yeogido.backend.domain.region.exception.RegionErrorCode;
import com.yeogido.backend.domain.region.repository.RegionRepository;
import com.yeogido.backend.domain.region.service.RegionService;
import com.yeogido.backend.domain.user.entity.User;
import com.yeogido.backend.domain.user.enums.UserRole;
import com.yeogido.backend.domain.user.repository.UserRepository;
import com.yeogido.backend.global.common.response.CursorResponse;
import com.yeogido.backend.global.exception.GeneralErrorCode;
Expand Down Expand Up @@ -69,7 +69,7 @@ public class CourseServiceImpl implements CourseService {

@Override
@Transactional
public CourseResDTO.CourseCreateRes createCourse(CourseReqDTO.CourseCreateReq request) {
public CourseResDTO.CourseIdRes createCourse(CourseReqDTO.CourseCreateReq request) {
validateCourseCreateRequest(request);

User user = getCurrentUser();
Expand All @@ -79,13 +79,49 @@ public CourseResDTO.CourseCreateRes createCourse(CourseReqDTO.CourseCreateReq re
saveCourseHashtags(course, request.hashtagIds());
saveCourseItems(course, request.courseItems());

return new CourseResDTO.CourseCreateRes(course.getId());
return new CourseResDTO.CourseIdRes(course.getId());
}

@Override
public CourseResDTO.CourseCreateRes updateCourse(Long courseId, CourseReqDTO.CourseCreateReq request) {
// TODO: 추천 코스 수정 로직 구현
return new CourseResDTO.CourseCreateRes(15L);
@Transactional
public CourseResDTO.CourseIdRes updateCourse(Long courseId, CourseReqDTO.CourseUpdateReq request) {
Course course = getActiveCourse(courseId);
User user = getCurrentUser();

validateCourseAuthority(course, user);
validateCourseUpdateRequest(request);

course.update(
request.title(),
request.description(),
request.durationType(),
request.transportType(),
request.companionType(),
request.monthStart(),
request.monthEnd(),
request.thumbnailKey()
);

if (request.hashtagIds() != null) {
replaceCourseHashtags(course, request.hashtagIds());
}

if (request.courseItems() != null) {
replaceCourseItems(course, request.courseItems());
}

return new CourseResDTO.CourseIdRes(course.getId());
}

@Override
@Transactional
public void deleteCourse(Long courseId) {
Course course = getActiveCourse(courseId);
User user = getCurrentUser();

validateCourseAuthority(course, user);

course.delete();
}

@Override
Expand Down Expand Up @@ -226,11 +262,40 @@ private Region getRegion(Long regionId) {
.orElseThrow(() -> new GeneralException(RegionErrorCode.REGION_NOT_FOUND));
}

private void validateCourseAuthority(Course course, User user) {
switch (course.getCourseType()) {
case OFFICIAL -> {
if (user.getRole() != UserRole.ADMIN) {
throw new GeneralException(GeneralErrorCode.FORBIDDEN);
}
}

case LOCAL -> {
if (course.getUser() == null ||
!Objects.equals(course.getUser().getId(), user.getId())) {
throw new GeneralException(GeneralErrorCode.FORBIDDEN);
}
}

default -> throw new GeneralException(GeneralErrorCode.FORBIDDEN);
}
}

private void validateCourseCreateRequest(CourseReqDTO.CourseCreateReq request) {
validateHashtags(request.hashtagIds());
validateCourseItems(request.courseItems());
}

private void validateCourseUpdateRequest(CourseReqDTO.CourseUpdateReq request) {
if (request.hashtagIds() != null) {
validateHashtags(request.hashtagIds());
}

if (request.courseItems() != null) {
validateCourseItems(request.courseItems());
}
}

private void validateHashtags(List<Long> hashtagIds) {
if (new HashSet<>(hashtagIds).size() != hashtagIds.size()) {
throw new GeneralException(CourseErrorCode.DUPLICATE_HASHTAG);
Expand Down Expand Up @@ -279,7 +344,6 @@ private void validatePlaceItem(CourseReqDTO.CourseItemCreateReq item) {

private void validateContentItem(CourseReqDTO.CourseItemCreateReq item) {
if (item.contentId() == null
|| item.placeId() != null
|| StringUtils.hasText(item.externalPlaceId())
|| StringUtils.hasText(item.categoryGroupCode())
|| StringUtils.hasText(item.name())
Expand Down Expand Up @@ -307,6 +371,11 @@ private void saveCourseHashtags(Course course, List<Long> hashtagIds) {
courseHashtagRepository.saveAll(courseHashtags);
}

private void replaceCourseHashtags(Course course, List<Long> hashtagIds) {
courseHashtagRepository.deleteAllByCourseId(course.getId());
saveCourseHashtags(course, hashtagIds);
}

private void saveCourseItems(Course course, List<CourseReqDTO.CourseItemCreateReq> courseItems) {
Map<String, Place> placeMap = placeService.getPlaceMap(courseItems);
Map<Long, Content> contentMap = getContentMap(courseItems);
Expand All @@ -329,6 +398,11 @@ private void saveCourseItems(Course course, List<CourseReqDTO.CourseItemCreateRe
courseItemRepository.saveAll(items);
}

private void replaceCourseItems(Course course, List<CourseReqDTO.CourseItemCreateReq> courseItems) {
courseItemRepository.deleteAllByCourseId(course.getId());
saveCourseItems(course, courseItems);
}

private Map<Long, Content> getContentMap(List<CourseReqDTO.CourseItemCreateReq> courseItems) {
Set<Long> contentIds = courseItems.stream()
.filter(item -> item.type() == CourseItemType.CONTENT)
Expand Down
Loading
Loading