diff --git a/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/exception/constant/ErrorCode.java b/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/exception/constant/ErrorCode.java index 3a1cced..bf95086 100644 --- a/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/exception/constant/ErrorCode.java +++ b/MS-Common/src/main/java/kr/suhsaechan/mapsy/common/exception/constant/ErrorCode.java @@ -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 diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/BookmarkDto.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/BookmarkDto.java new file mode 100644 index 0000000..b05cd0a --- /dev/null +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/BookmarkDto.java @@ -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(); + } +} diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/CreateFolderRequest.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/CreateFolderRequest.java index 7c87c7f..37d624f 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/CreateFolderRequest.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/CreateFolderRequest.java @@ -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; @@ -14,6 +15,7 @@ @Schema(description = "폴더 생성 요청") public class CreateFolderRequest { @Schema(description = "폴더 이름", example = "가고 싶은 곳") + @Size(max = 100, message = "폴더 이름은 100자 이하여야 합니다.") private String name; @Schema(description = "공개 설정", example = "PRIVATE") diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/KeywordDto.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/KeywordDto.java new file mode 100644 index 0000000..977d05a --- /dev/null +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/KeywordDto.java @@ -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(); + } +} diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/UpdateBookmarkRequest.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/UpdateBookmarkRequest.java new file mode 100644 index 0000000..59d0ae9 --- /dev/null +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/UpdateBookmarkRequest.java @@ -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; +} diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/UpdateFolderRequest.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/UpdateFolderRequest.java index 484deff..9afc430 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/UpdateFolderRequest.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/dto/UpdateFolderRequest.java @@ -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; @@ -14,6 +15,7 @@ @Schema(description = "폴더 수정 요청") public class UpdateFolderRequest { @Schema(description = "폴더 이름", example = "맛집 모음") + @Size(max = 100, message = "폴더 이름은 100자 이하여야 합니다.") private String name; @Schema(description = "공개 설정", example = "SHARED") diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/Folder.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/Folder.java index 3e7f3d7..e33bacf 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/Folder.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/Folder.java @@ -64,6 +64,9 @@ protected void onCreate() { if (visibility == null) { visibility = FolderVisibility.PRIVATE; } + if (isDefault == null) { + isDefault = false; + } } } diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/FolderPlace.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/FolderPlace.java index f882ead..1d192bd 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/FolderPlace.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/entity/FolderPlace.java @@ -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; @@ -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) diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/FolderRepository.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/FolderRepository.java index c8a76c9..3359869 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/FolderRepository.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/FolderRepository.java @@ -6,6 +6,8 @@ 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 @@ -13,6 +15,13 @@ public interface FolderRepository extends JpaRepository { List 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 findByOwnerWithPlaceCount(@Param("owner") Member owner); + Optional findByOwnerAndIsDefaultTrueAndDeletedAtIsNull(Member owner); Optional findByIdAndDeletedAtIsNull(UUID id); diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/MemberPlaceRepository.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/MemberPlaceRepository.java index 04bb124..357146d 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/MemberPlaceRepository.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/MemberPlaceRepository.java @@ -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; @@ -70,4 +72,65 @@ List 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 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 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 findTopPlacesByMemberIdAndSavedStatus( + @Param("memberId") UUID memberId, + @Param("savedStatus") PlaceSavedStatus savedStatus, + Pageable pageable + ); } diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/PlaceRepository.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/PlaceRepository.java index f87e136..2c0c266 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/PlaceRepository.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/repository/PlaceRepository.java @@ -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; @@ -46,4 +49,33 @@ Optional findByNormalizedNameAndAddress( @Param("name") String name, @Param("address") String address ); + + /** + * 최신 장소 피드 조회 + * - 생성일 기준 내림차순 정렬 + * + * @param pageable 페이지 정보 + * @return Page + */ + Page findAllByOrderByCreatedAtDesc(Pageable pageable); + + /** + * 인기 장소 피드 조회 + * - MemberPlace에 저장된 횟수 기준 내림차순 정렬 + * + * @param savedStatus 저장 상태 (SAVED 등) + * @param pageable 페이지 정보 + * @return Page + */ + @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 findPopularPlaces( + @Param("savedStatus") PlaceSavedStatus savedStatus, + Pageable pageable + ); } diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/FeedService.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/FeedService.java new file mode 100644 index 0000000..f6e70d0 --- /dev/null +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/FeedService.java @@ -0,0 +1,106 @@ +package kr.suhsaechan.mapsy.place.service; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import kr.suhsaechan.mapsy.place.constant.PlaceSavedStatus; +import kr.suhsaechan.mapsy.place.dto.KeywordDto; +import kr.suhsaechan.mapsy.place.dto.PlaceDto; +import kr.suhsaechan.mapsy.place.entity.Keyword; +import kr.suhsaechan.mapsy.place.entity.Place; +import kr.suhsaechan.mapsy.place.repository.PlaceRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Feed 비즈니스 로직 서비스 + * - 홈 화면 피드 데이터 제공 + */ +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class FeedService { + + private static final int DEFAULT_TOP_PLACES_LIMIT = 10; + + private final PlaceRepository placeRepository; + private final MemberPlaceService memberPlaceService; + private final KeywordService keywordService; + + /** + * 최신 장소 피드 조회 + * - 생성일 기준 최신순 정렬 + * + * @param pageable 페이지 정보 + * @return Page + */ + public Page getLatestPlaces(Pageable pageable) { + log.info("Fetching latest places: page={}, size={}", + pageable.getPageNumber(), pageable.getPageSize()); + + Page places = placeRepository.findAllByOrderByCreatedAtDesc(pageable); + + log.info("Found {} latest places (total: {})", + places.getNumberOfElements(), places.getTotalElements()); + + return places.map(PlaceDto::from); + } + + /** + * 인기 장소 피드 조회 + * - 저장 횟수 기준 내림차순 정렬 + * + * @param pageable 페이지 정보 + * @return Page + */ + public Page getPopularPlaces(Pageable pageable) { + log.info("Fetching popular places: page={}, size={}", + pageable.getPageNumber(), pageable.getPageSize()); + + Page places = placeRepository.findPopularPlaces(PlaceSavedStatus.SAVED, pageable); + + log.info("Found {} popular places (total: {})", + places.getNumberOfElements(), places.getTotalElements()); + + return places.map(PlaceDto::from); + } + + /** + * 내 TOP 저장 장소 조회 + * - 저장일 기준 최신순 + * - 최대 10개 + * + * @param memberId 회원 ID + * @return List + */ + public List getMyTopPlaces(UUID memberId) { + log.info("Fetching my top places for member: {}", memberId); + + return memberPlaceService.getMyTopPlaces(memberId, DEFAULT_TOP_PLACES_LIMIT); + } + + /** + * 트렌드 키워드 조회 + * - 트렌드 점수 기준 내림차순 정렬 + * + * @param size 조회 개수 + * @return List + */ + public List getTrendingKeywords(int size) { + log.info("Fetching trending keywords: size={}", size); + + Page keywords = keywordService.getTrendingKeywords(PageRequest.of(0, size)); + + log.info("Found {} trending keywords", keywords.getNumberOfElements()); + + return keywords.getContent().stream() + .map(KeywordDto::from) + .collect(Collectors.toList()); + } +} diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/FolderService.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/FolderService.java index df3db58..1cca629 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/FolderService.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/FolderService.java @@ -45,11 +45,12 @@ public class FolderService { public GetFoldersResponse getFolders(Member member) { log.info("Getting folders for member: {}", member.getId()); - List folders = folderRepository.findByOwnerAndDeletedAtIsNullOrderByCreatedAtAsc(member); + List results = folderRepository.findByOwnerWithPlaceCount(member); - List folderDtos = folders.stream() - .map(folder -> { - int placeCount = folderPlaceRepository.countByFolderAndDeletedAtIsNull(folder); + List folderDtos = results.stream() + .map(row -> { + Folder folder = (Folder) row[0]; + int placeCount = ((Long) row[1]).intValue(); return FolderDto.from(folder, placeCount); }) .collect(Collectors.toList()); diff --git a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/MemberPlaceService.java b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/MemberPlaceService.java index f3d55a4..da63269 100644 --- a/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/MemberPlaceService.java +++ b/MS-Place/src/main/java/kr/suhsaechan/mapsy/place/service/MemberPlaceService.java @@ -5,10 +5,12 @@ import kr.suhsaechan.mapsy.member.entity.Member; import kr.suhsaechan.mapsy.member.repository.MemberRepository; import kr.suhsaechan.mapsy.place.constant.PlaceSavedStatus; +import kr.suhsaechan.mapsy.place.dto.BookmarkDto; import kr.suhsaechan.mapsy.place.dto.GetSavedPlacesResponse; import kr.suhsaechan.mapsy.place.dto.GetTemporaryPlacesResponse; import kr.suhsaechan.mapsy.place.dto.PlaceDto; import kr.suhsaechan.mapsy.place.dto.SavePlaceResponse; +import kr.suhsaechan.mapsy.place.dto.UpdateBookmarkRequest; import kr.suhsaechan.mapsy.place.entity.MemberPlace; import kr.suhsaechan.mapsy.place.entity.Place; import kr.suhsaechan.mapsy.place.repository.MemberPlaceRepository; @@ -18,6 +20,10 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,6 +33,8 @@ @Transactional(readOnly = true) public class MemberPlaceService { + private static final int DEFAULT_TOP_PLACES_LIMIT = 10; + private final MemberPlaceRepository memberPlaceRepository; private final PlaceRepository placeRepository; private final MemberRepository memberRepository; @@ -206,6 +214,114 @@ public void deleteTemporaryPlace(UUID memberId, UUID placeId) { deleteTemporaryPlace(member, placeId); } + // ========== 북마크 관련 메서드 ========== + + /** + * 북마크 목록 조회 (페이지네이션) + * + * @param memberId 회원 ID + * @param pageable 페이지 정보 + * @return 북마크 Page + */ + public Page getBookmarks(UUID memberId, Pageable pageable) { + log.info("Getting bookmarks for member: {}, page: {}, size: {}", + memberId, pageable.getPageNumber(), pageable.getPageSize()); + + Page memberPlaces = memberPlaceRepository.findBookmarksByMemberIdAndSavedStatus( + memberId, PlaceSavedStatus.SAVED, pageable + ); + + log.info("Found {} bookmarks for member: {}", memberPlaces.getTotalElements(), memberId); + + return memberPlaces.map(BookmarkDto::from); + } + + /** + * 북마크 수정 + * - 폴더, 메모, 별점, 방문 여부 수정 가능 + * - null 값은 변경하지 않음 + * + * @param memberId 회원 ID + * @param memberPlaceId MemberPlace ID + * @param request 수정 요청 + * @return 수정된 북마크 정보 + */ + @Transactional + public BookmarkDto updateBookmark(UUID memberId, UUID memberPlaceId, UpdateBookmarkRequest request) { + log.info("Updating bookmark: memberId={}, memberPlaceId={}", memberId, memberPlaceId); + + // 1. MemberPlace 조회 (회원 검증 포함) + MemberPlace memberPlace = memberPlaceRepository.findByIdAndMemberIdAndDeletedAtIsNull( + memberPlaceId, memberId + ).orElseThrow(() -> { + log.error("MemberPlace not found: memberPlaceId={}, memberId={}", memberPlaceId, memberId); + return new CustomException(ErrorCode.MEMBER_PLACE_NOT_FOUND); + }); + + // 2. SAVED 상태만 수정 가능 + if (memberPlace.getSavedStatus() != PlaceSavedStatus.SAVED) { + log.error("Cannot update unsaved place: memberPlaceId={}, status={}", + memberPlaceId, memberPlace.getSavedStatus()); + throw new CustomException(ErrorCode.CANNOT_UPDATE_UNSAVED_PLACE); + } + + // 3. 필드별 수정 (null이 아닌 경우만) + if (request.getFolder() != null) { + memberPlace.updateFolder(request.getFolder()); + } + if (request.getMemo() != null) { + memberPlace.updateMemo(request.getMemo()); + } + if (request.getRating() != null) { + memberPlace.updateRating(request.getRating()); + } + if (request.getVisited() != null) { + if (request.getVisited()) { + memberPlace.markAsVisited(request.getVisitedAt()); + } else { + memberPlace.unmarkVisited(); + } + } + + MemberPlace savedMemberPlace = memberPlaceRepository.save(memberPlace); + log.info("Bookmark updated successfully: memberPlaceId={}", savedMemberPlace.getId()); + + return BookmarkDto.from(savedMemberPlace); + } + + /** + * 내 TOP 저장 장소 조회 + * + * @param memberId 회원 ID + * @param limit 조회 개수 (기본 10개) + * @return 장소 목록 + */ + public List getMyTopPlaces(UUID memberId, int limit) { + log.info("Getting top {} saved places for member: {}", limit, memberId); + + Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "savedAt")); + List memberPlaces = memberPlaceRepository.findTopPlacesByMemberIdAndSavedStatus( + memberId, PlaceSavedStatus.SAVED, pageable + ); + + log.info("Found {} top places for member: {}", memberPlaces.size(), memberId); + + return memberPlaces.stream() + .map(MemberPlace::getPlace) + .map(PlaceDto::from) + .collect(Collectors.toList()); + } + + /** + * 내 TOP 저장 장소 조회 (기본 개수) + * + * @param memberId 회원 ID + * @return 장소 목록 (최대 10개) + */ + public List getMyTopPlaces(UUID memberId) { + return getMyTopPlaces(memberId, DEFAULT_TOP_PLACES_LIMIT); + } + // ========== Private Helper Methods ========== /** diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/BookmarkController.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/BookmarkController.java new file mode 100644 index 0000000..4c6e542 --- /dev/null +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/BookmarkController.java @@ -0,0 +1,72 @@ +package kr.suhsaechan.mapsy.web.controller; + +import java.util.UUID; +import kr.suhsaechan.mapsy.auth.dto.CustomUserDetails; +import kr.suhsaechan.mapsy.place.dto.BookmarkDto; +import kr.suhsaechan.mapsy.place.dto.UpdateBookmarkRequest; +import kr.suhsaechan.mapsy.place.service.MemberPlaceService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@Slf4j +@RequestMapping("/api/bookmarks") +public class BookmarkController implements BookmarkControllerDocs { + + private final MemberPlaceService memberPlaceService; + + /** + * 북마크 목록 조회 (페이지네이션) + */ + @GetMapping + @Override + public ResponseEntity> getBookmarks( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PageableDefault(size = 20, sort = "savedAt", direction = Sort.Direction.DESC) Pageable pageable + ) { + log.info("Get bookmarks request from member: {}, page: {}, size: {}", + userDetails.getMemberId(), pageable.getPageNumber(), pageable.getPageSize()); + + Page response = memberPlaceService.getBookmarks( + userDetails.getMemberId(), + pageable + ); + + return ResponseEntity.ok(response); + } + + /** + * 북마크 수정 + */ + @PatchMapping("/{memberPlaceId}") + @Override + public ResponseEntity updateBookmark( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable UUID memberPlaceId, + @RequestBody UpdateBookmarkRequest request + ) { + log.info("Update bookmark request from member: {}, memberPlaceId: {}", + userDetails.getMemberId(), memberPlaceId); + + BookmarkDto response = memberPlaceService.updateBookmark( + userDetails.getMemberId(), + memberPlaceId, + request + ); + + return ResponseEntity.ok(response); + } +} diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/BookmarkControllerDocs.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/BookmarkControllerDocs.java new file mode 100644 index 0000000..c40b1d7 --- /dev/null +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/BookmarkControllerDocs.java @@ -0,0 +1,90 @@ +package kr.suhsaechan.mapsy.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import java.util.UUID; +import kr.suhsaechan.mapsy.auth.dto.CustomUserDetails; +import kr.suhsaechan.mapsy.common.constant.Author; +import kr.suhsaechan.mapsy.place.dto.BookmarkDto; +import kr.suhsaechan.mapsy.place.dto.UpdateBookmarkRequest; +import kr.suhsaechan.suhapilog.annotation.ApiLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; + +public interface BookmarkControllerDocs { + + @ApiLog(date = "2026.01.28", author = Author.SUHSAECHAN, issueNumber = 19, description = "북마크 목록 조회 API 추가") + @Operation(summary = "북마크 목록 조회", description = """ + ## 인증(JWT): **필요** + + ## 요청 파라미터 (Query Parameters) + - **`page`**: 페이지 번호 (0부터 시작, 기본값: 0) + - **`size`**: 페이지 크기 (기본값: 20) + - **`sort`**: 정렬 기준 (예: savedAt,desc) + + ## 반환값 (Page) + - **`content`**: 북마크 목록 + - **`memberPlaceId`**: MemberPlace ID + - **`place`**: 장소 정보 (PlaceDto) + - **`folder`**: 폴더명 + - **`memo`**: 메모 + - **`rating`**: 별점 (1-5) + - **`visited`**: 방문 여부 + - **`visitedAt`**: 방문 일시 + - **`savedAt`**: 저장 일시 + - **`totalElements`**: 전체 북마크 수 + - **`totalPages`**: 전체 페이지 수 + - **`size`**: 페이지 크기 + - **`number`**: 현재 페이지 번호 + + ## 특이사항 + - 저장된 상태(SAVED)의 북마크만 조회됩니다. + - 기본적으로 저장일 기준 최신순으로 정렬됩니다. + + ## 에러코드 + - **`MEMBER_NOT_FOUND`**: 회원을 찾을 수 없습니다. + """) + ResponseEntity> getBookmarks( + CustomUserDetails userDetails, + Pageable pageable + ); + + @ApiLog(date = "2026.01.28", author = Author.SUHSAECHAN, issueNumber = 19, description = "북마크 수정 API 추가") + @Operation(summary = "북마크 수정", description = """ + ## 인증(JWT): **필요** + + ## 요청 파라미터 + - **`memberPlaceId`**: 수정할 MemberPlace ID (필수, Path Variable) + + ## 요청 바디 (UpdateBookmarkRequest) + - **`folder`**: 폴더명 (null이면 변경하지 않음) + - **`memo`**: 메모 (null이면 변경하지 않음) + - **`rating`**: 별점 1-5 (null이면 변경하지 않음) + - **`visited`**: 방문 여부 (null이면 변경하지 않음) + - **`visitedAt`**: 방문 일시 (null이면 visited=true 시 현재 시간) + + ## 반환값 (BookmarkDto) + - **`memberPlaceId`**: MemberPlace ID + - **`place`**: 장소 정보 (PlaceDto) + - **`folder`**: 폴더명 + - **`memo`**: 메모 + - **`rating`**: 별점 (1-5) + - **`visited`**: 방문 여부 + - **`visitedAt`**: 방문 일시 + - **`savedAt`**: 저장 일시 + + ## 특이사항 + - 저장된 상태(SAVED)의 북마크만 수정 가능합니다. + - null 값인 필드는 변경되지 않습니다 (부분 업데이트). + + ## 에러코드 + - **`MEMBER_PLACE_NOT_FOUND`**: 회원의 장소 정보를 찾을 수 없습니다. + - **`CANNOT_UPDATE_UNSAVED_PLACE`**: 저장된 장소만 수정할 수 있습니다. + - **`INVALID_RATING`**: 별점은 1-5 사이의 값이어야 합니다. + """) + ResponseEntity updateBookmark( + CustomUserDetails userDetails, + UUID memberPlaceId, + UpdateBookmarkRequest request + ); +} diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FeedController.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FeedController.java new file mode 100644 index 0000000..e926f68 --- /dev/null +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FeedController.java @@ -0,0 +1,93 @@ +package kr.suhsaechan.mapsy.web.controller; + +import java.util.List; +import kr.suhsaechan.mapsy.auth.dto.CustomUserDetails; +import kr.suhsaechan.mapsy.place.dto.KeywordDto; +import kr.suhsaechan.mapsy.place.dto.PlaceDto; +import kr.suhsaechan.mapsy.place.service.FeedService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@Slf4j +@RequestMapping("/api/feed") +public class FeedController implements FeedControllerDocs { + + private final FeedService feedService; + + /** + * 최신 장소 피드 조회 + */ + @GetMapping("/latest") + @Override + public ResponseEntity> getLatestPlaces( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable + ) { + log.info("Get latest places feed: page={}, size={}", + pageable.getPageNumber(), pageable.getPageSize()); + + Page response = feedService.getLatestPlaces(pageable); + + return ResponseEntity.ok(response); + } + + /** + * 인기 장소 피드 조회 + */ + @GetMapping("/popular") + @Override + public ResponseEntity> getPopularPlaces( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PageableDefault(size = 20) Pageable pageable + ) { + log.info("Get popular places feed: page={}, size={}", + pageable.getPageNumber(), pageable.getPageSize()); + + Page response = feedService.getPopularPlaces(pageable); + + return ResponseEntity.ok(response); + } + + /** + * 내 TOP 저장 장소 조회 + */ + @GetMapping("/my-top") + @Override + public ResponseEntity> getMyTopPlaces( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + log.info("Get my top places for member: {}", userDetails.getMemberId()); + + List response = feedService.getMyTopPlaces(userDetails.getMemberId()); + + return ResponseEntity.ok(response); + } + + /** + * 트렌드 키워드 조회 + */ + @GetMapping("/trending-keywords") + @Override + public ResponseEntity> getTrendingKeywords( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(defaultValue = "20") int size + ) { + log.info("Get trending keywords: size={}", size); + + List response = feedService.getTrendingKeywords(size); + + return ResponseEntity.ok(response); + } +} diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FeedControllerDocs.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FeedControllerDocs.java new file mode 100644 index 0000000..b8b9b87 --- /dev/null +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FeedControllerDocs.java @@ -0,0 +1,122 @@ +package kr.suhsaechan.mapsy.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import java.util.List; +import kr.suhsaechan.mapsy.auth.dto.CustomUserDetails; +import kr.suhsaechan.mapsy.common.constant.Author; +import kr.suhsaechan.mapsy.place.dto.KeywordDto; +import kr.suhsaechan.mapsy.place.dto.PlaceDto; +import kr.suhsaechan.suhapilog.annotation.ApiLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; + +public interface FeedControllerDocs { + + @ApiLog(date = "2026.01.28", author = Author.SUHSAECHAN, issueNumber = 19, description = "최신 장소 피드 API 추가") + @Operation(summary = "최신 장소 피드 조회", description = """ + ## 인증(JWT): **필요** + + ## 요청 파라미터 (Query Parameters) + - **`page`**: 페이지 번호 (0부터 시작, 기본값: 0) + - **`size`**: 페이지 크기 (기본값: 20) + + ## 반환값 (Page) + - **`content`**: 장소 목록 + - **`placeId`**: 장소 ID + - **`name`**: 장소명 + - **`address`**: 주소 + - **`rating`**: 별점 (0.0 ~ 5.0) + - **`userRatingsTotal`**: 리뷰 수 + - **`photoUrls`**: 사진 URL 배열 + - **`description`**: 장소 요약 설명 + - **`totalElements`**: 전체 장소 수 + - **`totalPages`**: 전체 페이지 수 + - **`size`**: 페이지 크기 + - **`number`**: 현재 페이지 번호 + + ## 특이사항 + - 생성일 기준 최신순으로 정렬됩니다. + - 홈 피드에서 무한 스크롤에 사용됩니다. + """) + ResponseEntity> getLatestPlaces( + CustomUserDetails userDetails, + Pageable pageable + ); + + @ApiLog(date = "2026.01.28", author = Author.SUHSAECHAN, issueNumber = 19, description = "인기 장소 피드 API 추가") + @Operation(summary = "인기 장소 피드 조회", description = """ + ## 인증(JWT): **필요** + + ## 요청 파라미터 (Query Parameters) + - **`page`**: 페이지 번호 (0부터 시작, 기본값: 0) + - **`size`**: 페이지 크기 (기본값: 20) + + ## 반환값 (Page) + - **`content`**: 장소 목록 + - **`placeId`**: 장소 ID + - **`name`**: 장소명 + - **`address`**: 주소 + - **`rating`**: 별점 (0.0 ~ 5.0) + - **`userRatingsTotal`**: 리뷰 수 + - **`photoUrls`**: 사진 URL 배열 + - **`description`**: 장소 요약 설명 + - **`totalElements`**: 전체 장소 수 + - **`totalPages`**: 전체 페이지 수 + + ## 특이사항 + - 저장 횟수 기준 내림차순으로 정렬됩니다. + - 같은 저장 횟수의 경우 생성일 기준 최신순으로 정렬됩니다. + """) + ResponseEntity> getPopularPlaces( + CustomUserDetails userDetails, + Pageable pageable + ); + + @ApiLog(date = "2026.01.28", author = Author.SUHSAECHAN, issueNumber = 19, description = "내 TOP 저장 장소 API 추가") + @Operation(summary = "내 TOP 저장 장소 조회", description = """ + ## 인증(JWT): **필요** + + ## 반환값 (List) + - **`placeId`**: 장소 ID + - **`name`**: 장소명 + - **`address`**: 주소 + - **`rating`**: 별점 (0.0 ~ 5.0) + - **`userRatingsTotal`**: 리뷰 수 + - **`photoUrls`**: 사진 URL 배열 + - **`description`**: 장소 요약 설명 + + ## 특이사항 + - 최대 10개의 장소를 반환합니다. + - 저장일 기준 최신순으로 정렬됩니다. + - 저장된 상태(SAVED)의 장소만 포함됩니다. + + ## 에러코드 + - **`MEMBER_NOT_FOUND`**: 회원을 찾을 수 없습니다. + """) + ResponseEntity> getMyTopPlaces( + CustomUserDetails userDetails + ); + + @ApiLog(date = "2026.01.28", author = Author.SUHSAECHAN, issueNumber = 19, description = "트렌드 키워드 API 추가") + @Operation(summary = "트렌드 키워드 조회", description = """ + ## 인증(JWT): **필요** + + ## 요청 파라미터 (Query Parameters) + - **`size`**: 조회 개수 (기본값: 20) + + ## 반환값 (List) + - **`id`**: 키워드 ID + - **`keyword`**: 키워드 문자열 + - **`count`**: 사용 횟수 + - **`trendScore`**: 트렌드 점수 + + ## 특이사항 + - 트렌드 점수 기준 내림차순으로 정렬됩니다. + - 홈 화면의 "떠오르는 키워드" 섹션에 사용됩니다. + """) + ResponseEntity> getTrendingKeywords( + CustomUserDetails userDetails, + int size + ); +} diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderController.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderController.java index 1781be5..0cfea9b 100644 --- a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderController.java +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderController.java @@ -10,6 +10,7 @@ import kr.suhsaechan.mapsy.place.dto.UpdateFolderRequest; import kr.suhsaechan.mapsy.place.dto.UpdateFolderResponse; import kr.suhsaechan.mapsy.place.service.FolderService; +import jakarta.validation.Valid; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,12 +24,14 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @Slf4j @RequestMapping("/api/folders") +@Tag(name = "폴더 관리 API", description = "폴더 CRUD 및 폴더-장소 관리 관련 API 제공") public class FolderController implements FolderControllerDocs { private final FolderService folderService; @@ -47,7 +50,7 @@ public ResponseEntity getFolders( @Override public ResponseEntity createFolder( @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody CreateFolderRequest request + @Valid @RequestBody CreateFolderRequest request ) { log.info("Create folder request from member: {}", userDetails.getMemberId()); CreateFolderResponse response = folderService.createFolder(userDetails.getMemberId(), request); @@ -59,7 +62,7 @@ public ResponseEntity createFolder( public ResponseEntity updateFolder( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable UUID folderId, - @RequestBody UpdateFolderRequest request + @Valid @RequestBody UpdateFolderRequest request ) { log.info("Update folder request from member: {}, folderId: {}", userDetails.getMemberId(), folderId); UpdateFolderResponse response = folderService.updateFolder(userDetails.getMemberId(), folderId, request); diff --git a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderControllerDocs.java b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderControllerDocs.java index 5533072..d038044 100644 --- a/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderControllerDocs.java +++ b/MS-Web/src/main/java/kr/suhsaechan/mapsy/web/controller/FolderControllerDocs.java @@ -1,6 +1,7 @@ package kr.suhsaechan.mapsy.web.controller; import kr.suhsaechan.mapsy.auth.dto.CustomUserDetails; +import kr.suhsaechan.mapsy.common.constant.Author; import kr.suhsaechan.mapsy.place.dto.AddFolderPlaceRequest; import kr.suhsaechan.mapsy.place.dto.AddFolderPlaceResponse; import kr.suhsaechan.mapsy.place.dto.CreateFolderRequest; @@ -11,10 +12,12 @@ import kr.suhsaechan.mapsy.place.dto.UpdateFolderResponse; import io.swagger.v3.oas.annotations.Operation; import java.util.UUID; +import kr.suhsaechan.suhapilog.annotation.ApiLog; import org.springframework.http.ResponseEntity; public interface FolderControllerDocs { + @ApiLog(date = "2026.02.23", author = Author.SUHSAECHAN, issueNumber = 26, description = "폴더 목록 조회 API 구현") @Operation(summary = "내 폴더 목록 조회", description = """ ## 인증(JWT): **필요** @@ -42,6 +45,7 @@ ResponseEntity getFolders( CustomUserDetails userDetails ); + @ApiLog(date = "2026.02.23", author = Author.SUHSAECHAN, issueNumber = 26, description = "폴더 생성 API 구현") @Operation(summary = "폴더 생성", description = """ ## 인증(JWT): **필요** @@ -67,6 +71,7 @@ ResponseEntity createFolder( CreateFolderRequest request ); + @ApiLog(date = "2026.02.23", author = Author.SUHSAECHAN, issueNumber = 26, description = "폴더 수정 API 구현") @Operation(summary = "폴더 수정", description = """ ## 인증(JWT): **필요** @@ -95,6 +100,7 @@ ResponseEntity updateFolder( UpdateFolderRequest request ); + @ApiLog(date = "2026.02.23", author = Author.SUHSAECHAN, issueNumber = 26, description = "폴더 삭제 API 구현") @Operation(summary = "폴더 삭제", description = """ ## 인증(JWT): **필요** @@ -119,6 +125,7 @@ ResponseEntity deleteFolder( UUID folderId ); + @ApiLog(date = "2026.02.23", author = Author.SUHSAECHAN, issueNumber = 26, description = "폴더 내 장소 목록 조회 API 구현") @Operation(summary = "폴더 내 장소 목록 조회", description = """ ## 인증(JWT): **필요** @@ -150,6 +157,7 @@ ResponseEntity getFolderPlaces( UUID folderId ); + @ApiLog(date = "2026.02.23", author = Author.SUHSAECHAN, issueNumber = 26, description = "폴더에 장소 추가 API 구현") @Operation(summary = "폴더에 장소 추가", description = """ ## 인증(JWT): **필요** @@ -181,6 +189,7 @@ ResponseEntity addPlaceToFolder( AddFolderPlaceRequest request ); + @ApiLog(date = "2026.02.23", author = Author.SUHSAECHAN, issueNumber = 26, description = "폴더에서 장소 제거 API 구현") @Operation(summary = "폴더에서 장소 제거", description = """ ## 인증(JWT): **필요** diff --git a/README.md b/README.md index 3bebde9..066bff5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -## 최신 버전 : v0.1.25 (2026-02-10) +## 최신 버전 : v0.1.26 (2026-02-23) [전체 버전 기록 보기](CHANGELOG.md) diff --git a/build.gradle b/build.gradle index 86f958d..38b1cab 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ bootJar { allprojects { group = 'kr.suhsaechan.mapsy' - version = '0.1.26' + version = '0.1.28' repositories { mavenCentral() diff --git a/version.yml b/version.yml index a7edbae..4f21ba5 100644 --- a/version.yml +++ b/version.yml @@ -33,11 +33,11 @@ # - project_type은 최초 설정 후 변경하지 마세요 # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "0.1.26" -version_code: 30 # app build number +version: "0.1.28" +version_code: 32 # app build number project_type: "spring" # spring, flutter, next, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-02-23 04:57:46" + last_updated: "2026-02-23 05:07:22" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE"