Skip to content

Commit

Permalink
feature/#174 Carousel 관리자용 API 및 리스트 조회 API 구현 (#175)
Browse files Browse the repository at this point in the history
* feat: Carousel 저장 API

* feat: Carousel 삭제 API 구현 및 file 연관관계 REMOVE로 변경

* feat: Carousel 조회 API 구현

* refactor: 유효성 검증 추가

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor: 스키마 일치화

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor: Controller NPE 방지

* fix: Facade 계층 테스트 코드 작성

* fix: 도메인 계층 테스트 코드 작성

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
LeeHanEum and coderabbitai[bot] authored Jan 21, 2025
1 parent f1c4e66 commit e2fe9ca
Show file tree
Hide file tree
Showing 27 changed files with 774 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kgu.developers.admin.carousel.application;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import kgu.developers.admin.carousel.presentation.request.CarouselRequest;
import kgu.developers.admin.carousel.presentation.response.CarouselPersistResponse;
import kgu.developers.domain.carousel.application.command.CarouselCommandService;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class CarouselAdminFacade {
private final CarouselCommandService carouselCommandService;

@Transactional
public CarouselPersistResponse createCarousel(Long fileId, CarouselRequest request) {
Long id = carouselCommandService.createCarousel(fileId, request.text(), request.link());
return CarouselPersistResponse.of(id);
}

public void deleteCarousel(Long id) {
carouselCommandService.deleteCarouselById(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package kgu.developers.admin.carousel.presentation;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import kgu.developers.admin.carousel.presentation.request.CarouselRequest;
import kgu.developers.admin.carousel.presentation.response.CarouselPersistResponse;

@Tag(name = "Carousel", description = "캐러셀 관리자 API")
public interface CarouselAdminController {

@Operation(summary = "캐러셀 저장 API", description = """
- Description : 이 API는 캐러셀 이미지와 기타 정보를 저장합니다.
- Assignee : 이한음
""")
@ApiResponse(
responseCode = "201",
content = @Content(schema = @Schema(implementation = CarouselPersistResponse.class)))
ResponseEntity<CarouselPersistResponse> createCarousel(
@Parameter(
description = "캐러셀에 개시할 이미지의 ID 입니다.",
example = "1",
required = true
) @Positive @RequestParam Long fileId,
@Parameter(
description = "캐러셀 생성 request 객체 입니다."
) @Valid @RequestBody CarouselRequest request
);

@Operation(summary = "캐러셀 삭제 API", description = """
- Description : 이 API는 캐러셀을 삭제합니다.
- Assignee : 이한음
""")
@ApiResponse(responseCode = "204")
ResponseEntity<Void> deleteCarousel(
@Parameter(
description = "캐러셀 ID는 URL 경로 변수 입니다.",
example = "1",
required = true
) @Positive @PathVariable Long id
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kgu.developers.admin.carousel.presentation;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import kgu.developers.admin.carousel.application.CarouselAdminFacade;
import kgu.developers.admin.carousel.presentation.request.CarouselRequest;
import kgu.developers.admin.carousel.presentation.response.CarouselPersistResponse;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/carousels")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class CarouselAdminControllerImpl implements CarouselAdminController {
private final CarouselAdminFacade carouselAdminFacade;

@Override
@PostMapping
public ResponseEntity<CarouselPersistResponse> createCarousel(
@Positive @RequestParam Long fileId,
@Valid @RequestBody CarouselRequest request
) {
CarouselPersistResponse response = carouselAdminFacade.createCarousel(fileId, request);
return ResponseEntity.ok(response);
}

@Override
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCarousel(
@Positive @PathVariable Long id
) {
carouselAdminFacade.deleteCarousel(id);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kgu.developers.admin.carousel.presentation.request;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;

import org.hibernate.validator.constraints.URL;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;

public record CarouselRequest(
@Schema(description = "캐러셀 설명", example = "경기대학교 AI컴퓨터공학부 메인 이미지", requiredMode = NOT_REQUIRED)
@Size(max = 255, message = "설명은 255자를 초과할 수 없습니다")
String text,

@Schema(description = "캐러셀 이미지 링크", example = "https://www.kgu.ac.kr/", requiredMode = NOT_REQUIRED)
@URL(message = "올바른 URL 형식이어야 합니다")
String link
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kgu.developers.admin.carousel.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
public record CarouselPersistResponse(
@Schema(description = "캐러셀 ID", example = "1", requiredMode = REQUIRED)
Long id
) {
public static CarouselPersistResponse of(Long id) {
return CarouselPersistResponse.builder()
.id(id)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package carousel.application;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import kgu.developers.admin.carousel.application.CarouselAdminFacade;
import kgu.developers.admin.carousel.presentation.request.CarouselRequest;
import kgu.developers.admin.carousel.presentation.response.CarouselPersistResponse;
import kgu.developers.domain.carousel.application.command.CarouselCommandService;
import kgu.developers.domain.file.application.query.FileQueryService;
import kgu.developers.domain.file.domain.FileEntity;
import mock.repository.FakeCarouselRepository;
import mock.repository.FakeFileRepository;

public class CarouselAdminFacadeTest {
private CarouselAdminFacade carouselAdminFacade;

private static final Long TEST_FILE_ID = 1L;
private static final Long SAVE_TARGET_ID = 1L;

@BeforeEach
public void init() {
initializeCarouselAdminFacade();
}

private void initializeCarouselAdminFacade() {
FakeFileRepository fakeFileRepository = new FakeFileRepository();
carouselAdminFacade = new CarouselAdminFacade(
new CarouselCommandService(
new FileQueryService(fakeFileRepository),
new FakeCarouselRepository()
)
);
saveTestFile(fakeFileRepository);
}

private static void saveTestFile(FakeFileRepository fakeFileRepository) {
fakeFileRepository.save(
FileEntity.create(
"경기대학교 AI컴퓨터공학부 메인 이미지",
"/files/carousel/main_image.jpg",
1234L,
"image/jpeg"
)
);
}

@Test
@DisplayName("createCarousel은 Carousel을 생성한다")
public void createCarousel_Success() {
// given
CarouselRequest request = new CarouselRequest("경기대학교 AI컴퓨터공학부 메인 이미지", "https://www.kgu.ac.kr/");

// when
CarouselPersistResponse response = carouselAdminFacade.createCarousel(TEST_FILE_ID, request);

// then
assertEquals(SAVE_TARGET_ID, response.id());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void init() {

@Test
@DisplayName("getUsers는 유저 목록을 페이징해서 조회한다")
void getUsers_Success() {
public void getUsers_Success() {
// given
Pageable pageable = PageRequest.of(0, 10);

Expand All @@ -84,7 +84,7 @@ void getUsers_Success() {

@Test
@DisplayName("getUsers는 잘못된 페이지 요청시 빈 목록을 반환한다")
void getUsers_InvalidPage() {
public void getUsers_InvalidPage() {
// given
Pageable pageable = PageRequest.of(1, 10);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kgu.developers.api.carousel.application;

import java.util.List;

import org.springframework.stereotype.Component;

import kgu.developers.api.carousel.presentation.response.CarouselListResponse;
import kgu.developers.domain.carousel.application.query.CarouselQueryService;
import kgu.developers.domain.carousel.domain.Carousel;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class CarouselFacade {
private final CarouselQueryService carouselQueryService;

public CarouselListResponse getCarousels() {
List<Carousel> carousels = carouselQueryService.getAllCarousels();
return CarouselListResponse.from(carousels);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kgu.developers.api.carousel.presentation;

import org.springframework.http.ResponseEntity;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import kgu.developers.api.carousel.presentation.response.CarouselListResponse;

@Tag(name = "Carousel", description = "캐러셀 API")
public interface CarouselController {

@Operation(summary = "캐러셀 리스트 조회 API", description = """
- Description : 이 API는 모든 캐러셀 이미지와 기타 정보를 리스트로 조회합니다.
- Assignee : 이한음
""")
@ApiResponse(
responseCode = "200",
content = @Content(schema = @Schema(implementation = CarouselListResponse.class)))
ResponseEntity<CarouselListResponse> getCarousels();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kgu.developers.api.carousel.presentation;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import kgu.developers.api.carousel.application.CarouselFacade;
import kgu.developers.api.carousel.presentation.response.CarouselListResponse;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/carousels")
public class CarouselControllerImpl implements CarouselController {
private final CarouselFacade carouselFacade;

@Override
@GetMapping
public ResponseEntity<CarouselListResponse> getCarousels() {
CarouselListResponse response = carouselFacade.getCarousels();
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kgu.developers.api.carousel.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
import kgu.developers.domain.carousel.domain.Carousel;
import lombok.Builder;

@Builder
public record CarouselListResponse(
@Schema(description = "캐러셀 리스트",
example = """
[{
"id": 1,
"text": "경기대학교 AI컴퓨터공학부 메인 이미지",
"link": "https://www.kgu.ac.kr/",
"file": {
"id": 1,
"physicalPath": "/cloud/carousel/3/2025-curriculum"
}
}]
""",
requiredMode = REQUIRED)
List<CarouselResponse> contents
) {
public static CarouselListResponse from(List<Carousel> carousels) {
return CarouselListResponse.builder()
.contents(carousels.stream()
.map(CarouselResponse::from)
.toList())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package kgu.developers.api.carousel.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import kgu.developers.domain.carousel.domain.Carousel;
import kgu.developers.domain.file.application.response.FilePathResponse;
import lombok.Builder;

@Builder
public record CarouselResponse(
@Schema(description = "캐러셀 ID", example = "1", requiredMode = REQUIRED)
Long id,

@Schema(description = "캐러셀 설명", example = "경기대학교 AI컴퓨터공학부 메인 이미지", requiredMode = NOT_REQUIRED)
String text,

@Schema(description = "캐러셀 이미지 링크", example = "https://www.kgu.ac.kr/", requiredMode = NOT_REQUIRED)
String link,

@Schema(description = "첨부 파일 정보",
example = "{\"id\": 1, "
+ "\"physicalPath\": \"/files/2025-curriculum\"}",
requiredMode = NOT_REQUIRED)
FilePathResponse file
) {
public static CarouselResponse from(Carousel carousel) {
return CarouselResponse.builder()
.id(carousel.getId())
.text(carousel.getText())
.link(carousel.getLink())
.file(carousel.getFile() != null ? FilePathResponse.from(carousel.getFile()) : null)
.build();
}
}
Loading

0 comments on commit e2fe9ca

Please sign in to comment.