diff --git a/src/docs/asciidoc/gallery.adoc b/src/docs/asciidoc/gallery.adoc new file mode 100644 index 00000000..fbc33165 --- /dev/null +++ b/src/docs/asciidoc/gallery.adoc @@ -0,0 +1,28 @@ +== 갤러리 API +:source-highlighter: highlightjs + +--- +=== 갤러리 게시글 생성 (POST /galleries) +==== +operation::gallery-controller-test/create-gallery[snippets="http-request,request-fields,http-response,response-fields"] +==== + +=== 갤러리 게시글 목록 조회 (GET /galleries) +==== +operation::gallery-controller-test/get-galleries[snippets="http-request,query-parameters,http-response,response-fields"] +==== + +=== 갤러리 게시글 조회 (GET /galleries/{galleryId}) +==== +operation::gallery-controller-test/get-gallery[snippets="http-request,path-parameters,http-response,response-fields"] +==== + +=== 갤러리 게시글 삭제 (DELETE /galleries/{galleryId}) +==== +operation::gallery-controller-test/delete-gallery[snippets="http-request,path-parameters"] +==== + +=== 갤러리 게시글 수정 (PUT /galleries/{galleryId}) +==== +operation::gallery-controller-test/update-gallery[snippets="http-request,path-parameters,request-fields,http-response,response-fields"] +==== \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index b743a04c..3f28438c 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -14,3 +14,4 @@ include::talk.adoc[] include::notice.adoc[] include::eventNotice.adoc[] include::eventPeriod.adoc[] +include::gallery.adoc[] \ No newline at end of file diff --git a/src/main/java/com/scg/stop/domain/file/domain/File.java b/src/main/java/com/scg/stop/domain/file/domain/File.java index cbf74aa1..009a1613 100644 --- a/src/main/java/com/scg/stop/domain/file/domain/File.java +++ b/src/main/java/com/scg/stop/domain/file/domain/File.java @@ -4,7 +4,7 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; -import com.scg.stop.domain.gallery.domain.Gallery; +import com.scg.stop.gallery.domain.Gallery; import com.scg.stop.notice.domain.Notice; import com.scg.stop.event.domain.EventNotice; import com.scg.stop.global.domain.BaseTimeEntity; @@ -54,4 +54,8 @@ public void setNotice(Notice notice) { public void setEventNotice(EventNotice eventNotice) { this.eventNotice = eventNotice; } + + public void setGallery(Gallery gallery) { + this.gallery = gallery; + } } \ No newline at end of file diff --git a/src/main/java/com/scg/stop/domain/gallery/domain/Gallery.java b/src/main/java/com/scg/stop/domain/gallery/domain/Gallery.java deleted file mode 100644 index aa4e9bff..00000000 --- a/src/main/java/com/scg/stop/domain/gallery/domain/Gallery.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.scg.stop.domain.gallery.domain; - -import static jakarta.persistence.FetchType.LAZY; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import com.scg.stop.domain.file.domain.File; -import com.scg.stop.global.domain.BaseTimeEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = PROTECTED) -public class Gallery extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - private Long id; - - - @Column(nullable = false) - private String title; - - @Column(nullable = false, columnDefinition = "TEXT") - private String content; - - @Column(nullable = false) - private Integer year; - - @Column(nullable = false) - private Integer month; - - @OneToMany(fetch = LAZY, mappedBy = "gallery") - private List files = new ArrayList<>(); -} diff --git a/src/main/java/com/scg/stop/gallery/controller/GalleryController.java b/src/main/java/com/scg/stop/gallery/controller/GalleryController.java new file mode 100644 index 00000000..82ede5cc --- /dev/null +++ b/src/main/java/com/scg/stop/gallery/controller/GalleryController.java @@ -0,0 +1,67 @@ +package com.scg.stop.gallery.controller; + +import com.scg.stop.auth.annotation.AuthUser; +import com.scg.stop.gallery.dto.request.GalleryRequest; +import com.scg.stop.gallery.dto.response.GalleryResponse; +import com.scg.stop.gallery.service.GalleryService; +import com.scg.stop.user.domain.AccessType; +import com.scg.stop.user.domain.User; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/galleries") +public class GalleryController { + + private final GalleryService galleryService; + + @PostMapping + public ResponseEntity createGallery( + @RequestBody @Valid GalleryRequest galleryRequest, + @AuthUser(accessType = {AccessType.ADMIN}) User user + ) { + GalleryResponse galleryResponse = galleryService.createGallery(galleryRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(galleryResponse); + } + + @GetMapping + public ResponseEntity> getGalleries( + @RequestParam(value = "year", required = false) Integer year, + @RequestParam(value = "month", required = false) Integer month, + @PageableDefault(page = 0, size = 10) Pageable pageable) { + Page galleries = galleryService.getGalleries(year, month, pageable); + return ResponseEntity.ok(galleries); + } + + @GetMapping("/{galleryId}") + public ResponseEntity getGallery(@PathVariable("galleryId") Long galleryId) { + GalleryResponse galleryResponse = galleryService.getGallery(galleryId); + return ResponseEntity.ok(galleryResponse); + } + + @PutMapping("/{galleryId}") + public ResponseEntity updateGallery( + @PathVariable("galleryId") Long galleryId, + @RequestBody @Valid GalleryRequest galleryRequest, + @AuthUser(accessType = {AccessType.ADMIN}) User user + ) { + GalleryResponse galleryResponse = galleryService.updateGallery(galleryId, galleryRequest); + return ResponseEntity.ok(galleryResponse); + } + + @DeleteMapping("/{galleryId}") + public ResponseEntity deleteGallery( + @PathVariable("galleryId") Long galleryId, + @AuthUser(accessType = {AccessType.ADMIN}) User user + ) { + galleryService.deleteGallery(galleryId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/scg/stop/gallery/domain/Gallery.java b/src/main/java/com/scg/stop/gallery/domain/Gallery.java new file mode 100644 index 00000000..6f0bcbfe --- /dev/null +++ b/src/main/java/com/scg/stop/gallery/domain/Gallery.java @@ -0,0 +1,64 @@ +package com.scg.stop.gallery.domain; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import com.scg.stop.domain.file.domain.File; +import com.scg.stop.global.domain.BaseTimeEntity; +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +public class Gallery extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private Integer year; + + @Column(nullable = false) + private Integer month; + + @Column(nullable = false) + private Integer hitCount = 0; + + @OneToMany(fetch = LAZY, mappedBy = "gallery", cascade = CascadeType.ALL, orphanRemoval = true) + private List files = new ArrayList<>(); + + private Gallery(String title, Integer year, Integer month, List files) { + this.title = title; + this.year = year; + this.month = month; + this.files = files; + files.forEach(file -> file.setGallery(this)); + } + + public static Gallery of(String title, Integer year, Integer month, List files) { + return new Gallery(title, year, month, files); + } + + public void update(String title, Integer year, Integer month, List files) { + this.title = title; + this.year = year; + this.month = month; + this.files.clear(); + this.files.addAll(files); + files.forEach(file -> file.setGallery(this)); + } + + public void increaseHitCount() { + this.hitCount += 1; + } +} diff --git a/src/main/java/com/scg/stop/gallery/dto/request/GalleryRequest.java b/src/main/java/com/scg/stop/gallery/dto/request/GalleryRequest.java new file mode 100644 index 00000000..51adfeb0 --- /dev/null +++ b/src/main/java/com/scg/stop/gallery/dto/request/GalleryRequest.java @@ -0,0 +1,31 @@ +package com.scg.stop.gallery.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +import static lombok.AccessLevel.PRIVATE; + +@Getter +@NoArgsConstructor(access = PRIVATE) +@AllArgsConstructor +public class GalleryRequest { + + @NotBlank(message = "제목을 입력해주세요.") + private String title; + + @NotNull(message = "연도를 입력해주세요.") + private Integer year; + + @NotNull(message = "월을 입력해주세요.") + private Integer month; + + @Size(min = 1, message = "1개 이상의 파일을 첨부해야 합니다.") + @NotNull(message = "파일을 첨부해주세요.") + private List fileIds; +} \ No newline at end of file diff --git a/src/main/java/com/scg/stop/gallery/dto/response/GalleryResponse.java b/src/main/java/com/scg/stop/gallery/dto/response/GalleryResponse.java new file mode 100644 index 00000000..a3cdca55 --- /dev/null +++ b/src/main/java/com/scg/stop/gallery/dto/response/GalleryResponse.java @@ -0,0 +1,48 @@ +package com.scg.stop.gallery.dto.response; + +import static lombok.AccessLevel.PRIVATE; + +import com.scg.stop.domain.file.dto.response.FileResponse; +import com.scg.stop.gallery.domain.Gallery; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = PRIVATE) +@AllArgsConstructor +public class GalleryResponse { + + private Long id; + private String title; + private int year; + private int month; + private int hitCount; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private List files; + + public static GalleryResponse of(Gallery gallery, List fileResponses) { + return new GalleryResponse( + gallery.getId(), + gallery.getTitle(), + gallery.getYear(), + gallery.getMonth(), + gallery.getHitCount(), + gallery.getCreatedAt(), + gallery.getUpdatedAt(), + fileResponses + ); + } + + public static GalleryResponse from(Gallery gallery) { + List fileResponses = gallery.getFiles().stream() + .map(FileResponse::from) + .collect(Collectors.toList()); + return GalleryResponse.of(gallery, fileResponses); + } +} diff --git a/src/main/java/com/scg/stop/gallery/repository/GalleryRepository.java b/src/main/java/com/scg/stop/gallery/repository/GalleryRepository.java new file mode 100644 index 00000000..d978440f --- /dev/null +++ b/src/main/java/com/scg/stop/gallery/repository/GalleryRepository.java @@ -0,0 +1,22 @@ +package com.scg.stop.gallery.repository; + +import com.scg.stop.gallery.domain.Gallery; +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; +import org.springframework.stereotype.Repository; + +@Repository +public interface GalleryRepository extends JpaRepository { + + @Query("SELECT g FROM Gallery g " + + "WHERE (:year IS NULL OR g.year = :year) " + + "AND (:month IS NULL OR g.month = :month)") + Page findGalleries( + @Param("year") Integer year, + @Param("month") Integer month, + Pageable pageable + ); +} diff --git a/src/main/java/com/scg/stop/gallery/service/GalleryService.java b/src/main/java/com/scg/stop/gallery/service/GalleryService.java new file mode 100644 index 00000000..d306d3bc --- /dev/null +++ b/src/main/java/com/scg/stop/gallery/service/GalleryService.java @@ -0,0 +1,76 @@ +package com.scg.stop.gallery.service; + +import static com.scg.stop.global.exception.ExceptionCode.NOT_FOUND_FILE_ID; +import static com.scg.stop.global.exception.ExceptionCode.NOT_FOUND_GALLERY_ID; + +import com.scg.stop.domain.file.domain.File; +import com.scg.stop.domain.file.repository.FileRepository; +import com.scg.stop.gallery.domain.Gallery; +import com.scg.stop.gallery.dto.request.GalleryRequest; +import com.scg.stop.gallery.dto.response.GalleryResponse; +import com.scg.stop.gallery.repository.GalleryRepository; +import com.scg.stop.global.exception.BadRequestException; +import java.util.List; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class GalleryService { + + private final GalleryRepository galleryRepository; + private final FileRepository fileRepository; + + public GalleryResponse createGallery(GalleryRequest request) { + List files = fileRepository.findByIdIn(request.getFileIds()); + if (files.size() != request.getFileIds().size()) { + throw new BadRequestException(NOT_FOUND_FILE_ID); + } + + Gallery gallery = Gallery.of(request.getTitle(), request.getYear(), request.getMonth(), files); + Gallery savedGallery = galleryRepository.save(gallery); + + return GalleryResponse.from(savedGallery); + } + + @Transactional(readOnly = true) + public Page getGalleries(Integer year, Integer month, Pageable pageable) { + Page galleries = galleryRepository.findGalleries(year, month, pageable); + return galleries.map(GalleryResponse::from); + } + + public GalleryResponse getGallery(Long galleryId) { + Gallery gallery = galleryRepository.findById(galleryId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_GALLERY_ID)); + + gallery.increaseHitCount(); + + return GalleryResponse.from(gallery); + } + + public GalleryResponse updateGallery(Long galleryId, GalleryRequest request) { + Gallery gallery = galleryRepository.findById(galleryId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_GALLERY_ID)); + + List files = fileRepository.findByIdIn(request.getFileIds()); + if (files.size() != request.getFileIds().size()) { + throw new BadRequestException(NOT_FOUND_FILE_ID); + } + + gallery.update(request.getTitle(), request.getYear(), request.getMonth(), files); + Gallery savedGallery = galleryRepository.save(gallery); + + return GalleryResponse.from(savedGallery); + } + + public void deleteGallery(Long galleryId) { + Gallery gallery = galleryRepository.findById(galleryId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_GALLERY_ID)); + galleryRepository.delete(gallery); + } +} \ No newline at end of file diff --git a/src/main/java/com/scg/stop/global/exception/ExceptionCode.java b/src/main/java/com/scg/stop/global/exception/ExceptionCode.java index fad2a19c..3a58e294 100644 --- a/src/main/java/com/scg/stop/global/exception/ExceptionCode.java +++ b/src/main/java/com/scg/stop/global/exception/ExceptionCode.java @@ -34,8 +34,10 @@ public enum ExceptionCode { ID_NOT_FOUND(8200,"해당 ID에 해당하는 잡페어 인터뷰가 없습니다."), TALK_ID_NOT_FOUND(8400, "해당 ID에 해당하는 대담 영상이 없습니다."), - NO_QUIZ(8401, "퀴즈 데이터가 존재하지 않습니다."); + NO_QUIZ(8401, "퀴즈 데이터가 존재하지 않습니다."), + NOT_FOUND_FILE_ID(5001, "요청한 ID에 해당하는 파일이 존재하지 않습니다."), + NOT_FOUND_GALLERY_ID(9001, "요청한 ID에 해당하는 갤러리가 존재하지 않습니다."); private final int code; private final String message; diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 83de3bda..8b4e9460 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -15,5 +15,8 @@ spring: hibernate: default_batch_fetch_size: 15 +logging.level: + org.hibernate.SQL: debug + org.hibernate.orm.jdbc.bind: trace server.port: 8000 \ No newline at end of file diff --git a/src/test/java/com/scg/stop/gallery/controller/GalleryControllerTest.java b/src/test/java/com/scg/stop/gallery/controller/GalleryControllerTest.java new file mode 100644 index 00000000..9b807bab --- /dev/null +++ b/src/test/java/com/scg/stop/gallery/controller/GalleryControllerTest.java @@ -0,0 +1,348 @@ +package com.scg.stop.gallery.controller; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scg.stop.configuration.AbstractControllerTest; +import com.scg.stop.domain.file.dto.response.FileResponse; +import com.scg.stop.gallery.dto.request.GalleryRequest; +import com.scg.stop.gallery.dto.response.GalleryResponse; +import com.scg.stop.gallery.service.GalleryService; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(controllers = GalleryController.class) +@MockBean(JpaMetamodelMappingContext.class) +@AutoConfigureRestDocs +class GalleryControllerTest extends AbstractControllerTest { + + private static final String ACCESS_TOKEN = "admin_access_token"; + private static final String REFRESH_TOKEN = "refresh_token"; + + @MockBean + private GalleryService galleryService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("갤러리 게시글을 생성할 수 있다.") + void createGallery() throws Exception { + + // given + List fileIds = Arrays.asList(1L, 2L, 3L); + List fileResponses = Arrays.asList( + new FileResponse(1L, "014eb8a0-d4a6-11ee-adac-117d766aca1d", "사진1.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(2L, "11a480c0-13fa-11ef-9047-570191b390ea", "사진2.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(3L, "1883fc70-cfb4-11ee-a387-e754bd392d45", "사진3.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()) + ); + GalleryRequest request = new GalleryRequest("새내기 배움터", 2024, 4, fileIds); + GalleryResponse response = new GalleryResponse( + 1L, + "새내기 배움터", + 2024, + 4, + 1, + LocalDateTime.now(), + LocalDateTime.now(), + fileResponses + ); + when(galleryService.createGallery(any(GalleryRequest.class))).thenReturn(response); + + // when + ResultActions result = mockMvc.perform( + post("/galleries") + .header(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN) + .cookie(new Cookie("refresh-token", REFRESH_TOKEN)) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + result.andExpect(status().isCreated()) + .andDo(restDocs.document( + requestCookies( + cookieWithName("refresh-token").description("갱신 토큰") + ), + requestHeaders( + headerWithName("Authorization").description("access token") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("year").type(JsonFieldType.NUMBER).description("연도"), + fieldWithPath("month").type(JsonFieldType.NUMBER).description("월"), + fieldWithPath("fileIds").type(JsonFieldType.ARRAY).description("파일 ID 리스트") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("갤러리 ID"), + fieldWithPath("title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("year").type(JsonFieldType.NUMBER).description("연도"), + fieldWithPath("month").type(JsonFieldType.NUMBER).description("월"), + fieldWithPath("hitCount").type(JsonFieldType.NUMBER).description("조회수"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("생성일"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("수정일"), + fieldWithPath("files").type(JsonFieldType.ARRAY).description("파일 목록"), + fieldWithPath("files[].id").type(JsonFieldType.NUMBER).description("파일 ID"), + fieldWithPath("files[].uuid").type(JsonFieldType.STRING).description("파일 UUID"), + fieldWithPath("files[].name").type(JsonFieldType.STRING).description("파일 이름"), + fieldWithPath("files[].mimeType").type(JsonFieldType.STRING).description("파일 MIME 타입"), + fieldWithPath("files[].createdAt").type(JsonFieldType.STRING).description("파일 생성일"), + fieldWithPath("files[].updatedAt").type(JsonFieldType.STRING).description("파일 수정일") + ) + )); + } + + @Test + @DisplayName("갤러리 게시글 목록을 조회할 수 있다.") + void getGalleries() throws Exception { + + // given + List fileResponses = Arrays.asList( + new FileResponse(1L, "014eb8a0-d4a6-11ee-adac-117d766aca1d", "사진1.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(2L, "11a480c0-13fa-11ef-9047-570191b390ea", "사진2.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(3L, "1883fc70-cfb4-11ee-a387-e754bd392d45", "사진3.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()) + ); + GalleryResponse galleryResponse = new GalleryResponse( + 1L, + "새내기 배움터", + 2024, + 4, + 0, // getGalleries 에선 조회수를 증가시키지 않음 + LocalDateTime.now(), + LocalDateTime.now(), + fileResponses + ); + PageImpl galleryResponses = new PageImpl<>(Collections.singletonList(galleryResponse)); + when(galleryService.getGalleries(anyInt(), anyInt(), any(Pageable.class))).thenReturn(galleryResponses); + + // when + ResultActions result = mockMvc.perform( + get("/galleries") + .param("year", "2024") + .param("month", "4") + .contentType(APPLICATION_JSON) + ); + + // then + result.andExpect(status().isOk()) + .andDo(restDocs.document( + queryParameters( + parameterWithName("year").optional().description("연도"), + parameterWithName("month").optional().description("월"), + parameterWithName("page").optional().description("페이지 번호 [default: 0]"), + parameterWithName("size").optional().description("페이지 크기 [default: 10]") + ), + responseFields( + fieldWithPath("totalElements").type(JsonFieldType.NUMBER).description("전체 데이터 수"), + fieldWithPath("totalPages").type(JsonFieldType.NUMBER).description("전체 페이지 수"), + fieldWithPath("size").type(JsonFieldType.NUMBER).description("페이지당 데이터 수"), + fieldWithPath("content").type(JsonFieldType.ARRAY).description("갤러리 목록"), + fieldWithPath("content[].id").type(JsonFieldType.NUMBER).description("갤러리 ID"), + fieldWithPath("content[].title").type(JsonFieldType.STRING).description("갤러리 제목"), + fieldWithPath("content[].year").type(JsonFieldType.NUMBER).description("갤러리 연도"), + fieldWithPath("content[].month").type(JsonFieldType.NUMBER).description("갤러리 월"), + fieldWithPath("content[].hitCount").type(JsonFieldType.NUMBER).description("갤러리 조회수"), + fieldWithPath("content[].createdAt").type(JsonFieldType.STRING).description("갤러리 생성일"), + fieldWithPath("content[].updatedAt").type(JsonFieldType.STRING).description("갤러리 수정일"), + fieldWithPath("content[].files").type(JsonFieldType.ARRAY).description("파일 목록"), + fieldWithPath("content[].files[].id").type(JsonFieldType.NUMBER).description("파일 ID"), + fieldWithPath("content[].files[].uuid").type(JsonFieldType.STRING).description("파일 UUID"), + fieldWithPath("content[].files[].name").type(JsonFieldType.STRING).description("파일 이름"), + fieldWithPath("content[].files[].mimeType").type(JsonFieldType.STRING).description("파일 MIME 타입"), + fieldWithPath("content[].files[].createdAt").type(JsonFieldType.STRING).description("파일 생성일"), + fieldWithPath("content[].files[].updatedAt").type(JsonFieldType.STRING).description("파일 수정일"), + fieldWithPath("number").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("sort.empty").type(JsonFieldType.BOOLEAN).description("정렬 정보"), + fieldWithPath("sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬 정보"), + fieldWithPath("sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬 정보"), + fieldWithPath("first").type(JsonFieldType.BOOLEAN).description("첫 페이지 여부"), + fieldWithPath("last").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("numberOfElements").type(JsonFieldType.NUMBER).description("현재 페이지의 데이터 수"), + fieldWithPath("pageable").ignored(), + fieldWithPath("empty").type(JsonFieldType.BOOLEAN).description("빈 페이지 여부") + ) + )); + } + + @Test + @DisplayName("id로 갤러리 게시글을 조회할 수 있다.") + void getGallery() throws Exception { + + // given + List fileResponses = Arrays.asList( + new FileResponse(1L, "014eb8a0-d4a6-11ee-adac-117d766aca1d", "사진1.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(2L, "11a480c0-13fa-11ef-9047-570191b390ea", "사진2.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(3L, "1883fc70-cfb4-11ee-a387-e754bd392d45", "사진3.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()) + ); + GalleryResponse galleryResponse = new GalleryResponse( + 1L, + "새내기 배움터", + 2024, + 4, + 1, + LocalDateTime.now(), + LocalDateTime.now(), + fileResponses + ); + when(galleryService.getGallery(1L)).thenReturn(galleryResponse); + + // when + ResultActions result = mockMvc.perform( + get("/galleries/{galleryId}", 1L) + .contentType(APPLICATION_JSON) + ); + + // then + result.andExpect(status().isOk()) + .andDo(restDocs.document( + pathParameters( + parameterWithName("galleryId").description("조회할 갤러리 ID") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("갤러리 ID"), + fieldWithPath("title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("year").type(JsonFieldType.NUMBER).description("연도"), + fieldWithPath("month").type(JsonFieldType.NUMBER).description("월"), + fieldWithPath("hitCount").type(JsonFieldType.NUMBER).description("조회수"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("생성일"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("수정일"), + fieldWithPath("files").type(JsonFieldType.ARRAY).description("파일 목록"), + fieldWithPath("files[].id").type(JsonFieldType.NUMBER).description("파일 ID"), + fieldWithPath("files[].uuid").type(JsonFieldType.STRING).description("파일 UUID"), + fieldWithPath("files[].name").type(JsonFieldType.STRING).description("파일 이름"), + fieldWithPath("files[].mimeType").type(JsonFieldType.STRING).description("파일 MIME 타입"), + fieldWithPath("files[].createdAt").type(JsonFieldType.STRING).description("파일 생성일"), + fieldWithPath("files[].updatedAt").type(JsonFieldType.STRING).description("파일 수정일") + ) + )); + } + + @Test + @DisplayName("갤러리 게시글을 수정할 수 있다.") + void updateGallery() throws Exception { + + // given + List fileIds = Arrays.asList(1L, 2L, 3L); + List fileResponses = Arrays.asList( + new FileResponse(1L, "014eb8a0-d4a6-11ee-adac-117d766aca1d", "사진1.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(2L, "11a480c0-13fa-11ef-9047-570191b390ea", "사진2.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()), + new FileResponse(3L, "1883fc70-cfb4-11ee-a387-e754bd392d45", "사진3.jpg", "image/jpeg", LocalDateTime.now(), LocalDateTime.now()) + ); + GalleryRequest request = new GalleryRequest("수정된 제목", 2024, 5, fileIds); + GalleryResponse response = new GalleryResponse( + 1L, + "수정된 제목", + 2024, + 5, + 1, + LocalDateTime.now(), + LocalDateTime.now(), + fileResponses + ); + when(galleryService.updateGallery(anyLong(), any(GalleryRequest.class))).thenReturn(response); + + // when + ResultActions result = mockMvc.perform( + put("/galleries/{galleryId}", 1L) + .header(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN) + .cookie(new Cookie("refresh-token", REFRESH_TOKEN)) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + result.andExpect(status().isOk()) + .andDo(restDocs.document( + requestCookies( + cookieWithName("refresh-token").description("갱신 토큰") + ), + requestHeaders( + headerWithName("Authorization").description("access token") + ), + pathParameters( + parameterWithName("galleryId").description("수정할 갤러리 ID") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("year").type(JsonFieldType.NUMBER).description("연도"), + fieldWithPath("month").type(JsonFieldType.NUMBER).description("월"), + fieldWithPath("fileIds").type(JsonFieldType.ARRAY).description("파일 ID 리스트") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("갤러리 ID"), + fieldWithPath("title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("year").type(JsonFieldType.NUMBER).description("연도"), + fieldWithPath("month").type(JsonFieldType.NUMBER).description("월"), + fieldWithPath("hitCount").type(JsonFieldType.NUMBER).description("조회수"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("생성일"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("수정일"), + fieldWithPath("files").type(JsonFieldType.ARRAY).description("파일 목록"), + fieldWithPath("files[].id").type(JsonFieldType.NUMBER).description("파일 ID"), + fieldWithPath("files[].uuid").type(JsonFieldType.STRING).description("파일 UUID"), + fieldWithPath("files[].name").type(JsonFieldType.STRING).description("파일 이름"), + fieldWithPath("files[].mimeType").type(JsonFieldType.STRING).description("파일 MIME 타입"), + fieldWithPath("files[].createdAt").type(JsonFieldType.STRING).description("파일 생성일"), + fieldWithPath("files[].updatedAt").type(JsonFieldType.STRING).description("파일 수정일") + ) + )); + } + + @Test + @DisplayName("갤러리 게시글을 삭제할 수 있다.") + void deleteGallery() throws Exception { + + // given + Long galleryId = 1L; + doNothing().when(galleryService).deleteGallery(galleryId); + + // when + ResultActions result = mockMvc.perform( + delete("/galleries/{galleryId}", galleryId) + .header(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN) + .cookie(new Cookie("refresh-token", REFRESH_TOKEN)) + ); + + // then + result.andExpect(status().isNoContent()) + .andDo(restDocs.document( + requestCookies( + cookieWithName("refresh-token").description("갱신 토큰") + ), + requestHeaders( + headerWithName("Authorization").description("access token") + ), + pathParameters( + parameterWithName("galleryId").description("삭제할 갤러리 ID") + ) + )); + } +} \ No newline at end of file