-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 행사 프로그램 조회 기반 추가 #539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package gg.agit.konect.domain.event.controller; | ||
|
|
||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
|
|
||
| import gg.agit.konect.domain.event.dto.EventProgramsResponse; | ||
| import gg.agit.konect.domain.event.enums.EventProgramType; | ||
| import gg.agit.konect.global.auth.annotation.UserId; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.constraints.Min; | ||
|
|
||
| @Tag(name = "(Normal) Event: 행사", description = "행사 API") | ||
| @RequestMapping("/events") | ||
| public interface EventApi { | ||
|
|
||
| @Operation(summary = "행사 프로그램 목록을 조회한다.") | ||
| @GetMapping("/{eventId}/programs") | ||
| ResponseEntity<EventProgramsResponse> getEventPrograms( | ||
| @PathVariable Integer eventId, | ||
| @RequestParam(defaultValue = "ALL") EventProgramType type, | ||
| @RequestParam(defaultValue = "1") @Min(1) Integer page, | ||
| @RequestParam(defaultValue = "20") @Min(1) Integer limit, | ||
| @UserId Integer userId | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package gg.agit.konect.domain.event.controller; | ||
|
|
||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.validation.annotation.Validated; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import gg.agit.konect.domain.event.dto.EventProgramsResponse; | ||
| import gg.agit.konect.domain.event.enums.EventProgramType; | ||
| import gg.agit.konect.domain.event.service.EventService; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @RestController | ||
| @Validated | ||
| @RequiredArgsConstructor | ||
| public class EventController implements EventApi { | ||
|
|
||
| private final EventService eventService; | ||
|
|
||
| @Override | ||
| public ResponseEntity<EventProgramsResponse> getEventPrograms(Integer eventId, EventProgramType type, Integer page, | ||
| Integer limit, | ||
| Integer userId) { | ||
| return ResponseEntity.ok(eventService.getEventPrograms(eventId, type, page, limit, userId)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package gg.agit.konect.domain.event.dto; | ||
|
|
||
| public record EventProgramSummaryResponse( | ||
| Integer programId, | ||
| String title, | ||
| String description, | ||
| String thumbnailUrl, | ||
| Integer rewardPoint, | ||
| String status, | ||
| boolean participated | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package gg.agit.konect.domain.event.dto; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import gg.agit.konect.domain.event.enums.EventProgramType; | ||
|
|
||
| public record EventProgramsResponse( | ||
| Long totalCount, | ||
| Integer currentCount, | ||
| Integer totalPage, | ||
| Integer currentPage, | ||
| EventProgramType type, | ||
| List<EventProgramSummaryResponse> programs | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package gg.agit.konect.domain.event.enums; | ||
|
|
||
| public enum EventProgramType { | ||
| ALL, | ||
| POINT, | ||
| RESONANCE | ||
|
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LEVEL: medium] DB enum과 도메인 enum 값이 불일치합니다. Line 4의 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package gg.agit.konect.domain.event.enums; | ||
|
|
||
| public enum EventProgressStatus { | ||
| UPCOMING, | ||
| ONGOING, | ||
| ENDED | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package gg.agit.konect.domain.event.enums; | ||
|
|
||
| public enum EventStatus { | ||
| DRAFT, | ||
| PUBLISHED, | ||
| ENDED | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package gg.agit.konect.domain.event.model; | ||
|
|
||
| import static jakarta.persistence.GenerationType.IDENTITY; | ||
| import static lombok.AccessLevel.PROTECTED; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import gg.agit.konect.domain.event.enums.EventStatus; | ||
| import gg.agit.konect.global.model.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Table; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table(name = "event") | ||
| @NoArgsConstructor(access = PROTECTED) | ||
| public class Event extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = IDENTITY) | ||
| @Column(name = "id", nullable = false, updatable = false, unique = true) | ||
| private Integer id; | ||
|
|
||
| @Column(name = "title", nullable = false, length = 100) | ||
| private String title; | ||
|
|
||
| @Column(name = "subtitle", length = 255) | ||
| private String subtitle; | ||
|
|
||
| @Column(name = "poster_image_url", length = 255) | ||
| private String posterImageUrl; | ||
|
|
||
| @Column(name = "notice", columnDefinition = "TEXT") | ||
| private String notice; | ||
|
|
||
| @Column(name = "start_at", nullable = false) | ||
| private LocalDateTime startAt; | ||
|
|
||
| @Column(name = "end_at", nullable = false) | ||
| private LocalDateTime endAt; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "status", nullable = false, length = 20) | ||
| private EventStatus status; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package gg.agit.konect.domain.event.model; | ||
|
|
||
| import static jakarta.persistence.EnumType.STRING; | ||
| import static jakarta.persistence.FetchType.LAZY; | ||
| import static jakarta.persistence.GenerationType.IDENTITY; | ||
| import static lombok.AccessLevel.PROTECTED; | ||
|
|
||
| import gg.agit.konect.domain.event.enums.EventProgressStatus; | ||
| import gg.agit.konect.domain.event.enums.EventProgramType; | ||
| import gg.agit.konect.global.model.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table(name = "event_program") | ||
| @NoArgsConstructor(access = PROTECTED) | ||
| public class EventProgram extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = IDENTITY) | ||
| @Column(name = "id", nullable = false, updatable = false, unique = true) | ||
| private Integer id; | ||
|
|
||
| @ManyToOne(fetch = LAZY) | ||
| @JoinColumn(name = "event_id", nullable = false, updatable = false) | ||
| private Event event; | ||
|
|
||
| @Enumerated(STRING) | ||
| @Column(name = "type", nullable = false, length = 20) | ||
| private EventProgramType type; | ||
|
Comment on lines
+37
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LEVEL: medium] 🤖 Prompt for AI Agents |
||
|
|
||
| @Column(name = "title", nullable = false, length = 100) | ||
| private String title; | ||
|
|
||
| @Column(name = "description", nullable = false, length = 255) | ||
| private String description; | ||
|
|
||
| @Column(name = "thumbnail_url", length = 255) | ||
| private String thumbnailUrl; | ||
|
|
||
| @Column(name = "reward_point") | ||
| private Integer rewardPoint; | ||
|
|
||
| @Enumerated(STRING) | ||
| @Column(name = "status", nullable = false, length = 20) | ||
| private EventProgressStatus status; | ||
|
|
||
| @Column(name = "display_order", nullable = false) | ||
| private Integer displayOrder; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package gg.agit.konect.domain.event.repository; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.springframework.data.repository.Repository; | ||
|
|
||
| import gg.agit.konect.domain.event.model.EventProgram; | ||
|
|
||
| public interface EventProgramRepository extends Repository<EventProgram, Integer> { | ||
|
|
||
| List<EventProgram> findAllByEventIdOrderByDisplayOrderAscIdAsc(Integer eventId); | ||
|
|
||
| int countByEventId(Integer eventId); | ||
|
Comment on lines
+11
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# 서비스가 전체 조회 후 메모리 필터/페이지네이션을 수행하는지 확인
rg -n -A8 -B4 'findAllByEventIdOrderByDisplayOrderAscIdAsc|paginate\(|\.filter\(program -> type == EventProgramType.ALL' --type=javaRepository: BCSDLab/KONECT_BACK_END Length of output: 4573 🏁 Script executed: # EventProgram 엔티티의 type 필드 및 구조 확인
fd -n 'EventProgram.java' | head -5Repository: BCSDLab/KONECT_BACK_END Length of output: 297 🏁 Script executed: # 코드베이스의 Pageable 패턴 사용 사례 확인
rg -n 'Pageable|Page<' --type=java | head -20Repository: BCSDLab/KONECT_BACK_END Length of output: 2942 🏁 Script executed: # EventProgram 엔티티 파일 위치 확인 및 내용 검토
fd 'EventProgram.java' --type fRepository: BCSDLab/KONECT_BACK_END Length of output: 132 🏁 Script executed: # EventProgram 엔티티의 type 필드 확인
rg -A 20 'public class EventProgram' --type=java | head -40Repository: BCSDLab/KONECT_BACK_END Length of output: 2082 🏁 Script executed: # EventService의 getEventPrograms 메서드 전체 서명 및 호출 컨텍스트 확인
rg -B 5 'findAllByEventIdOrderByDisplayOrderAscIdAsc' src/main/java/gg/agit/konect/domain/event/service/EventService.javaRepository: BCSDLab/KONECT_BACK_END Length of output: 437 🏁 Script executed: # EventProgramType enum 확인
rg -A 10 'enum EventProgramType' --type=javaRepository: BCSDLab/KONECT_BACK_END Length of output: 483 🏁 Script executed: # Repository의 다른 쿼리 메서드들이 Pageable을 어떻게 사용하는지 확인
rg -B 2 -A 4 'Page<' src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeRankingRepository.java | head -20Repository: BCSDLab/KONECT_BACK_END Length of output: 629 [LEVEL: medium] 저장소가 타입 필터링과 페이지네이션을 지원하지 않아 메모리 처리를 유도합니다.
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package gg.agit.konect.domain.event.repository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.data.repository.Repository; | ||
|
|
||
| import gg.agit.konect.domain.event.model.Event; | ||
|
|
||
| public interface EventRepository extends Repository<Event, Integer> { | ||
|
|
||
| Optional<Event> findById(Integer id); | ||
|
|
||
| Event save(Event event); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package gg.agit.konect.domain.event.service; | ||
|
|
||
| import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_EVENT; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import gg.agit.konect.domain.event.dto.EventProgramSummaryResponse; | ||
| import gg.agit.konect.domain.event.dto.EventProgramsResponse; | ||
| import gg.agit.konect.domain.event.enums.EventProgramType; | ||
| import gg.agit.konect.domain.event.model.EventProgram; | ||
| import gg.agit.konect.domain.event.repository.EventProgramRepository; | ||
| import gg.agit.konect.domain.event.repository.EventRepository; | ||
| import gg.agit.konect.global.exception.CustomException; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class EventService { | ||
|
|
||
| private final EventRepository eventRepository; | ||
| private final EventProgramRepository eventProgramRepository; | ||
|
|
||
| public EventProgramsResponse getEventPrograms(Integer eventId, EventProgramType type, Integer page, Integer limit, | ||
| Integer userId) { | ||
|
|
||
| eventRepository.findById(eventId) | ||
| .orElseThrow(() -> CustomException.of(NOT_FOUND_EVENT)); | ||
|
|
||
| List<EventProgram> filteredPrograms = eventProgramRepository.findAllByEventIdOrderByDisplayOrderAscIdAsc( | ||
| eventId).stream() | ||
| .filter(program -> type == EventProgramType.ALL || program.getType() == type) | ||
| .toList(); | ||
|
|
||
| PagedResult<EventProgram> pagedPrograms = paginate(filteredPrograms, page, limit); | ||
|
Comment on lines
+32
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LEVEL: high] 🤖 Prompt for AI Agents |
||
| List<EventProgramSummaryResponse> programs = pagedPrograms.items().stream() | ||
| .map(this::toEventProgramSummaryResponse) | ||
| .toList(); | ||
|
|
||
| return new EventProgramsResponse( | ||
| (long)pagedPrograms.totalCount(), | ||
| programs.size(), | ||
| pagedPrograms.totalPage(), | ||
| page, | ||
| type, | ||
| programs | ||
| ); | ||
| } | ||
|
|
||
| private <T> PagedResult<T> paginate(List<T> items, Integer page, Integer limit) { | ||
| int totalCount = items.size(); | ||
| int fromIndex = Math.max((page - 1) * limit, 0); | ||
| int toIndex = Math.min(fromIndex + limit, totalCount); | ||
| List<T> pagedItems = fromIndex >= totalCount ? List.of() : items.subList(fromIndex, toIndex); | ||
| int totalPage = totalCount == 0 ? 0 : (int)Math.ceil((double)totalCount / limit); | ||
| return new PagedResult<>(pagedItems, totalCount, totalPage); | ||
| } | ||
|
|
||
| private EventProgramSummaryResponse toEventProgramSummaryResponse(EventProgram program) { | ||
| return new EventProgramSummaryResponse( | ||
| program.getId(), | ||
| program.getTitle(), | ||
| program.getDescription(), | ||
| program.getThumbnailUrl(), | ||
| program.getRewardPoint(), | ||
| program.getStatus().name(), | ||
| false | ||
| ); | ||
| } | ||
|
|
||
| private record PagedResult<T>(List<T> items, int totalCount, int totalPage) { | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| CREATE TABLE IF NOT EXISTS event | ||
| ( | ||
|
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LEVEL: medium] Also applies to: 15-16 🤖 Prompt for AI Agents |
||
| id INT AUTO_INCREMENT PRIMARY KEY, | ||
| title VARCHAR(100) NOT NULL, | ||
| subtitle VARCHAR(255), | ||
| poster_image_url VARCHAR(255), | ||
| notice TEXT, | ||
| start_at TIMESTAMP NOT NULL, | ||
| end_at TIMESTAMP NOT NULL, | ||
| status ENUM ('DRAFT', 'PUBLISHED', 'ENDED') NOT NULL, | ||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, | ||
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL | ||
| ); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS event_program | ||
| ( | ||
| id INT AUTO_INCREMENT PRIMARY KEY, | ||
| event_id INT NOT NULL, | ||
| type ENUM ('POINT', 'RESONANCE') NOT NULL, | ||
| title VARCHAR(100) NOT NULL, | ||
| description VARCHAR(255) NOT NULL, | ||
| thumbnail_url VARCHAR(255), | ||
| reward_point INT, | ||
| status ENUM ('UPCOMING', 'ONGOING', 'ENDED') NOT NULL, | ||
| display_order INT NOT NULL DEFAULT 0, | ||
|
Comment on lines
+18
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [LEVEL: medium] 🤖 Prompt for AI Agents |
||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, | ||
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, | ||
|
|
||
| FOREIGN KEY (event_id) REFERENCES event (id) ON DELETE CASCADE | ||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[LEVEL: medium]
문제:
limit파라미터에 상한 검증이 없어limit=100000같은 과도한 요청이 그대로 허용됩니다.영향: 현재 서비스가 DB 레벨 페이징이 아니라 메모리 기반 페이징을 사용하므로 큰
limit요청에서 응답 지연과 메모리 사용량 급증으로 이어질 수 있습니다.제안:
@Max(예: 100) 또는 공통 페이징 정책 상수를 추가해 상한을 강제해 주세요, As per coding guidelines, "보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다."🤖 Prompt for AI Agents