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/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/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-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): **필요**