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 @@ -114,6 +114,8 @@ public enum ErrorCode {

CANNOT_DELETE_SAVED_PLACE(HttpStatus.BAD_REQUEST, "임시 저장된 장소만 삭제할 수 있습니다."),

CANNOT_UPDATE_UNSAVED_PLACE(HttpStatus.BAD_REQUEST, "저장된 장소만 수정할 수 있습니다."),

INVALID_RATING(HttpStatus.BAD_REQUEST, "별점은 1-5 사이의 값이어야 합니다."),

// Folder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package kr.suhsaechan.mapsy.place.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.UUID;
import kr.suhsaechan.mapsy.place.entity.MemberPlace;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "북마크 DTO")
public class BookmarkDto {

@Schema(description = "MemberPlace ID", example = "550e8400-e29b-41d4-a716-446655440000")
private UUID memberPlaceId;

@Schema(description = "장소 정보")
private PlaceDto place;

@Schema(description = "폴더명", example = "가고 싶은 곳")
private String folder;

@Schema(description = "메모", example = "친구랑 같이 가기")
private String memo;

@Schema(description = "별점 (1-5)", example = "4")
private Integer rating;

@Schema(description = "방문 여부", example = "false")
private Boolean visited;

@Schema(description = "방문 일시")
private LocalDateTime visitedAt;

@Schema(description = "저장 일시")
private LocalDateTime savedAt;

public static BookmarkDto from(MemberPlace memberPlace) {
if (memberPlace == null) {
return null;
}

return BookmarkDto.builder()
.memberPlaceId(memberPlace.getId())
.place(PlaceDto.from(memberPlace.getPlace()))
.folder(memberPlace.getFolder())
.memo(memberPlace.getMemo())
.rating(memberPlace.getRating())
.visited(memberPlace.getVisited())
.visitedAt(memberPlace.getVisitedAt())
.savedAt(memberPlace.getSavedAt())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import kr.suhsaechan.mapsy.place.constant.FolderVisibility;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -14,6 +15,7 @@
@Schema(description = "폴더 생성 요청")
public class CreateFolderRequest {
@Schema(description = "폴더 이름", example = "가고 싶은 곳")
@Size(max = 100, message = "폴더 이름은 100자 이하여야 합니다.")
private String name;

@Schema(description = "공개 설정", example = "PRIVATE")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package kr.suhsaechan.mapsy.place.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import java.util.UUID;
import kr.suhsaechan.mapsy.place.entity.Keyword;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "키워드 DTO")
public class KeywordDto {

@Schema(description = "키워드 ID", example = "550e8400-e29b-41d4-a716-446655440000")
private UUID id;

@Schema(description = "키워드 문자열", example = "강남카페")
private String keyword;

@Schema(description = "사용 횟수", example = "42")
private Integer count;

@Schema(description = "트렌드 점수", example = "15.50")
private BigDecimal trendScore;

public static KeywordDto from(Keyword keyword) {
if (keyword == null) {
return null;
}

return KeywordDto.builder()
.id(keyword.getId())
.keyword(keyword.getKeyword())
.count(keyword.getCount())
.trendScore(keyword.getTrendScore())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package kr.suhsaechan.mapsy.place.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "북마크 수정 요청")
public class UpdateBookmarkRequest {

@Schema(description = "폴더명 (null이면 변경하지 않음)", example = "가고 싶은 곳")
private String folder;

@Schema(description = "메모 (null이면 변경하지 않음)", example = "친구랑 같이 가기")
private String memo;

@Schema(description = "별점 1-5 (null이면 변경하지 않음)", example = "4")
private Integer rating;

@Schema(description = "방문 여부 (null이면 변경하지 않음)", example = "true")
private Boolean visited;

@Schema(description = "방문 일시 (null이면 visited=true 시 현재 시간)")
private LocalDateTime visitedAt;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import kr.suhsaechan.mapsy.place.constant.FolderVisibility;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -14,6 +15,7 @@
@Schema(description = "폴더 수정 요청")
public class UpdateFolderRequest {
@Schema(description = "폴더 이름", example = "맛집 모음")
@Size(max = 100, message = "폴더 이름은 100자 이하여야 합니다.")
private String name;

@Schema(description = "공개 설정", example = "SHARED")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ protected void onCreate() {
if (visibility == null) {
visibility = FolderVisibility.PRIVATE;
}
if (isDefault == null) {
isDefault = false;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand All @@ -16,6 +18,15 @@
import lombok.NoArgsConstructor;

@Entity
@Table(
name = "folder_place",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_folder_place",
columnNames = {"folder_id", "place_id"}
)
}
)
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface FolderRepository extends JpaRepository<Folder, UUID> {

List<Folder> findByOwnerAndDeletedAtIsNullOrderByCreatedAtAsc(Member owner);

@Query("SELECT f, COUNT(fp) FROM Folder f " +
"LEFT JOIN FolderPlace fp ON fp.folder = f AND fp.deletedAt IS NULL " +
"WHERE f.owner = :owner AND f.deletedAt IS NULL " +
"GROUP BY f " +
"ORDER BY f.createdAt ASC")
List<Object[]> findByOwnerWithPlaceCount(@Param("owner") Member owner);

Optional<Folder> findByOwnerAndIsDefaultTrueAndDeletedAtIsNull(Member owner);

Optional<Folder> findByIdAndDeletedAtIsNull(UUID id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down Expand Up @@ -70,4 +72,65 @@ List<MemberPlace> findByMemberAndSavedStatusWithPlace(
@Param("member") Member member,
@Param("savedStatus") PlaceSavedStatus savedStatus
);

/**
* 회원의 북마크 페이지네이션 조회
* - N+1 문제 방지를 위한 Fetch Join
* - countQuery를 분리하여 Page 객체 지원
*
* @param memberId 회원 ID
* @param savedStatus 저장 상태 (SAVED, TEMPORARY 등)
* @param pageable 페이지 정보
* @return 북마크 Page
*/
@Query(value = "SELECT mp FROM MemberPlace mp " +
"JOIN FETCH mp.place " +
"WHERE mp.member.id = :memberId " +
"AND mp.savedStatus = :savedStatus " +
"AND mp.deletedAt IS NULL",
countQuery = "SELECT COUNT(mp) FROM MemberPlace mp " +
"WHERE mp.member.id = :memberId " +
"AND mp.savedStatus = :savedStatus " +
"AND mp.deletedAt IS NULL")
Page<MemberPlace> findBookmarksByMemberIdAndSavedStatus(
@Param("memberId") UUID memberId,
@Param("savedStatus") PlaceSavedStatus savedStatus,
Pageable pageable
);

/**
* MemberPlace ID로 조회 (회원 검증 포함)
*
* @param id MemberPlace ID
* @param memberId 회원 ID
* @return MemberPlace (Optional)
*/
@Query("SELECT mp FROM MemberPlace mp " +
"WHERE mp.id = :id " +
"AND mp.member.id = :memberId " +
"AND mp.deletedAt IS NULL")
Optional<MemberPlace> findByIdAndMemberIdAndDeletedAtIsNull(
@Param("id") UUID id,
@Param("memberId") UUID memberId
);

/**
* 회원의 TOP 저장 장소 조회
* - Pageable의 size로 개수 제한, sort로 정렬 가능
*
* @param memberId 회원 ID
* @param savedStatus 저장 상태
* @param pageable 페이지 정보 (size, sort 사용)
* @return MemberPlace 목록
*/
@Query("SELECT mp FROM MemberPlace mp " +
"JOIN FETCH mp.place " +
"WHERE mp.member.id = :memberId " +
"AND mp.savedStatus = :savedStatus " +
"AND mp.deletedAt IS NULL")
List<MemberPlace> findTopPlacesByMemberIdAndSavedStatus(
@Param("memberId") UUID memberId,
@Param("savedStatus") PlaceSavedStatus savedStatus,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package kr.suhsaechan.mapsy.place.repository;

import kr.suhsaechan.mapsy.place.constant.PlaceSavedStatus;
import kr.suhsaechan.mapsy.place.entity.Place;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down Expand Up @@ -46,4 +49,33 @@ Optional<Place> findByNormalizedNameAndAddress(
@Param("name") String name,
@Param("address") String address
);

/**
* 최신 장소 피드 조회
* - 생성일 기준 내림차순 정렬
*
* @param pageable 페이지 정보
* @return Page<Place>
*/
Page<Place> findAllByOrderByCreatedAtDesc(Pageable pageable);

/**
* 인기 장소 피드 조회
* - MemberPlace에 저장된 횟수 기준 내림차순 정렬
*
* @param savedStatus 저장 상태 (SAVED 등)
* @param pageable 페이지 정보
* @return Page<Place>
*/
@Query(value = "SELECT p FROM Place p " +
"LEFT JOIN MemberPlace mp ON mp.place = p " +
"AND mp.savedStatus = :savedStatus " +
"AND mp.deletedAt IS NULL " +
"GROUP BY p " +
"ORDER BY COUNT(mp) DESC, p.createdAt DESC",
countQuery = "SELECT COUNT(DISTINCT p) FROM Place p")
Page<Place> findPopularPlaces(
@Param("savedStatus") PlaceSavedStatus savedStatus,
Pageable pageable
);
}
Loading