From 3b3e62d61264c8b50d03e4bf9ad1e85ec1a2fe65 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 05:35:13 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[#8]=20feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sight/controller/SightController.java | 22 +++++++++++ .../sight/dto/response/CurationList.java | 11 ++++++ .../sight/dto/response/CurationResponse.java | 18 +++++++++ .../com/earseo/sight/entity/Curation.java | 28 ++++++++++++++ .../earseo/sight/entity/CurationSight.java | 26 +++++++++++++ .../sight/repository/CurationRepository.java | 16 ++++++++ .../repository/CurationSightRepository.java | 7 ++++ .../earseo/sight/service/CurationService.java | 38 +++++++++++++++++++ 8 files changed, 166 insertions(+) create mode 100644 src/main/java/com/earseo/sight/dto/response/CurationList.java create mode 100644 src/main/java/com/earseo/sight/dto/response/CurationResponse.java create mode 100644 src/main/java/com/earseo/sight/entity/Curation.java create mode 100644 src/main/java/com/earseo/sight/entity/CurationSight.java create mode 100644 src/main/java/com/earseo/sight/repository/CurationRepository.java create mode 100644 src/main/java/com/earseo/sight/repository/CurationSightRepository.java create mode 100644 src/main/java/com/earseo/sight/service/CurationService.java diff --git a/src/main/java/com/earseo/sight/controller/SightController.java b/src/main/java/com/earseo/sight/controller/SightController.java index 6cb709b..aaad422 100644 --- a/src/main/java/com/earseo/sight/controller/SightController.java +++ b/src/main/java/com/earseo/sight/controller/SightController.java @@ -1,9 +1,11 @@ package com.earseo.sight.controller; import com.earseo.sight.common.BaseResponse; +import com.earseo.sight.dto.response.CurationList; import com.earseo.sight.dto.response.DocentResponse; import com.earseo.sight.dto.response.SightDetailInfoResponse; import com.earseo.sight.dto.response.SightMapInfoList; +import com.earseo.sight.service.CurationService; import com.earseo.sight.service.SightService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -29,6 +31,7 @@ public class SightController { private final SightService sightService; + private final CurationService curationService; @Operation( summary = "지도 사각형 영역 내 관광지 조회", @@ -354,4 +357,23 @@ public ResponseEntity> getDocent( ) { return ResponseEntity.ok(BaseResponse.ok(sightService.getDocent(sightId))); } + + @Operation( + summary = "큐레이션 목록 조회", + description = "큐레이션 목록을 조회합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "큐레이션 목록 조회 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = CurationList.class) + ) + ) + }) + @GetMapping("/curation") + public ResponseEntity> getCurationList() { + return ResponseEntity.ok(BaseResponse.ok(curationService.getCurationList())); + } } diff --git a/src/main/java/com/earseo/sight/dto/response/CurationList.java b/src/main/java/com/earseo/sight/dto/response/CurationList.java new file mode 100644 index 0000000..d3de4f1 --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/response/CurationList.java @@ -0,0 +1,11 @@ +package com.earseo.sight.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record CurationList( + @Schema(description = "큐레이션 목록") + List curationList +) { +} diff --git a/src/main/java/com/earseo/sight/dto/response/CurationResponse.java b/src/main/java/com/earseo/sight/dto/response/CurationResponse.java new file mode 100644 index 0000000..790ee75 --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/response/CurationResponse.java @@ -0,0 +1,18 @@ +package com.earseo.sight.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CurationResponse( + @Schema(description = "큐레이션 ID", example = "1") + Long curationId, + + @Schema(description = "큐레이션 제목", example = "") + String curationTitle, + + @Schema(description = "큐레이션 세부 설명", example = "") + String description, + + @Schema(description = "큐레이션 이미지 URL", example = "") + String curationImgUrl +) { +} diff --git a/src/main/java/com/earseo/sight/entity/Curation.java b/src/main/java/com/earseo/sight/entity/Curation.java new file mode 100644 index 0000000..ef9b7bc --- /dev/null +++ b/src/main/java/com/earseo/sight/entity/Curation.java @@ -0,0 +1,28 @@ +package com.earseo.sight.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Curation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "curation_img_url", nullable = false) + private String curationImgUrl; +} diff --git a/src/main/java/com/earseo/sight/entity/CurationSight.java b/src/main/java/com/earseo/sight/entity/CurationSight.java new file mode 100644 index 0000000..f56c07b --- /dev/null +++ b/src/main/java/com/earseo/sight/entity/CurationSight.java @@ -0,0 +1,26 @@ +package com.earseo.sight.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "curation_sight") +public class CurationSight { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "sight_content_id", nullable = false) + private String sightContentId; + + @Column(name = "curation_id", nullable = false) + private Long curationId; +} diff --git a/src/main/java/com/earseo/sight/repository/CurationRepository.java b/src/main/java/com/earseo/sight/repository/CurationRepository.java new file mode 100644 index 0000000..9cc81db --- /dev/null +++ b/src/main/java/com/earseo/sight/repository/CurationRepository.java @@ -0,0 +1,16 @@ +package com.earseo.sight.repository; + +import com.earseo.sight.entity.Curation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface CurationRepository extends JpaRepository { + @Query(value = "SELECT * FROM curation ORDER BY RANDOM() LIMIT :curationListSize", + nativeQuery = true) + List findRandomCurations( + @Param("curationListSize") Integer curationListSize + ); +} diff --git a/src/main/java/com/earseo/sight/repository/CurationSightRepository.java b/src/main/java/com/earseo/sight/repository/CurationSightRepository.java new file mode 100644 index 0000000..174a59b --- /dev/null +++ b/src/main/java/com/earseo/sight/repository/CurationSightRepository.java @@ -0,0 +1,7 @@ +package com.earseo.sight.repository; + +import com.earseo.sight.entity.CurationSight; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CurationSightRepository extends JpaRepository { +} diff --git a/src/main/java/com/earseo/sight/service/CurationService.java b/src/main/java/com/earseo/sight/service/CurationService.java new file mode 100644 index 0000000..bb84b88 --- /dev/null +++ b/src/main/java/com/earseo/sight/service/CurationService.java @@ -0,0 +1,38 @@ +package com.earseo.sight.service; + +import com.earseo.sight.dto.response.CurationList; +import com.earseo.sight.dto.response.CurationResponse; +import com.earseo.sight.entity.Curation; +import com.earseo.sight.repository.CurationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CurationService { + + private static final int CURATION_LIST_SIZE = 4; + + private final CurationRepository curationRepository; + + @Transactional(readOnly = true) + public CurationList getCurationList() { + + List curations = curationRepository.findRandomCurations(CURATION_LIST_SIZE); + List curationResponses = curations.stream() + .map( + item -> new CurationResponse( + item.getId(), + item.getTitle(), + item.getDescription(), + item.getCurationImgUrl() + ) + ) + .toList(); + + return new CurationList(curationResponses); + } +} From fb8a18f20325c2c45b8623358fc04d6a4dad1aa5 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 06:52:19 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[#8]=20feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20-=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=EC=97=90=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=EB=90=9C=20=EA=B4=80=EA=B4=91=EC=A7=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sight/common/exception/SightError.java | 2 + .../sight/controller/SightController.java | 74 ++++++++++++++++++- .../dto/projection/CurationSightItemDto.java | 10 +++ .../sight/dto/response/CurationResponse.java | 6 +- .../sight/dto/response/CurationSightList.java | 17 +++++ .../dto/response/CurationSightResponse.java | 31 ++++++++ .../sight/repository/SightRepository.java | 19 +++++ .../earseo/sight/service/CurationService.java | 26 +++++++ 8 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/earseo/sight/dto/projection/CurationSightItemDto.java create mode 100644 src/main/java/com/earseo/sight/dto/response/CurationSightList.java create mode 100644 src/main/java/com/earseo/sight/dto/response/CurationSightResponse.java diff --git a/src/main/java/com/earseo/sight/common/exception/SightError.java b/src/main/java/com/earseo/sight/common/exception/SightError.java index d358c08..8ded246 100644 --- a/src/main/java/com/earseo/sight/common/exception/SightError.java +++ b/src/main/java/com/earseo/sight/common/exception/SightError.java @@ -9,6 +9,8 @@ public enum SightError implements ErrorCodeInterface { INVALID_COORDINATE_RANGE("SIT001", "최소 위도/경도는 최대 위도/경도보다 작아야 합니다.", HttpStatus.BAD_REQUEST), SIGHT_NOT_FOUND("SIT002", "해당 관광지를 찾을 수 없습니다", HttpStatus.NOT_FOUND), + + CURATION_NOT_FOUND("CUR001", "큐레이션이 존재하지 않습니다.", HttpStatus.NOT_FOUND), ; private final String status; diff --git a/src/main/java/com/earseo/sight/controller/SightController.java b/src/main/java/com/earseo/sight/controller/SightController.java index aaad422..3a66511 100644 --- a/src/main/java/com/earseo/sight/controller/SightController.java +++ b/src/main/java/com/earseo/sight/controller/SightController.java @@ -1,10 +1,7 @@ package com.earseo.sight.controller; import com.earseo.sight.common.BaseResponse; -import com.earseo.sight.dto.response.CurationList; -import com.earseo.sight.dto.response.DocentResponse; -import com.earseo.sight.dto.response.SightDetailInfoResponse; -import com.earseo.sight.dto.response.SightMapInfoList; +import com.earseo.sight.dto.response.*; import com.earseo.sight.service.CurationService; import com.earseo.sight.service.SightService; import io.swagger.v3.oas.annotations.Operation; @@ -376,4 +373,73 @@ public ResponseEntity> getDocent( public ResponseEntity> getCurationList() { return ResponseEntity.ok(BaseResponse.ok(curationService.getCurationList())); } + + @Operation( + summary = "큐레이션에 포함된 관광지 목록 조회", + description = "지정된 큐레이션 ID에 포함된 관광지들을 중심점(위도/경도) 기준 거리순으로 반환합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content( + schema = @Schema(implementation = CurationSightList.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "파라미터 오류" + ), + @ApiResponse( + responseCode = "404", + description = "큐레이션을 찾을 수 없음", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + examples = @ExampleObject( + value = """ + { + "status": "CURATION_NOT_FOUND", + "message": "큐레이션이 존재하지 않습니다.", + "data": null + } + """ + ) + ) + ) + }) + @GetMapping("/curation/{curationId}") + public ResponseEntity> getCurationSight( + @Parameter( + description = "큐레이션 ID", + required = true, + example = "1" + ) + @PathVariable + Long curationId, + @Parameter( + description = "중심점 경도", + required = true, + example = "126.9780", + schema = @Schema(minimum = "124", maximum = "133") + ) + @RequestParam + @NotNull(message = "경도는 필수입니다") + @DecimalMin(value = "124", message = "경도는 124 이상이어야 합니다") + @DecimalMax(value = "133", message = "경도는 133 이하여야 합니다") + Double longitude, + + @Parameter( + description = "중심점 위도", + required = true, + example = "37.5665", + schema = @Schema(minimum = "33.0", maximum = "39") + ) + @RequestParam + @NotNull(message = "위도는 필수입니다") + @DecimalMin(value = "33.0", message = "위도는 33 이상이어야 합니다") + @DecimalMax(value = "39", message = "위도는 39 이하여야 합니다") + Double latitude + ) { + return ResponseEntity.ok(BaseResponse.ok(curationService.getCurationSight(curationId, longitude, latitude))); + } } diff --git a/src/main/java/com/earseo/sight/dto/projection/CurationSightItemDto.java b/src/main/java/com/earseo/sight/dto/projection/CurationSightItemDto.java new file mode 100644 index 0000000..0caafd1 --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/projection/CurationSightItemDto.java @@ -0,0 +1,10 @@ +package com.earseo.sight.dto.projection; + +public record CurationSightItemDto( + String contentId, + String title, + String cat2, + Double distance, + String addr3 +) { +} diff --git a/src/main/java/com/earseo/sight/dto/response/CurationResponse.java b/src/main/java/com/earseo/sight/dto/response/CurationResponse.java index 790ee75..4f83a05 100644 --- a/src/main/java/com/earseo/sight/dto/response/CurationResponse.java +++ b/src/main/java/com/earseo/sight/dto/response/CurationResponse.java @@ -6,13 +6,13 @@ public record CurationResponse( @Schema(description = "큐레이션 ID", example = "1") Long curationId, - @Schema(description = "큐레이션 제목", example = "") + @Schema(description = "큐레이션 제목", example = "시간이 머문 서울의 길을 걸어보세요") String curationTitle, - @Schema(description = "큐레이션 세부 설명", example = "") + @Schema(description = "큐레이션 세부 설명", example = "서울의 유산이 살아 숨쉬는 이야기로 안내합니다.") String description, - @Schema(description = "큐레이션 이미지 URL", example = "") + @Schema(description = "큐레이션 이미지 URL", example = "https://example.com/image.jpg") String curationImgUrl ) { } diff --git a/src/main/java/com/earseo/sight/dto/response/CurationSightList.java b/src/main/java/com/earseo/sight/dto/response/CurationSightList.java new file mode 100644 index 0000000..0303635 --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/response/CurationSightList.java @@ -0,0 +1,17 @@ +package com.earseo.sight.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record CurationSightList( + @Schema(description = "큐레이션 제목", example = "시간이 머문 서울의 길을 걸어보세요") + String curationTitle, + + @Schema(description = "큐레이션 세부 설명", example = "서울의 유산이 살아 숨쉬는 이야기로 안내합니다.") + String description, + + @Schema(description = "큐레이션 내 관광지 목록") + List curationSightList +) { +} diff --git a/src/main/java/com/earseo/sight/dto/response/CurationSightResponse.java b/src/main/java/com/earseo/sight/dto/response/CurationSightResponse.java new file mode 100644 index 0000000..a87d38e --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/response/CurationSightResponse.java @@ -0,0 +1,31 @@ +package com.earseo.sight.dto.response; + +import com.earseo.sight.dto.projection.CurationSightItemDto; +import io.swagger.v3.oas.annotations.media.Schema; + +public record CurationSightResponse( + @Schema(description = "관광지 고유 ID", example = "126508") + String sightId, + + @Schema(description = "관광지 이름", example = "경복궁") + String title, + + @Schema(description = "관광지 테마 (예: 문화시설, 자연관광지 등)", example = "인문") + String theme, + + @Schema(description = "현재 위치로부터의 직선 거리 (미터)", example = "1234.56") + Double distance, + + @Schema(description = "주소 요약 (구/동 단위)", example = "서울 종로구") + String address +) { + public static CurationSightResponse toDto(CurationSightItemDto dto) { + return new CurationSightResponse( + dto.contentId(), + dto.title(), + dto.cat2(), + Math.round(dto.distance()/1000 * 10.0) / 10.0, + dto.addr3() + ); + } +} diff --git a/src/main/java/com/earseo/sight/repository/SightRepository.java b/src/main/java/com/earseo/sight/repository/SightRepository.java index 40b00e2..d2a12de 100644 --- a/src/main/java/com/earseo/sight/repository/SightRepository.java +++ b/src/main/java/com/earseo/sight/repository/SightRepository.java @@ -1,5 +1,6 @@ package com.earseo.sight.repository; +import com.earseo.sight.dto.projection.CurationSightItemDto; import com.earseo.sight.dto.projection.SightDetailItemDto; import com.earseo.sight.dto.projection.SightMapItemDto; import com.earseo.sight.dto.projection.SightMetaDto; @@ -71,4 +72,22 @@ SightDetailItemDto findByContentId( WHERE s.content_id IN :ids """, nativeQuery = true) List findByContentId(@Param("ids") List ids); + + @Query(value = """ + SELECT s.content_id, s.title, s.cat2, + ST_Distance( + s.geom::geography, + ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326)::geography + ) as distance, + s.addr3 + FROM sight s + JOIN curation_sight cs ON cs.sight_content_id = s.content_id + WHERE cs.curation_id = :curationId + ORDER BY cs.id + """, nativeQuery = true) + List findByCurationId( + @Param("curationId") Long curationId, + @Param("longitude") Double longitude, + @Param("latitude") Double latitude + ); } diff --git a/src/main/java/com/earseo/sight/service/CurationService.java b/src/main/java/com/earseo/sight/service/CurationService.java index bb84b88..ffc0e5d 100644 --- a/src/main/java/com/earseo/sight/service/CurationService.java +++ b/src/main/java/com/earseo/sight/service/CurationService.java @@ -1,9 +1,15 @@ package com.earseo.sight.service; +import com.earseo.sight.common.exception.BaseException; +import com.earseo.sight.common.exception.SightError; +import com.earseo.sight.dto.projection.CurationSightItemDto; import com.earseo.sight.dto.response.CurationList; import com.earseo.sight.dto.response.CurationResponse; +import com.earseo.sight.dto.response.CurationSightList; +import com.earseo.sight.dto.response.CurationSightResponse; import com.earseo.sight.entity.Curation; import com.earseo.sight.repository.CurationRepository; +import com.earseo.sight.repository.SightRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +23,7 @@ public class CurationService { private static final int CURATION_LIST_SIZE = 4; private final CurationRepository curationRepository; + private final SightRepository sightRepository; @Transactional(readOnly = true) public CurationList getCurationList() { @@ -35,4 +42,23 @@ public CurationList getCurationList() { return new CurationList(curationResponses); } + + @Transactional(readOnly = true) + public CurationSightList getCurationSight(Long curationId, Double longitude, Double latitude) { + + Curation curation = curationRepository.findById(curationId).orElseThrow( + () -> new BaseException(SightError.CURATION_NOT_FOUND) + ); + + List curationSights = sightRepository.findByCurationId(curationId, longitude, latitude); + List curationSightResponses = curationSights.stream() + .map(CurationSightResponse::toDto) + .toList(); + + return new CurationSightList( + curation.getTitle(), + curation.getDescription(), + curationSightResponses + ); + } } From ab5af0b4c384bb176e2e8267bcdb1df9f3755d3d Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 13:13:08 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[#8]=20feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SightAdminController.java | 23 +++++++++++ .../dto/request/CurationCreateRequest.java | 23 +++++++++++ .../sight/repository/SightRepository.java | 2 + .../earseo/sight/service/CurationService.java | 39 ++++++++++++++++++- 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/earseo/sight/dto/request/CurationCreateRequest.java diff --git a/src/main/java/com/earseo/sight/controller/SightAdminController.java b/src/main/java/com/earseo/sight/controller/SightAdminController.java index be7ab19..ac9e2e7 100644 --- a/src/main/java/com/earseo/sight/controller/SightAdminController.java +++ b/src/main/java/com/earseo/sight/controller/SightAdminController.java @@ -1,10 +1,18 @@ package com.earseo.sight.controller; import com.earseo.sight.common.BaseResponse; +import com.earseo.sight.dto.request.CurationCreateRequest; +import com.earseo.sight.dto.response.CurationResponse; +import com.earseo.sight.service.CurationService; import com.earseo.sight.service.InitService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -16,6 +24,7 @@ public class SightAdminController { private final InitService initService; + private final CurationService curationService; @PostMapping("/init") public ResponseEntity> initSight() { @@ -23,4 +32,18 @@ public ResponseEntity> initSight() { initService.initDocent(); return ResponseEntity.ok(BaseResponse.ok(null)); } + + @Operation(summary = "큐레이션 생성") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "생성 성공"), + @ApiResponse(responseCode = "400", description = "유효하지 않은 입력") + }) + @PostMapping("/curation") + public ResponseEntity> createCuration( + @RequestBody + @Valid + CurationCreateRequest request + ){ + return ResponseEntity.ok(BaseResponse.ok(curationService.createCuration(request))); + } } diff --git a/src/main/java/com/earseo/sight/dto/request/CurationCreateRequest.java b/src/main/java/com/earseo/sight/dto/request/CurationCreateRequest.java new file mode 100644 index 0000000..da6f215 --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/request/CurationCreateRequest.java @@ -0,0 +1,23 @@ +package com.earseo.sight.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +import java.util.List; + +public record CurationCreateRequest( + @NotBlank(message = "제목은 필수입니다") + @Size(max = 100, message = "제목은 100자 이하여야 합니다") + String title, + + @Size(max = 500, message = "설명은 500자 이하여야 합니다") + String subtitle, + + String curationImgUrl, + + @NotEmpty(message = "관광지 목록은 필수입니다") + List contentIds +) { + +} \ No newline at end of file diff --git a/src/main/java/com/earseo/sight/repository/SightRepository.java b/src/main/java/com/earseo/sight/repository/SightRepository.java index d2a12de..74ca89e 100644 --- a/src/main/java/com/earseo/sight/repository/SightRepository.java +++ b/src/main/java/com/earseo/sight/repository/SightRepository.java @@ -90,4 +90,6 @@ List findByCurationId( @Param("longitude") Double longitude, @Param("latitude") Double latitude ); + + List findAllByContentIdIn(List sightIds); } diff --git a/src/main/java/com/earseo/sight/service/CurationService.java b/src/main/java/com/earseo/sight/service/CurationService.java index ffc0e5d..aa18d45 100644 --- a/src/main/java/com/earseo/sight/service/CurationService.java +++ b/src/main/java/com/earseo/sight/service/CurationService.java @@ -3,12 +3,16 @@ import com.earseo.sight.common.exception.BaseException; import com.earseo.sight.common.exception.SightError; import com.earseo.sight.dto.projection.CurationSightItemDto; +import com.earseo.sight.dto.request.CurationCreateRequest; import com.earseo.sight.dto.response.CurationList; import com.earseo.sight.dto.response.CurationResponse; import com.earseo.sight.dto.response.CurationSightList; import com.earseo.sight.dto.response.CurationSightResponse; import com.earseo.sight.entity.Curation; +import com.earseo.sight.entity.CurationSight; +import com.earseo.sight.entity.Sight; import com.earseo.sight.repository.CurationRepository; +import com.earseo.sight.repository.CurationSightRepository; import com.earseo.sight.repository.SightRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,6 +27,7 @@ public class CurationService { private static final int CURATION_LIST_SIZE = 4; private final CurationRepository curationRepository; + private final CurationSightRepository curationSightRepository; private final SightRepository sightRepository; @Transactional(readOnly = true) @@ -61,4 +66,36 @@ public CurationSightList getCurationSight(Long curationId, Double longitude, Dou curationSightResponses ); } -} + + @Transactional + public CurationResponse createCuration(CurationCreateRequest request) { + List sights = sightRepository.findAllByContentIdIn(request.contentIds()); + if (sights.size() != request.contentIds().size()) { + throw new BaseException(SightError.SIGHT_NOT_FOUND); + } + + Curation curation = Curation.builder() + .title(request.title()) + .description(request.subtitle()) + .curationImgUrl(request.curationImgUrl()) + .build(); + + curationRepository.save(curation); + + List curationSights = request.contentIds().stream() + .map(sightId -> CurationSight.builder() + .curationId(curation.getId()) + .sightContentId(sightId) + .build()) + .toList(); + + curationSightRepository.saveAll(curationSights); + + return new CurationResponse( + curation.getId(), + curation.getTitle(), + curation.getDescription(), + curation.getCurationImgUrl() + ); + } +} \ No newline at end of file From a29802e78b41c0eaee16cd1513c88a8c8f7e57bc Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 13:30:34 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[#8]=20feat:=20=EA=B4=80=EA=B4=91=EC=A7=80?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=EC=97=90=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/SightDetailInfoResponse.java | 12 +++++++--- .../sight/repository/CurationRepository.java | 9 ++++++++ .../earseo/sight/service/SightService.java | 23 +++++++++++++++---- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/earseo/sight/dto/response/SightDetailInfoResponse.java b/src/main/java/com/earseo/sight/dto/response/SightDetailInfoResponse.java index 7f81b48..67536c5 100644 --- a/src/main/java/com/earseo/sight/dto/response/SightDetailInfoResponse.java +++ b/src/main/java/com/earseo/sight/dto/response/SightDetailInfoResponse.java @@ -3,6 +3,8 @@ import com.earseo.sight.dto.projection.SightDetailItemDto; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + public record SightDetailInfoResponse( @Schema(description = "관광지 고유 ID", example = "126508") String id, @@ -53,9 +55,12 @@ public record SightDetailInfoResponse( String docentUrl, @Schema(description = "북마크 여부", example = "true") - boolean isBookmarked + boolean isBookmarked, + + @Schema(description = "관광지가 포함된 큐레이션 목록") + List curationList ) { - public static SightDetailInfoResponse toDto(SightDetailItemDto dto) { + public static SightDetailInfoResponse toDto(SightDetailItemDto dto, List curationList) { return new SightDetailInfoResponse( dto.contentId(), dto.cat1(), @@ -73,7 +78,8 @@ public static SightDetailInfoResponse toDto(SightDetailItemDto dto) { dto.usefee(), Math.round(dto.distance()/1000 * 10.0) / 10.0, dto.docentUrl(), - dto.isBookmarked() + dto.isBookmarked(), + curationList ); } } diff --git a/src/main/java/com/earseo/sight/repository/CurationRepository.java b/src/main/java/com/earseo/sight/repository/CurationRepository.java index 9cc81db..03061c4 100644 --- a/src/main/java/com/earseo/sight/repository/CurationRepository.java +++ b/src/main/java/com/earseo/sight/repository/CurationRepository.java @@ -13,4 +13,13 @@ public interface CurationRepository extends JpaRepository { List findRandomCurations( @Param("curationListSize") Integer curationListSize ); + + @Query(value = """ + SELECT * FROM curation c + JOIN curation_sight cs ON c.id = cs.curation_id + WHERE cs.sight_content_id = :contentId + """, nativeQuery = true) + List findAllBySightContentId( + @Param("contentId") String contentId + ); } diff --git a/src/main/java/com/earseo/sight/service/SightService.java b/src/main/java/com/earseo/sight/service/SightService.java index eacd055..5d268c6 100644 --- a/src/main/java/com/earseo/sight/service/SightService.java +++ b/src/main/java/com/earseo/sight/service/SightService.java @@ -5,11 +5,10 @@ import com.earseo.sight.common.exception.SightError; import com.earseo.sight.dto.projection.SightDetailItemDto; import com.earseo.sight.dto.projection.SightMapItemDto; -import com.earseo.sight.dto.response.DocentResponse; -import com.earseo.sight.dto.response.SightDetailInfoResponse; -import com.earseo.sight.dto.response.SightInfoResponse; -import com.earseo.sight.dto.response.SightMapInfoList; +import com.earseo.sight.dto.response.*; +import com.earseo.sight.entity.Curation; import com.earseo.sight.entity.Docent; +import com.earseo.sight.repository.CurationRepository; import com.earseo.sight.repository.DocentRepository; import com.earseo.sight.repository.SightRepository; import lombok.RequiredArgsConstructor; @@ -24,6 +23,7 @@ public class SightService { private final SightRepository sightRepository; private final DocentRepository docentRepository; + private final CurationRepository curationRepository; public SightMapInfoList getMapRectangle(Double minLongitude, Double minLatitude, Double maxLongitude, Double maxLatitude) { if (minLongitude >= maxLongitude || minLatitude >= maxLatitude) { @@ -68,7 +68,20 @@ public SightDetailInfoResponse getSightDetailInfo(String id, Double longitude, D throw new BaseException(SightError.SIGHT_NOT_FOUND); } - return SightDetailInfoResponse.toDto(dto); + + List curations = curationRepository.findAllBySightContentId(id); + List curationResponses = curations.stream() + .map( + item -> new CurationResponse( + item.getId(), + item.getTitle(), + item.getDescription(), + item.getCurationImgUrl() + ) + ) + .toList(); + + return SightDetailInfoResponse.toDto(dto, curationResponses); } public DocentResponse getDocent(String sightId) { From 274fc8a971294b373251985f4b01cd7f78850449 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 13:41:56 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[#8]=20feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SightAdminController.java | 31 ++++++++++++++++--- .../dto/response/CurationDeleteResponse.java | 6 ++++ .../repository/CurationSightRepository.java | 9 ++++++ .../earseo/sight/service/CurationService.java | 17 +++++++--- 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/earseo/sight/dto/response/CurationDeleteResponse.java diff --git a/src/main/java/com/earseo/sight/controller/SightAdminController.java b/src/main/java/com/earseo/sight/controller/SightAdminController.java index ac9e2e7..cd1b58b 100644 --- a/src/main/java/com/earseo/sight/controller/SightAdminController.java +++ b/src/main/java/com/earseo/sight/controller/SightAdminController.java @@ -2,21 +2,19 @@ import com.earseo.sight.common.BaseResponse; import com.earseo.sight.dto.request.CurationCreateRequest; +import com.earseo.sight.dto.response.CurationDeleteResponse; import com.earseo.sight.dto.response.CurationResponse; import com.earseo.sight.service.CurationService; import com.earseo.sight.service.InitService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import java.io.IOException; @RestController @RequestMapping("/api/admin/sight") @@ -46,4 +44,27 @@ public ResponseEntity> createCuration( ){ return ResponseEntity.ok(BaseResponse.ok(curationService.createCuration(request))); } + + + @Operation( + summary = "큐레이션 삭제", + description = "큐레이션을 삭제합니다. 연관된 큐레이션-관광지 매핑도 함께 삭제됩니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "큐레이션 삭제 성공" + ), + @ApiResponse( + responseCode = "404", + description = "큐레이션을 찾을 수 없음" + ) + }) + @DeleteMapping("/curation/{curationId}") + public ResponseEntity> deleteCuration( + @Parameter(description = "큐레이션 ID", required = true) + @PathVariable Long curationId + ) { + return ResponseEntity.ok(BaseResponse.ok(curationService.deleteCuration(curationId))); + } } diff --git a/src/main/java/com/earseo/sight/dto/response/CurationDeleteResponse.java b/src/main/java/com/earseo/sight/dto/response/CurationDeleteResponse.java new file mode 100644 index 0000000..cd1fd7f --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/response/CurationDeleteResponse.java @@ -0,0 +1,6 @@ +package com.earseo.sight.dto.response; + +public record CurationDeleteResponse( + Long id +) { +} diff --git a/src/main/java/com/earseo/sight/repository/CurationSightRepository.java b/src/main/java/com/earseo/sight/repository/CurationSightRepository.java index 174a59b..075fe74 100644 --- a/src/main/java/com/earseo/sight/repository/CurationSightRepository.java +++ b/src/main/java/com/earseo/sight/repository/CurationSightRepository.java @@ -2,6 +2,15 @@ import com.earseo.sight.entity.CurationSight; 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; +import org.springframework.transaction.annotation.Transactional; public interface CurationSightRepository extends JpaRepository { + + @Transactional + @Modifying + @Query("DELETE FROM CurationSight cs WHERE cs.curationId = :curationId") + void deleteByCurationId(@Param("curationId") Long curationId); } diff --git a/src/main/java/com/earseo/sight/service/CurationService.java b/src/main/java/com/earseo/sight/service/CurationService.java index aa18d45..55109aa 100644 --- a/src/main/java/com/earseo/sight/service/CurationService.java +++ b/src/main/java/com/earseo/sight/service/CurationService.java @@ -4,10 +4,7 @@ import com.earseo.sight.common.exception.SightError; import com.earseo.sight.dto.projection.CurationSightItemDto; import com.earseo.sight.dto.request.CurationCreateRequest; -import com.earseo.sight.dto.response.CurationList; -import com.earseo.sight.dto.response.CurationResponse; -import com.earseo.sight.dto.response.CurationSightList; -import com.earseo.sight.dto.response.CurationSightResponse; +import com.earseo.sight.dto.response.*; import com.earseo.sight.entity.Curation; import com.earseo.sight.entity.CurationSight; import com.earseo.sight.entity.Sight; @@ -98,4 +95,16 @@ public CurationResponse createCuration(CurationCreateRequest request) { curation.getCurationImgUrl() ); } + + @Transactional + public CurationDeleteResponse deleteCuration(Long curationId) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(() -> new BaseException(SightError.CURATION_NOT_FOUND)); + + curationSightRepository.deleteByCurationId(curationId); + + curationRepository.delete(curation); + + return new CurationDeleteResponse(curationId); + } } \ No newline at end of file From ccbf8122d93c5b1ce4c54b0af4948423f92e3c6a Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 13:48:17 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[#8]=20feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SightAdminController.java | 30 +++++++++++++++++++ .../dto/request/CurationUpdateRequest.java | 15 ++++++++++ .../com/earseo/sight/entity/Curation.java | 12 ++++++++ .../earseo/sight/service/CurationService.java | 21 +++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 src/main/java/com/earseo/sight/dto/request/CurationUpdateRequest.java diff --git a/src/main/java/com/earseo/sight/controller/SightAdminController.java b/src/main/java/com/earseo/sight/controller/SightAdminController.java index cd1b58b..9cfb28f 100644 --- a/src/main/java/com/earseo/sight/controller/SightAdminController.java +++ b/src/main/java/com/earseo/sight/controller/SightAdminController.java @@ -2,12 +2,15 @@ import com.earseo.sight.common.BaseResponse; import com.earseo.sight.dto.request.CurationCreateRequest; +import com.earseo.sight.dto.request.CurationUpdateRequest; import com.earseo.sight.dto.response.CurationDeleteResponse; import com.earseo.sight.dto.response.CurationResponse; import com.earseo.sight.service.CurationService; import com.earseo.sight.service.InitService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; @@ -45,6 +48,33 @@ public ResponseEntity> createCuration( return ResponseEntity.ok(BaseResponse.ok(curationService.createCuration(request))); } + @PutMapping("/curation/{curationId}") + @Operation( + summary = "큐레이션 수정", + description = "큐레이션 정보를 수정합니다. 제공된 필드만 업데이트됩니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "큐레이션 수정 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = CurationResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "큐레이션을 찾을 수 없음" + ) + }) + public ResponseEntity> updateCuration( + @Parameter(description = "큐레이션 ID", required = true) + @PathVariable Long curationId, + @RequestBody @Valid CurationUpdateRequest request + ) { + CurationResponse response = curationService.updateCuration(curationId, request); + return ResponseEntity.ok(BaseResponse.ok(response)); + } @Operation( summary = "큐레이션 삭제", diff --git a/src/main/java/com/earseo/sight/dto/request/CurationUpdateRequest.java b/src/main/java/com/earseo/sight/dto/request/CurationUpdateRequest.java new file mode 100644 index 0000000..4778d65 --- /dev/null +++ b/src/main/java/com/earseo/sight/dto/request/CurationUpdateRequest.java @@ -0,0 +1,15 @@ +package com.earseo.sight.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record CurationUpdateRequest( + @Schema(description = "큐레이션 제목", example = "서울 핫플 여행") + String title, + + @Schema(description = "큐레이션 설명", example = "서울의 인기 관광지를 모았습니다") + String description, + + @Schema(description = "큐레이션 이미지 URL", example = "https://example.com/image.jpg") + String curationImgUrl +) { +} diff --git a/src/main/java/com/earseo/sight/entity/Curation.java b/src/main/java/com/earseo/sight/entity/Curation.java index ef9b7bc..cbdfbc0 100644 --- a/src/main/java/com/earseo/sight/entity/Curation.java +++ b/src/main/java/com/earseo/sight/entity/Curation.java @@ -25,4 +25,16 @@ public class Curation { @Column(name = "curation_img_url", nullable = false) private String curationImgUrl; + + public void update(String title, String description, String curationImgUrl) { + if (title != null && !title.isBlank()) { + this.title = title; + } + if (description != null && !description.isBlank()) { + this.description = description; + } + if (curationImgUrl != null && !curationImgUrl.isBlank()) { + this.curationImgUrl = curationImgUrl; + } + } } diff --git a/src/main/java/com/earseo/sight/service/CurationService.java b/src/main/java/com/earseo/sight/service/CurationService.java index 55109aa..26babd5 100644 --- a/src/main/java/com/earseo/sight/service/CurationService.java +++ b/src/main/java/com/earseo/sight/service/CurationService.java @@ -4,6 +4,7 @@ import com.earseo.sight.common.exception.SightError; import com.earseo.sight.dto.projection.CurationSightItemDto; import com.earseo.sight.dto.request.CurationCreateRequest; +import com.earseo.sight.dto.request.CurationUpdateRequest; import com.earseo.sight.dto.response.*; import com.earseo.sight.entity.Curation; import com.earseo.sight.entity.CurationSight; @@ -96,6 +97,26 @@ public CurationResponse createCuration(CurationCreateRequest request) { ); } + @Transactional + public CurationResponse updateCuration(Long curationId, CurationUpdateRequest request) { + + Curation curation = curationRepository.findById(curationId) + .orElseThrow(() -> new BaseException(SightError.CURATION_NOT_FOUND)); + + curation.update( + request.title(), + request.description(), + request.curationImgUrl() + ); + + return new CurationResponse( + curation.getId(), + curation.getTitle(), + curation.getDescription(), + curation.getCurationImgUrl() + ); + } + @Transactional public CurationDeleteResponse deleteCuration(Long curationId) { Curation curation = curationRepository.findById(curationId) From e4a329f81cfbece46fcd213aeb78f5ea3c47ef34 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 18:23:20 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[#8]=20fix:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=A1=B0=ED=9A=8C=20sql=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/earseo/sight/repository/CurationRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/earseo/sight/repository/CurationRepository.java b/src/main/java/com/earseo/sight/repository/CurationRepository.java index 03061c4..008cf44 100644 --- a/src/main/java/com/earseo/sight/repository/CurationRepository.java +++ b/src/main/java/com/earseo/sight/repository/CurationRepository.java @@ -15,7 +15,7 @@ List findRandomCurations( ); @Query(value = """ - SELECT * FROM curation c + SELECT c.id, c.title, c.description, c.curation_img_url FROM curation c JOIN curation_sight cs ON c.id = cs.curation_id WHERE cs.sight_content_id = :contentId """, nativeQuery = true)