feat: 코스발견 탭 상단 배너 조회 API 추가 - #209
Conversation
Banner 엔티티/Repository/Service/Controller 추가, GET /api/banner로 활성 배너를 sortOrder 순으로 반환. Android 연동은 후속 작업.
📝 WalkthroughWalkthroughAdded banner persistence, response DTOs, service mapping, and a ChangesBanner retrieval
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant BannerController
participant BannerService
participant BannerRepository
Client->>BannerController: GET /api/banner
BannerController->>BannerService: getBanners()
BannerService->>BannerRepository: query active banners by sort order
BannerRepository-->>BannerService: ordered banners
BannerService-->>BannerController: GetBannerResponseDto
BannerController-->>Client: HTTP 200 ApiResponseDto
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/org/runnect/server/banner/dto/response/GetBannerResponseDto.java`:
- Around line 13-17: Update BannerService to return List<BannerResponse>
directly and pass that list as the ApiResponseDto data payload, matching the
declared array response shape. Remove GetBannerResponseDto and its factory usage
so responses are not wrapped under a banners property.
In `@src/main/java/org/runnect/server/banner/repository/BannerRepository.java`:
- Line 10: Update BannerRepository.findByIsActiveTrueOrderBySortOrderAsc to
order active banners by sortOrder ascending and a stable secondary key such as
id ascending, then rename the repository method accordingly and update
BannerService to call the renamed method.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8e0077fc-6715-42af-b52a-fca3554e7ac8
📒 Files selected for processing (7)
src/main/java/org/runnect/server/banner/controller/BannerController.javasrc/main/java/org/runnect/server/banner/dto/response/BannerResponse.javasrc/main/java/org/runnect/server/banner/dto/response/GetBannerResponseDto.javasrc/main/java/org/runnect/server/banner/entity/Banner.javasrc/main/java/org/runnect/server/banner/repository/BannerRepository.javasrc/main/java/org/runnect/server/banner/service/BannerService.javasrc/main/java/org/runnect/server/common/constant/SuccessStatus.java
unam98
left a comment
There was a problem hiding this comment.
안드로이드 개발자 관점에서 백엔드(Spring/JPA) 신규 개념 위주로 코멘트 남겼습니다. 승인/거부와 무관한 학습용 코멘트예요.
| import org.runnect.server.common.entity.AuditingTimeEntity; | ||
|
|
||
| @Getter | ||
| @Entity |
There was a problem hiding this comment.
@entity — ORM 매핑
JPA(자바 ORM 표준)에게 "이 클래스는 DB 테이블 하나에 대응된다"고 알려주는 애너테이션. Room의 @Entity와 개념은 같은데, JPA는 필드 변경 감지(dirty checking)까지 자동화되어 있어서 트랜잭션 안에서 필드만 바꿔도(save() 명시 호출 없이) 커밋 시점에 알아서 UPDATE 쿼리가 나감.
|
|
||
| @Getter | ||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) |
There was a problem hiding this comment.
JPA는 왜 파라미터 없는 생성자가 필요할까
Hibernate가 DB row를 객체로 바꿀 때 리플렉션으로 빈 객체부터 만들고 필드를 채워 넣기 때문에 기본 생성자가 필수. 그렇다고 아무데서나 new Banner()로 빈 값 객체를 만들 수 있게 열어두긴 싫어서 PROTECTED로 제한 — JPA(같은 패키지/상속 구조)는 쓸 수 있지만 비즈니스 코드에서는 못 씀. 불변성을 지키기 위한 관용구.
| @Getter | ||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class Banner extends AuditingTimeEntity { |
There was a problem hiding this comment.
AuditingTimeEntity — 공통 필드는 상속으로 분리
createdAt/updatedAt/deletedAt처럼 거의 모든 테이블에 필요한 필드를 @MappedSuperclass 부모 클래스로 빼서 상속받음. Spring Data JPA Auditing(@EnableJpaAuditing)이 저장/수정 시점에 자동으로 채워줘서, 매번 LocalDateTime.now()를 직접 넣을 필요가 없음.
| public class Banner extends AuditingTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) |
There was a problem hiding this comment.
PK 생성 전략(GenerationType.IDENTITY)
DB의 auto-increment(PostgreSQL SERIAL)에 ID 생성을 위임하는 전략. INSERT 쿼리를 실제로 날려야 ID 값을 알 수 있다는 특징이 있음(SEQUENCE 전략과 달리 배치 insert 최적화가 제한적). 이 프로젝트는 기존 Course 엔티티와 동일하게 IDENTITY로 통일해서 씀.
| @Column(nullable = false) | ||
| private Boolean isActive; | ||
|
|
||
| @Builder |
There was a problem hiding this comment.
빌더 패턴 — 생성자 대신 쓰는 이유
필드가 여러 개일 때 new Banner(a, b, c)처럼 순서/타입을 외워야 하는 걸 Banner.builder().imageUrl(...).linkUrl(...).build()처럼 이름을 붙여 조립하게 해줌(Lombok이 보일러플레이트 자동 생성). isActive처럼 항상 고정값(true)으로 시작해야 하는 필드는 빌더 파라미터에서 빼고 생성자 본문에서 직접 세팅.
| import org.springframework.web.bind.annotation.ResponseStatus; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController |
There was a problem hiding this comment.
@RestController = @Controller + @ResponseBody
메서드 리턴값을 뷰(HTML 템플릿)로 렌더링하는 대신, 그대로 JSON으로 직렬화해서 응답 바디에 실어줌. Android 쪽에서 쓰는 Retrofit 인터페이스의 반대편 — 서버가 이 JSON을 만들어서 내려주는 쪽이라고 보면 됨.
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/banner") |
There was a problem hiding this comment.
라우팅 — @RequestMapping + @GetMapping 조합
클래스 레벨 @RequestMapping으로 공통 prefix(/api/banner)를 잡고, 메서드별로 @GetMapping/@PostMapping 등으로 세부 경로+HTTP 메서드를 정의. 여기선 메서드에 경로를 안 붙였으니 최종 엔드포인트는 GET /api/banner.
| private final BannerService bannerService; | ||
|
|
||
| @GetMapping | ||
| @ResponseStatus(HttpStatus.OK) |
There was a problem hiding this comment.
@ResponseStatus — 명시적 상태 코드
@RestController의 기본 성공 상태가 200이라 사실 이 줄이 없어도 동작은 같음. 이 프로젝트는 관례적으로 모든 엔드포인트에 상태 코드를 명시해둠(POST는 201 CREATED처럼 기본값과 다른 경우가 많아서, 아예 전부 명시하는 쪽으로 통일한 걸로 보임).
| @GetMapping | ||
| @ResponseStatus(HttpStatus.OK) | ||
| public ApiResponseDto<GetBannerResponseDto> getBanners() { | ||
| return ApiResponseDto.success(SuccessStatus.GET_BANNER_SUCCESS, bannerService.getBanners()); |
There was a problem hiding this comment.
공통 응답 포맷(response envelope)
모든 API가 {status, success, message, data} 구조로 통일돼서 내려감. 클라이언트가 API마다 다른 응답 구조를 파싱할 필요 없이 data 안쪽만 API별로 다르게 보면 되게끔 하는 컨벤션. SuccessStatus enum이 상태코드+메시지를 한 쌍으로 관리해서 문자열이 코드 여기저기 흩어지는 걸(매직 스트링) 막아줌.
|
|
||
| GET_HEALTH_DATA_SUCCESS(HttpStatus.OK, "건강 데이터 조회 성공"), | ||
| GET_HEALTH_SUMMARY_SUCCESS(HttpStatus.OK, "건강 통계 조회 성공"), | ||
| GET_BANNER_SUCCESS(HttpStatus.OK, "배너 조회 성공"), |
There was a problem hiding this comment.
Enum으로 성공 상태를 관리하는 이유
API마다 성공 메시지를 하드코딩하면 오타나 중복이 나기 쉬움. SuccessStatus에 (HttpStatus, message) 쌍으로 몰아두면 이 서버의 모든 성공 응답 목록을 한 곳에서 파악할 수 있음. 에러도 동일한 패턴으로 ErrorStatus enum이 따로 있음.
sortOrder에 유니크 제약이 없어 값이 같은 배너가 있으면 정렬 순서가 요청마다 달라질 수 있어, id ASC를 보조 키로 붙여 결정론적으로 정렬되도록 함.
배경
코스발견 탭 상단 배너 이미지가 현재 파이어베이스에서 직접 내려오고 있음. 배너를 서버에서 관리할 수 있도록 조회 API를 우선 추가함. Android 연동은 후속 작업.
변경 사항
Banner엔티티 (imageUrl,linkUrl,sortOrder,isActive)BannerRepository.findByIsActiveTrueOrderBySortOrderAscIdAsc()— sortOrder 동률 시 id를 보조 정렬키로 사용해 결정론적으로 정렬GET /api/banner— 활성 배너를 sortOrder(동률 시 id) 순으로 반환. 응답 형태:{ data: { banners: [{ index, imageUrl, linkUrl }] } }(배너 없음 콜백 없이banners: []로 응답)SuccessStatus.GET_BANNER_SUCCESS추가영향 범위
신규 도메인 추가로 기존 API/로직에는 영향 없음. 인증 불필요한 공개 엔드포인트.
검증
./gradlew compileJava성공GET /api/banner호출 →ddl-auto: update로banner테이블 자동 생성 확인,200 {"data":{"banners":[]}}정상 응답 확인 (배너 데이터가 아직 없어 빈 배열)Summary by CodeRabbit