From 9732ee6fb22f01258fa35854b618ce0e51dce4ba Mon Sep 17 00:00:00 2001 From: Seokjin Jeon Date: Tue, 27 Feb 2024 02:25:40 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[BE]=20refactor:=20Validator=20hasBlank,=20?= =?UTF-8?q?isNegative=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20(#753)=20(#755)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: hasBlank -> notBlank 메서드 명 변경 * refactor: isNegative -> notNegative 메서드 명 변경 --- .../src/main/java/com/festago/admin/domain/Admin.java | 4 ++-- .../admin/presentation/v1/AdminSchoolV1Controller.java | 5 +++-- .../main/java/com/festago/common/util/Validator.java | 6 +++--- .../main/java/com/festago/fcm/domain/MemberFCM.java | 2 +- .../java/com/festago/festival/domain/Festival.java | 4 ++-- .../main/java/com/festago/member/domain/Member.java | 4 ++-- .../main/java/com/festago/school/domain/School.java | 4 ++-- .../main/java/com/festago/student/domain/Student.java | 2 +- .../java/com/festago/student/domain/StudentCode.java | 2 +- .../com/festago/student/domain/VerificationCode.java | 2 +- .../java/com/festago/common/util/ValidatorTest.java | 10 +++++----- 11 files changed, 23 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/com/festago/admin/domain/Admin.java b/backend/src/main/java/com/festago/admin/domain/Admin.java index 48fea47aa..80205ee21 100644 --- a/backend/src/main/java/com/festago/admin/domain/Admin.java +++ b/backend/src/main/java/com/festago/admin/domain/Admin.java @@ -61,14 +61,14 @@ private void validate(String username, String password) { private void validateUsername(String username) { String fieldName = "username"; - Validator.hasBlank(username, fieldName); + Validator.notBlank(username, fieldName); Validator.minLength(username, MIN_USERNAME_LENGTH, fieldName); Validator.maxLength(username, MAX_USERNAME_LENGTH, fieldName); } private void validatePassword(String password) { String fieldName = "password"; - Validator.hasBlank(password, fieldName); + Validator.notBlank(password, fieldName); Validator.minLength(password, MIN_PASSWORD_LENGTH, fieldName); Validator.maxLength(password, MAX_PASSWORD_LENGTH, fieldName); } diff --git a/backend/src/main/java/com/festago/admin/presentation/v1/AdminSchoolV1Controller.java b/backend/src/main/java/com/festago/admin/presentation/v1/AdminSchoolV1Controller.java index c4ed314f2..abe269b19 100644 --- a/backend/src/main/java/com/festago/admin/presentation/v1/AdminSchoolV1Controller.java +++ b/backend/src/main/java/com/festago/admin/presentation/v1/AdminSchoolV1Controller.java @@ -14,6 +14,7 @@ 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.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -64,11 +65,11 @@ public ResponseEntity deleteSchool( } @GetMapping - @ValidPageable(maxSize = 20) + @ValidPageable(maxSize = 50) public ResponseEntity> findAllSchools( @RequestParam(defaultValue = "") String searchFilter, @RequestParam(defaultValue = "") String searchKeyword, - Pageable pageable + @PageableDefault(size = 10) Pageable pageable ) { return ResponseEntity.ok() .body(schoolQueryService.findAll(new SearchCondition(searchFilter, searchKeyword, pageable))); diff --git a/backend/src/main/java/com/festago/common/util/Validator.java b/backend/src/main/java/com/festago/common/util/Validator.java index 6ae4516aa..fddb2bfa2 100644 --- a/backend/src/main/java/com/festago/common/util/Validator.java +++ b/backend/src/main/java/com/festago/common/util/Validator.java @@ -15,7 +15,7 @@ private Validator() { * @param fieldName 예외 메시지에 출력할 필드명 * @throws ValidException input이 null 또는 공백이면 */ - public static void hasBlank(String input, String fieldName) { + public static void notBlank(String input, String fieldName) { if (input == null || input.isBlank()) { throw new ValidException("%s은/는 null 또는 공백이 될 수 없습니다.".formatted(fieldName)); } @@ -141,7 +141,7 @@ public static void minValue(long value, long minValue, String fieldName) { * @param fieldName 예외 메시지에 출력할 필드명 * @throws ValidException value가 음수이면 */ - public static void isNegative(int value, String fieldName) { + public static void notNegative(int value, String fieldName) { if (value < 0) { throw new ValidException("%s은/는 음수가 될 수 없습니다.".formatted(fieldName)); } @@ -154,7 +154,7 @@ public static void isNegative(int value, String fieldName) { * @param fieldName 예외 메시지에 출력할 필드명 * @throws ValidException value가 음수이면 */ - public static void isNegative(long value, String fieldName) { + public static void notNegative(long value, String fieldName) { if (value < 0) { throw new ValidException("%s은/는 음수가 될 수 없습니다.".formatted(fieldName)); } diff --git a/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java index 852f9b615..6b110d3d9 100644 --- a/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java +++ b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java @@ -52,7 +52,7 @@ private void validateMemberId(Long memberId) { private void validateFcmToken(String fcmToken) { String fieldName = "fcmToken"; - Validator.hasBlank(fcmToken, fieldName); + Validator.notBlank(fcmToken, fieldName); Validator.maxLength(fcmToken, MAX_FCM_TOKEN_LENGTH, fieldName); } diff --git a/backend/src/main/java/com/festago/festival/domain/Festival.java b/backend/src/main/java/com/festago/festival/domain/Festival.java index a47de38c0..be07ae610 100644 --- a/backend/src/main/java/com/festago/festival/domain/Festival.java +++ b/backend/src/main/java/com/festago/festival/domain/Festival.java @@ -74,13 +74,13 @@ private void validate(String name, LocalDate startDate, LocalDate endDate, Strin private void validateName(String name) { String fieldName = "name"; - Validator.hasBlank(name, fieldName); + Validator.notBlank(name, fieldName); Validator.maxLength(name, MAX_NAME_LENGTH, fieldName); } private void validateThumbnail(String thumbnail) { String fieldName = "thumbnail"; - Validator.hasBlank(thumbnail, fieldName); + Validator.notBlank(thumbnail, fieldName); Validator.maxLength(thumbnail, MAX_THUMBNAIL_LENGTH, fieldName); } diff --git a/backend/src/main/java/com/festago/member/domain/Member.java b/backend/src/main/java/com/festago/member/domain/Member.java index a7f0e3378..e8d64f922 100644 --- a/backend/src/main/java/com/festago/member/domain/Member.java +++ b/backend/src/main/java/com/festago/member/domain/Member.java @@ -89,7 +89,7 @@ private void validate(String socialId, SocialType socialType, String nickname, S private void validateSocialId(String socialId) { String fieldName = "socialId"; - Validator.hasBlank(socialId, fieldName); + Validator.notBlank(socialId, fieldName); Validator.maxLength(socialId, MAX_SOCIAL_ID_LENGTH, fieldName); } @@ -99,7 +99,7 @@ private void validateSocialType(SocialType socialType) { private void validateNickname(String nickname) { String fieldName = "nickname"; - Validator.hasBlank(nickname, fieldName); + Validator.notBlank(nickname, fieldName); Validator.maxLength(nickname, MAX_NICKNAME_LENGTH, fieldName); } diff --git a/backend/src/main/java/com/festago/school/domain/School.java b/backend/src/main/java/com/festago/school/domain/School.java index cf6d09cfa..fd72a20fa 100644 --- a/backend/src/main/java/com/festago/school/domain/School.java +++ b/backend/src/main/java/com/festago/school/domain/School.java @@ -76,13 +76,13 @@ private void validate(String domain, String name, SchoolRegion region, String lo private void validateDomain(String domain) { String fieldName = "domain"; - Validator.hasBlank(domain, fieldName); + Validator.notBlank(domain, fieldName); Validator.maxLength(domain, MAX_DOMAIN_LENGTH, fieldName); } private void validateName(String name) { String fieldName = "name"; - Validator.hasBlank(name, fieldName); + Validator.notBlank(name, fieldName); Validator.maxLength(name, MAX_NAME_LENGTH, fieldName); } diff --git a/backend/src/main/java/com/festago/student/domain/Student.java b/backend/src/main/java/com/festago/student/domain/Student.java index 7857d6a8d..c93d80ea8 100644 --- a/backend/src/main/java/com/festago/student/domain/Student.java +++ b/backend/src/main/java/com/festago/student/domain/Student.java @@ -66,7 +66,7 @@ private void validateSchool(School school) { private void validateUsername(String username) { String fieldName = "username"; - Validator.hasBlank(username, fieldName); + Validator.notBlank(username, fieldName); Validator.maxLength(username, MAX_USERNAME_LENGTH, fieldName); } diff --git a/backend/src/main/java/com/festago/student/domain/StudentCode.java b/backend/src/main/java/com/festago/student/domain/StudentCode.java index aaf8cb966..6d3615f27 100644 --- a/backend/src/main/java/com/festago/student/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/student/domain/StudentCode.java @@ -90,7 +90,7 @@ private void validateMember(Member member) { private void validateUsername(String username) { String fieldName = "username"; - Validator.hasBlank(username, fieldName); + Validator.notBlank(username, fieldName); Validator.maxLength(username, MAX_USERNAME_LENGTH, fieldName); } diff --git a/backend/src/main/java/com/festago/student/domain/VerificationCode.java b/backend/src/main/java/com/festago/student/domain/VerificationCode.java index 24fe117bf..fe8d1e136 100644 --- a/backend/src/main/java/com/festago/student/domain/VerificationCode.java +++ b/backend/src/main/java/com/festago/student/domain/VerificationCode.java @@ -29,7 +29,7 @@ private void validate(String value) { } private void validateBlank(String value) { - Validator.hasBlank(value, "VerificationCode"); + Validator.notBlank(value, "VerificationCode"); } private void validateLength(String value) { diff --git a/backend/src/test/java/com/festago/common/util/ValidatorTest.java b/backend/src/test/java/com/festago/common/util/ValidatorTest.java index dad4a00dd..8e60376e2 100644 --- a/backend/src/test/java/com/festago/common/util/ValidatorTest.java +++ b/backend/src/test/java/com/festago/common/util/ValidatorTest.java @@ -24,7 +24,7 @@ class hasBlank { @NullSource void 문자열이_null이면_예외(String input) { // when & then - assertThatThrownBy(() -> Validator.hasBlank(input, "")) + assertThatThrownBy(() -> Validator.notBlank(input, "")) .isInstanceOf(ValidException.class); } @@ -32,7 +32,7 @@ class hasBlank { @ValueSource(strings = {"", " ", "\t", "\n"}) void 문자열이_공백이면_예외(String input) { // when & then - assertThatThrownBy(() -> Validator.hasBlank(input, "")) + assertThatThrownBy(() -> Validator.notBlank(input, "")) .isInstanceOf(ValidException.class); } @@ -43,7 +43,7 @@ class hasBlank { // when & then assertThatNoException() - .isThrownBy(() -> Validator.hasBlank(input, "")); + .isThrownBy(() -> Validator.notBlank(input, "")); } } @@ -222,7 +222,7 @@ class isNegative { int value = -1; // when & then - assertThatThrownBy(() -> Validator.isNegative(value, "")) + assertThatThrownBy(() -> Validator.notNegative(value, "")) .isInstanceOf(ValidException.class); } @@ -231,7 +231,7 @@ class isNegative { void 값이_음수가_아니면_통과(int value) { // when & then assertThatNoException() - .isThrownBy(() -> Validator.isNegative(value, "")); + .isThrownBy(() -> Validator.notNegative(value, "")); } } } From 8308755a673c4e5a702cfc163af311df8625e7e4 Mon Sep 17 00:00:00 2001 From: Seokjin Jeon Date: Tue, 27 Feb 2024 02:26:43 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[BE]=20refactor:=20Stage=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20lineUp=20=ED=95=84=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#752)=20(#754)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Stage 엔티티 lineUp 필드 제거 - getter는 하위 호환성을 위해 빈 문자열을 반환하도록 유지 * test: 데이터 셋업용 SQL에 lineUp 삭제 * fix: 빈 문자열 반환에서 "deprecated" 의미를 가지는 문자열 반환 --- .../stage/application/StageService.java | 2 -- .../java/com/festago/stage/domain/Stage.java | 35 +++++-------------- .../migration/V16__remove_stage_line_up.sql | 2 ++ .../com/festago/support/StageFixture.java | 8 +---- .../test/resources/ticketing-test-data.sql | 4 +-- 5 files changed, 14 insertions(+), 37 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V16__remove_stage_line_up.sql diff --git a/backend/src/main/java/com/festago/stage/application/StageService.java b/backend/src/main/java/com/festago/stage/application/StageService.java index e59a7e35e..cf5e01b8b 100644 --- a/backend/src/main/java/com/festago/stage/application/StageService.java +++ b/backend/src/main/java/com/festago/stage/application/StageService.java @@ -27,7 +27,6 @@ public StageResponse create(StageCreateRequest request) { Festival festival = festivalRepository.getOrThrow(request.festivalId()); Stage newStage = stageRepository.save(new Stage( request.startTime(), - request.lineUp(), request.ticketOpenTime(), festival)); @@ -47,7 +46,6 @@ private Stage findStage(Long stageId) { public void update(Long stageId, StageUpdateRequest request) { Stage stage = findStage(stageId); stage.changeTime(request.startTime(), request.ticketOpenTime()); - stage.changeLineUp(request.lineUp()); } public void delete(Long stageId) { diff --git a/backend/src/main/java/com/festago/stage/domain/Stage.java b/backend/src/main/java/com/festago/stage/domain/Stage.java index 6f0ffd4ea..0c2d5ee64 100644 --- a/backend/src/main/java/com/festago/stage/domain/Stage.java +++ b/backend/src/main/java/com/festago/stage/domain/Stage.java @@ -14,7 +14,6 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -25,8 +24,6 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Stage extends BaseTimeEntity { - private static final int MAX_LINEUP_LENGTH = 255; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -34,9 +31,6 @@ public class Stage extends BaseTimeEntity { @NotNull private LocalDateTime startTime; - @Size(max = MAX_LINEUP_LENGTH) - private String lineUp; - @NotNull private LocalDateTime ticketOpenTime; @@ -47,34 +41,24 @@ public class Stage extends BaseTimeEntity { @OneToMany(mappedBy = "stage", fetch = FetchType.LAZY) private List tickets = new ArrayList<>(); - public Stage(LocalDateTime startTime, String lineUp, LocalDateTime ticketOpenTime, Festival festival) { - this(null, startTime, lineUp, ticketOpenTime, festival); - } - public Stage(LocalDateTime startTime, LocalDateTime ticketOpenTime, Festival festival) { - this(null, startTime, null, ticketOpenTime, festival); + this(null, startTime, ticketOpenTime, festival); } - public Stage(Long id, LocalDateTime startTime, String lineUp, LocalDateTime ticketOpenTime, + public Stage(Long id, LocalDateTime startTime, LocalDateTime ticketOpenTime, Festival festival) { - validate(startTime, lineUp, ticketOpenTime, festival); + validate(startTime, ticketOpenTime, festival); this.id = id; this.startTime = startTime; - this.lineUp = lineUp; this.ticketOpenTime = ticketOpenTime; this.festival = festival; } - private void validate(LocalDateTime startTime, String lineUp, LocalDateTime ticketOpenTime, Festival festival) { - validateLineUp(lineUp); + private void validate(LocalDateTime startTime, LocalDateTime ticketOpenTime, Festival festival) { validateFestival(festival); validateTime(startTime, ticketOpenTime, festival); } - private void validateLineUp(String lineUp) { - Validator.maxLength(lineUp, MAX_LINEUP_LENGTH, "lineUp"); - } - private void validateFestival(Festival festival) { Validator.notNull(festival, "festival"); } @@ -100,11 +84,6 @@ public void changeTime(LocalDateTime startTime, LocalDateTime ticketOpenTime) { this.ticketOpenTime = ticketOpenTime; } - public void changeLineUp(String lineUp) { - validateLineUp(lineUp); - this.lineUp = lineUp; - } - public Long getId() { return id; } @@ -113,8 +92,12 @@ public LocalDateTime getStartTime() { return startTime; } + /** + * API 일부에 사용되는 곳이 있기 때문에 deprecated 문자열을 반환하도록 처리 + */ + @Deprecated(forRemoval = true) public String getLineUp() { - return lineUp; + return "deprecated"; } public LocalDateTime getTicketOpenTime() { diff --git a/backend/src/main/resources/db/migration/V16__remove_stage_line_up.sql b/backend/src/main/resources/db/migration/V16__remove_stage_line_up.sql new file mode 100644 index 000000000..1bc48a948 --- /dev/null +++ b/backend/src/main/resources/db/migration/V16__remove_stage_line_up.sql @@ -0,0 +1,2 @@ +alter table stage + drop column line_up diff --git a/backend/src/test/java/com/festago/support/StageFixture.java b/backend/src/test/java/com/festago/support/StageFixture.java index 5c13eda00..6c4b12ebc 100644 --- a/backend/src/test/java/com/festago/support/StageFixture.java +++ b/backend/src/test/java/com/festago/support/StageFixture.java @@ -8,7 +8,6 @@ public class StageFixture { private Long id; private LocalDateTime startTime = LocalDateTime.now(); - private String lineUp = "오리, 글렌, 푸우, 애쉬"; private LocalDateTime ticketOpenTime = startTime.minusWeeks(1); private Festival festival = FestivalFixture.festival().build(); @@ -29,11 +28,6 @@ public StageFixture startTime(LocalDateTime startTime) { return this; } - public StageFixture lineUp(String lineUp) { - this.lineUp = lineUp; - return this; - } - public StageFixture ticketOpenTime(LocalDateTime ticketOpenTime) { this.ticketOpenTime = ticketOpenTime; return this; @@ -46,6 +40,6 @@ public StageFixture festival(Festival festival) { } public Stage build() { - return new Stage(id, startTime, lineUp, ticketOpenTime, festival); + return new Stage(id, startTime, ticketOpenTime, festival); } } diff --git a/backend/src/test/resources/ticketing-test-data.sql b/backend/src/test/resources/ticketing-test-data.sql index 58a8894dc..0b0c146cf 100644 --- a/backend/src/test/resources/ticketing-test-data.sql +++ b/backend/src/test/resources/ticketing-test-data.sql @@ -4,8 +4,8 @@ values ('festago.com', '페스타고 대학교'); insert into festival (school_id, end_date, name, start_date, thumbnail) values (1, '2023-07-30', '테코 대학교', '2023-08-02', ''); -insert into stage (festival_id, line_up, start_time, ticket_open_time) -values (1, '', '2023-07-30T03:21:31.964676', '2023-07-23T03:21:31.964676'); +insert into stage (festival_id, start_time, ticket_open_time) +values (1, '2023-07-30T03:21:31.964676', '2023-07-23T03:21:31.964676'); insert into ticket (school_id, stage_id, ticket_type) values (1, 1, 'VISITOR'); From 6e066bdccb27dbc4e68d1f3a161c4c5d9e1e7287 Mon Sep 17 00:00:00 2001 From: Guga Date: Tue, 27 Feb 2024 16:08:30 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[BE]=20feat:=20Artist=20=EC=83=9D=EC=84=B1,?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.(#732)=20(#756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ArtistUpdateRequest 에 backgroundImageUrl 를 보낸다. * chore: bean validation 의 기본 메시지를 사용하도록 변경. * feat: 아티스트 생성 요청에 backgroundImageUrl 을 포함하도록 변경 * chore: bean validation 메세지를 사용하도록 변경 * test: 테스트 수정 * feat: 사용하지 않는 생성자 삭제 * test: 사용하지 않는 쿠키 값 제거 --- .../com/festago/admin/dto/ArtistCreateRequest.java | 8 +++++--- .../com/festago/admin/dto/ArtistUpdateRequest.java | 8 +++++--- .../artist/application/ArtistCommandService.java | 5 +++-- .../main/java/com/festago/artist/domain/Artist.java | 7 ++++--- .../presentation/v1/AdminArtistV1ControllerTest.java | 11 ++++------- .../ArtistCommandServiceIntegrationTest.java | 6 ++++-- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/backend/src/main/java/com/festago/admin/dto/ArtistCreateRequest.java b/backend/src/main/java/com/festago/admin/dto/ArtistCreateRequest.java index 4058a9cfc..b8c7c121a 100644 --- a/backend/src/main/java/com/festago/admin/dto/ArtistCreateRequest.java +++ b/backend/src/main/java/com/festago/admin/dto/ArtistCreateRequest.java @@ -3,10 +3,12 @@ import jakarta.validation.constraints.NotBlank; public record ArtistCreateRequest( - @NotBlank(message = "아티스트 이름은 비어있을 수 없습니다.") + @NotBlank String name, - @NotBlank(message = "아티스트 이미지는 비어있을 수 없습니다.") - String profileImage + @NotBlank + String profileImage, + @NotBlank + String backgroundImageUrl ) { } diff --git a/backend/src/main/java/com/festago/admin/dto/ArtistUpdateRequest.java b/backend/src/main/java/com/festago/admin/dto/ArtistUpdateRequest.java index 35b4c6f82..f827955fc 100644 --- a/backend/src/main/java/com/festago/admin/dto/ArtistUpdateRequest.java +++ b/backend/src/main/java/com/festago/admin/dto/ArtistUpdateRequest.java @@ -3,10 +3,12 @@ import jakarta.validation.constraints.NotBlank; public record ArtistUpdateRequest( - @NotBlank(message = "아티스트 이름은 비어있을 수 없습니다.") + @NotBlank String name, - @NotBlank(message = "아티스트 이미지는 비어있을 수 없습니다.") - String profileImage + @NotBlank + String profileImage, + @NotBlank + String backgroundImageUrl ) { } diff --git a/backend/src/main/java/com/festago/artist/application/ArtistCommandService.java b/backend/src/main/java/com/festago/artist/application/ArtistCommandService.java index 54926b8fe..9e7748368 100644 --- a/backend/src/main/java/com/festago/artist/application/ArtistCommandService.java +++ b/backend/src/main/java/com/festago/artist/application/ArtistCommandService.java @@ -16,13 +16,14 @@ public class ArtistCommandService { private final ArtistRepository artistRepository; public Long save(ArtistCreateRequest request) { - Artist artist = artistRepository.save(new Artist(request.name(), request.profileImage())); + Artist artist = artistRepository.save( + new Artist(request.name(), request.profileImage(), request.backgroundImageUrl())); return artist.getId(); } public void update(ArtistUpdateRequest request, Long artistId) { Artist artist = artistRepository.getOrThrow(artistId); - artist.update(request.name(), request.profileImage()); + artist.update(request.name(), request.profileImage(), request.backgroundImageUrl()); } public void delete(Long artistId) { diff --git a/backend/src/main/java/com/festago/artist/domain/Artist.java b/backend/src/main/java/com/festago/artist/domain/Artist.java index 13c24915a..f29ecf703 100644 --- a/backend/src/main/java/com/festago/artist/domain/Artist.java +++ b/backend/src/main/java/com/festago/artist/domain/Artist.java @@ -34,13 +34,14 @@ public Artist(String name, String profileImage) { this(null, name, profileImage, DEFAULT_URL); } - public Artist(Long id, String name, String profileImage) { - this(id, name, profileImage, DEFAULT_URL); + public Artist(String name, String profileImage, String backgroundImageUrl) { + this(null, name, profileImage, backgroundImageUrl); } - public void update(String name, String profileImage) { + public void update(String name, String profileImage, String backgroundImageUrl) { this.name = name; this.profileImage = profileImage; + this.backgroundImageUrl = backgroundImageUrl; } public Long getId() { diff --git a/backend/src/test/java/com/festago/admin/presentation/v1/AdminArtistV1ControllerTest.java b/backend/src/test/java/com/festago/admin/presentation/v1/AdminArtistV1ControllerTest.java index 5d4f6f939..705bbd3a3 100644 --- a/backend/src/test/java/com/festago/admin/presentation/v1/AdminArtistV1ControllerTest.java +++ b/backend/src/test/java/com/festago/admin/presentation/v1/AdminArtistV1ControllerTest.java @@ -41,18 +41,13 @@ class AdminArtistV1ControllerTest { @Autowired MockMvc mockMvc; - @Autowired ObjectMapper objectMapper; - @Autowired ArtistV1QueryService artistV1QueryService; - @Autowired ArtistCommandService artistCommandService; - private static final Cookie COOKIE = new Cookie("token", "token"); - @Nested class 아티스트_생성 { @@ -66,7 +61,8 @@ class 올바른_주소로 { @WithMockAuth(role = Role.ADMIN) void 요청을_보내면_201_응답과_Location_헤더에_식별자가_반환된다() throws Exception { // given - ArtistCreateRequest request = new ArtistCreateRequest("윤서연", "https://image.com/image.png"); + ArtistCreateRequest request = new ArtistCreateRequest("윤서연", "https://image.com/image.png", + "https://image.com/image.png"); given(artistCommandService.save(any(ArtistCreateRequest.class))) .willReturn(1L); @@ -111,7 +107,8 @@ class 올바른_주소로 { @WithMockAuth(role = Role.ADMIN) void 요청을_보내면_200_응답이_반환된다() throws Exception { // given - ArtistUpdateRequest request = new ArtistUpdateRequest("윤하", "https://image.com/image.png"); + ArtistUpdateRequest request = new ArtistUpdateRequest("윤하", "https://image.com/image.png", + "https://image.com/image.png"); // when & then mockMvc.perform(put(uri, 1L) diff --git a/backend/src/test/java/com/festago/artist/application/integration/ArtistCommandServiceIntegrationTest.java b/backend/src/test/java/com/festago/artist/application/integration/ArtistCommandServiceIntegrationTest.java index fa5cd0c67..d89887765 100644 --- a/backend/src/test/java/com/festago/artist/application/integration/ArtistCommandServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/artist/application/integration/ArtistCommandServiceIntegrationTest.java @@ -27,7 +27,8 @@ class ArtistCommandServiceIntegrationTest extends ApplicationIntegrationTest { @Test void 아티스트를_저장한다() { // given - ArtistCreateRequest request = new ArtistCreateRequest("윤서연", "https://image.com/image.png"); + ArtistCreateRequest request = new ArtistCreateRequest("윤서연", "https://image.com/image.png", + "https://image.com/image.png"); // when Long artistId = artistCommandService.save(request); @@ -41,7 +42,8 @@ class ArtistCommandServiceIntegrationTest extends ApplicationIntegrationTest { void 아티스트_정보를_변경한다() { // given Long artistId = artistRepository.save(new Artist("고윤하", "https://image.com/image1.png")).getId(); - ArtistUpdateRequest request = new ArtistUpdateRequest("윤하", "https://image.com/image2.png"); + ArtistUpdateRequest request = new ArtistUpdateRequest("윤하", "https://image.com/image2.png", + "https://image.com/image2.png"); // when artistCommandService.update(request, artistId); From c41210db87888ef206e41ee18fe8e9cbe8674449 Mon Sep 17 00:00:00 2001 From: SeongHoonC <108349655+SeongHoonC@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:07:41 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[AN/USER]=20feat:=20=EC=B6=95=EC=A0=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=99=94=EB=A9=B4=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=EC=B6=95=EC=A0=9C=20=EB=AA=A9=EB=A1=9D=EC=97=90=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=EC=9D=84=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=EB=8B=A4(#735)=20(#736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(PopularFestivalForegroundAdapter): foreground 인기 축제 목록을 무한 스크롤 가능하게 변경한다. * refactor(PopularFestivalViewPagerAdapter): 변수 이름 카멜 케이스로 변경 * refactor(FestivalListPopularViewHolder): DotIndicator 는 터치해서 이동할 수 없다 * refactor(PopularFestivalViewPagerAdapter): initialPosition 계산 변수로 분리 * fix(PopularFestivalViewPagerAdapter): 축제 목록이 변경되면 포지션 초기화 * feat(PopularFestivalViewPagerAdapter): 미리 로딩하는 좌우 화면 개수를 2로 변경한다 * feat(PopularFestivalViewPagerAdapter): 인기 축제 개수가 0 개면 인기 축제 목록이 보이지 않는다 --- .../home/festivallist/FestivalListFragment.kt | 32 ++++++++++------ .../festivallist/FestivalListViewModel.kt | 2 +- .../festival/FestivalListPopularViewHolder.kt | 8 ++-- .../PopularFestivalViewPagerAdapter.kt | 23 +++++++++--- .../PopularFestivalForegroundAdapter.kt | 37 +++++++++---------- .../uistate/FestivalListUiState.kt | 2 +- .../uistate/PopularFestivalUiState.kt | 2 +- 7 files changed, 62 insertions(+), 44 deletions(-) diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt index 3f03a190f..d0930335c 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt @@ -130,19 +130,27 @@ class FestivalListFragment : Fragment() { } private fun handleSuccess(uiState: FestivalListUiState.Success) { - festivalListAdapter.submitList( - listOf( - uiState.popularFestivals, - FestivalTabUiState { - val festivalFilter = when (it) { - 0 -> FestivalFilterUiState.PROGRESS - 1 -> FestivalFilterUiState.PLANNED - else -> FestivalFilterUiState.PROGRESS - } - vm.loadFestivals(festivalFilter) - }, - ) + uiState.festivals, + val items = uiState.getItems() + festivalListAdapter.submitList(items) + } + + private fun FestivalListUiState.Success.getItems(): List { + val items = mutableListOf() + if (popularFestivalUiState.festivals.isNotEmpty()) { + items.add(popularFestivalUiState) + } + items.add( + FestivalTabUiState { + val festivalFilter = when (it) { + 0 -> FestivalFilterUiState.PROGRESS + 1 -> FestivalFilterUiState.PLANNED + else -> FestivalFilterUiState.PROGRESS + } + vm.loadFestivals(festivalFilter) + }, ) + items.addAll(festivals) + return items.toList() } private fun showSchoolDetail() { diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt index 26173889f..fb362b89d 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt @@ -49,7 +49,7 @@ class FestivalListViewModel @Inject constructor( _uiState.value = FestivalListUiState.Success( PopularFestivalUiState( title = popularFestivals.title, - popularFestivals = popularFestivals.festivals.map { it.toUiState() }, + festivals = popularFestivals.festivals.map { it.toUiState() }, ), festivals = festivalsPage.festivals.map { it.toUiState() }, isLastPage = festivalsPage.isLastPage, diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListPopularViewHolder.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListPopularViewHolder.kt index 5cd451149..64bd3c047 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListPopularViewHolder.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListPopularViewHolder.kt @@ -22,13 +22,15 @@ class FestivalListPopularViewHolder(val binding: ItemFestivalListPopularBinding) init { TabLayoutMediator( binding.tlDotIndicator, - binding.vpPopularFestivalForeground, - ) { tab, position -> }.attach() + binding.vpPopularFestivalBackground, + ) { tab, position -> + tab.view.isClickable = false + }.attach() } fun bind(popularFestivalUiState: PopularFestivalUiState) { binding.tvPopularFestivalTitle.text = popularFestivalUiState.title - popularFestivalViewPager.submitList(popularFestivalUiState.popularFestivals) + popularFestivalViewPager.submitList(popularFestivalUiState.festivals) } companion object { diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/PopularFestivalViewPagerAdapter.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/PopularFestivalViewPagerAdapter.kt index 792643f92..3357e515f 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/PopularFestivalViewPagerAdapter.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/PopularFestivalViewPagerAdapter.kt @@ -9,7 +9,7 @@ import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalIte import kotlin.math.abs class PopularFestivalViewPagerAdapter( - foregroundViewPager: ViewPager2, + private val foregroundViewPager: ViewPager2, backgroundViewPager: ViewPager2, private val onPopularFestivalSelected: (FestivalItemUiState) -> Unit, ) { @@ -24,22 +24,23 @@ class PopularFestivalViewPagerAdapter( foregroundViewPager.adapter = foregroundAdapter backgroundViewPager.adapter = backgroundAdapter - setTargetItemOnPageSelected(viewpager = foregroundViewPager, target = backgroundViewPager) + setTargetItemOnPageSelected(viewPager = foregroundViewPager, target = backgroundViewPager) narrowSpaceViewPager(viewPager = foregroundViewPager) setOffscreenPagesLimit(foregroundViewPager, PAGE_LIMIT) setOffscreenPagesLimit(backgroundViewPager, PAGE_LIMIT) backgroundViewPager.isUserInputEnabled = false } - private fun setTargetItemOnPageSelected(viewpager: ViewPager2, target: ViewPager2) { + private fun setTargetItemOnPageSelected(viewPager: ViewPager2, target: ViewPager2) { val onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) - target.setCurrentItem(position, false) - onPopularFestivalSelected(popularFestivals[position]) + val itemIndex = position % popularFestivals.size + target.setCurrentItem(itemIndex, false) + onPopularFestivalSelected(popularFestivals[itemIndex]) } } - viewpager.registerOnPageChangeCallback(onPageChangeCallback) + viewPager.registerOnPageChangeCallback(onPageChangeCallback) } private fun narrowSpaceViewPager(viewPager: ViewPager2) { @@ -85,10 +86,20 @@ class PopularFestivalViewPagerAdapter( } fun submitList(festivals: List) { + val lastFestivals = popularFestivals.toList() popularFestivals.clear() popularFestivals.addAll(festivals) foregroundAdapter.submitList(festivals) backgroundAdapter.submitList(festivals) + + if (lastFestivals != festivals) { + initItemPosition() + } + } + + private fun initItemPosition() { + val initialPosition = Int.MAX_VALUE / 2 - (Int.MAX_VALUE / 2 % popularFestivals.size) + foregroundViewPager.setCurrentItem(initialPosition, false) } companion object { diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/foreground/PopularFestivalForegroundAdapter.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/foreground/PopularFestivalForegroundAdapter.kt index 02f571510..03548fbaf 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/foreground/PopularFestivalForegroundAdapter.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/popularfestival/foreground/PopularFestivalForegroundAdapter.kt @@ -1,36 +1,33 @@ package com.festago.festago.presentation.ui.home.festivallist.popularfestival.foreground import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalItemUiState -class PopularFestivalForegroundAdapter : - ListAdapter(diffUtil) { +class PopularFestivalForegroundAdapter(festivals: List = listOf()) : + RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PopularFestivalForegroundViewHolder { + private val _festivals = festivals.toMutableList() + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): PopularFestivalForegroundViewHolder { return PopularFestivalForegroundViewHolder.of(parent) } override fun onBindViewHolder(holder: PopularFestivalForegroundViewHolder, position: Int) { - holder.bind(getItem(position)) + holder.bind(_festivals[position % _festivals.size]) } - companion object { - val diffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: FestivalItemUiState, - newItem: FestivalItemUiState, - ): Boolean { - return oldItem.id == newItem.id - } + override fun getItemCount(): Int = Int.MAX_VALUE - override fun areContentsTheSame( - oldItem: FestivalItemUiState, - newItem: FestivalItemUiState, - ): Boolean { - return oldItem == newItem - } + fun submitList(festivals: List) { + if (_festivals.toList() == festivals) { + return } + _festivals.clear() + _festivals.addAll(festivals) + notifyDataSetChanged() } } diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt index be1410762..0a4ef1618 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt @@ -4,7 +4,7 @@ sealed interface FestivalListUiState { object Loading : FestivalListUiState data class Success( - val popularFestivals: PopularFestivalUiState, + val popularFestivalUiState: PopularFestivalUiState, val festivals: List, val isLastPage: Boolean, ) : FestivalListUiState diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/PopularFestivalUiState.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/PopularFestivalUiState.kt index 524f7ed14..55a6f3d7e 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/PopularFestivalUiState.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/PopularFestivalUiState.kt @@ -2,5 +2,5 @@ package com.festago.festago.presentation.ui.home.festivallist.uistate data class PopularFestivalUiState( val title: String, - val popularFestivals: List, + val festivals: List, ) From 19faf78967a44b8b0e69ded52f5149203d5da906 Mon Sep 17 00:00:00 2001 From: Seokjin Jeon Date: Tue, 5 Mar 2024 23:53:05 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[BE]=20feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=20=EC=B6=95=EC=A0=9C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#745)=20(#749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 관리자 축제 조회 기능 추가 * feat: 관리자 축제 조회 API 추가 --- .../AdminFestivalV1QueryService.java | 21 ++ .../admin/dto/AdminFestivalV1Response.java | 18 ++ .../v1/AdminFestivalV1Controller.java | 21 ++ .../AdminFestivalV1QueryDslRepository.java | 97 +++++++ .../common/querydsl/OrderSpecifierUtils.java | 20 ++ .../common/querydsl/QueryDslHelper.java | 39 +++ ...FestivalV1QueryServiceIntegrationTest.java | 269 ++++++++++++++++++ .../v1/AdminFestivalV1ControllerTest.java | 55 ++++ 8 files changed, 540 insertions(+) create mode 100644 backend/src/main/java/com/festago/admin/application/AdminFestivalV1QueryService.java create mode 100644 backend/src/main/java/com/festago/admin/dto/AdminFestivalV1Response.java create mode 100644 backend/src/main/java/com/festago/admin/repository/AdminFestivalV1QueryDslRepository.java create mode 100644 backend/src/main/java/com/festago/common/querydsl/OrderSpecifierUtils.java create mode 100644 backend/src/main/java/com/festago/common/querydsl/QueryDslHelper.java create mode 100644 backend/src/test/java/com/festago/admin/application/integration/AdminFestivalV1QueryServiceIntegrationTest.java diff --git a/backend/src/main/java/com/festago/admin/application/AdminFestivalV1QueryService.java b/backend/src/main/java/com/festago/admin/application/AdminFestivalV1QueryService.java new file mode 100644 index 000000000..01d8430b5 --- /dev/null +++ b/backend/src/main/java/com/festago/admin/application/AdminFestivalV1QueryService.java @@ -0,0 +1,21 @@ +package com.festago.admin.application; + +import com.festago.admin.dto.AdminFestivalV1Response; +import com.festago.admin.repository.AdminFestivalV1QueryDslRepository; +import com.festago.common.querydsl.SearchCondition; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AdminFestivalV1QueryService { + + private final AdminFestivalV1QueryDslRepository adminFestivalV1QueryDslRepository; + + public Page findAll(SearchCondition searchCondition) { + return adminFestivalV1QueryDslRepository.findAll(searchCondition); + } +} diff --git a/backend/src/main/java/com/festago/admin/dto/AdminFestivalV1Response.java b/backend/src/main/java/com/festago/admin/dto/AdminFestivalV1Response.java new file mode 100644 index 000000000..593844918 --- /dev/null +++ b/backend/src/main/java/com/festago/admin/dto/AdminFestivalV1Response.java @@ -0,0 +1,18 @@ +package com.festago.admin.dto; + +import com.querydsl.core.annotations.QueryProjection; +import java.time.LocalDate; + +public record AdminFestivalV1Response( + Long id, + String name, + String schoolName, + LocalDate startDate, + LocalDate endDate, + long stageCount +) { + + @QueryProjection + public AdminFestivalV1Response { + } +} diff --git a/backend/src/main/java/com/festago/admin/presentation/v1/AdminFestivalV1Controller.java b/backend/src/main/java/com/festago/admin/presentation/v1/AdminFestivalV1Controller.java index c77cfdd50..a467ade4f 100644 --- a/backend/src/main/java/com/festago/admin/presentation/v1/AdminFestivalV1Controller.java +++ b/backend/src/main/java/com/festago/admin/presentation/v1/AdminFestivalV1Controller.java @@ -1,19 +1,28 @@ package com.festago.admin.presentation.v1; +import com.festago.admin.application.AdminFestivalV1QueryService; +import com.festago.admin.dto.AdminFestivalV1Response; import com.festago.admin.dto.FestivalV1CreateRequest; import com.festago.admin.dto.FestivalV1UpdateRequest; +import com.festago.common.aop.ValidPageable; +import com.festago.common.querydsl.SearchCondition; import com.festago.festival.application.command.FestivalCommandFacadeService; import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.Valid; import java.net.URI; 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.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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; @RestController @@ -22,8 +31,20 @@ @Hidden public class AdminFestivalV1Controller { + private final AdminFestivalV1QueryService adminFestivalV1QueryService; private final FestivalCommandFacadeService festivalCommandFacadeService; + @ValidPageable(maxSize = 50) + @GetMapping + public ResponseEntity> findAll( + @RequestParam(defaultValue = "") String searchFilter, + @RequestParam(defaultValue = "") String searchKeyword, + @PageableDefault(size = 10) Pageable pageable + ) { + return ResponseEntity.ok() + .body(adminFestivalV1QueryService.findAll(new SearchCondition(searchFilter, searchKeyword, pageable))); + } + @PostMapping public ResponseEntity createFestival( @RequestBody @Valid FestivalV1CreateRequest request diff --git a/backend/src/main/java/com/festago/admin/repository/AdminFestivalV1QueryDslRepository.java b/backend/src/main/java/com/festago/admin/repository/AdminFestivalV1QueryDslRepository.java new file mode 100644 index 000000000..f4bfdb79e --- /dev/null +++ b/backend/src/main/java/com/festago/admin/repository/AdminFestivalV1QueryDslRepository.java @@ -0,0 +1,97 @@ +package com.festago.admin.repository; + +import static com.festago.festival.domain.QFestival.festival; +import static com.festago.school.domain.QSchool.school; +import static com.festago.stage.domain.QStage.stage; + +import com.festago.admin.dto.AdminFestivalV1Response; +import com.festago.admin.dto.QAdminFestivalV1Response; +import com.festago.common.querydsl.OrderSpecifierUtils; +import com.festago.common.querydsl.QueryDslHelper; +import com.festago.common.querydsl.SearchCondition; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; + +@Repository +@RequiredArgsConstructor +public class AdminFestivalV1QueryDslRepository { + + private final QueryDslHelper queryDslHelper; + + public Page findAll(SearchCondition searchCondition) { + Pageable pageable = searchCondition.pageable(); + String searchFilter = searchCondition.searchFilter(); + String searchKeyword = searchCondition.searchKeyword(); + return queryDslHelper.applyPagination(pageable, + queryFactory -> queryFactory.select( + new QAdminFestivalV1Response( + festival.id, + festival.name, + school.name, + festival.startDate, + festival.endDate, + stage.count() + )) + .from(festival) + .innerJoin(school).on(school.id.eq(festival.school.id)) + .leftJoin(stage).on(stage.festival.id.eq(festival.id)) + .where(applySearchFilter(searchFilter, searchKeyword)) + .groupBy(festival.id) + .orderBy(getOrderSpecifier(pageable.getSort())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()), + queryFactory -> queryFactory.select(festival.count()) + .from(festival) + .where(applySearchFilter(searchFilter, searchKeyword))); + } + + private BooleanExpression applySearchFilter(String searchFilter, String searchKeyword) { + return switch (searchFilter) { + case "id" -> eqId(searchKeyword); + case "name" -> containsName(searchKeyword); + case "schoolName" -> containsSchoolName(searchKeyword); + default -> null; + }; + } + + private BooleanExpression eqId(String searchKeyword) { + if (StringUtils.hasText(searchKeyword)) { + return festival.id.stringValue().eq(searchKeyword); + } + return null; + } + + private BooleanExpression containsName(String searchKeyword) { + if (StringUtils.hasText(searchKeyword)) { + return festival.name.contains(searchKeyword); + } + return null; + } + + private BooleanExpression containsSchoolName(String searchKeyword) { + if (StringUtils.hasText(searchKeyword)) { + return school.name.contains(searchKeyword); + } + return null; + } + + private OrderSpecifier getOrderSpecifier(Sort sort) { + return sort.stream() + .findFirst() + .map(it -> switch (it.getProperty()) { + case "id" -> OrderSpecifierUtils.of(it.getDirection(), festival.id); + case "name" -> OrderSpecifierUtils.of(it.getDirection(), festival.name); + case "schoolName" -> OrderSpecifierUtils.of(it.getDirection(), school.name); + case "startDate" -> OrderSpecifierUtils.of(it.getDirection(), festival.startDate); + case "endDate" -> OrderSpecifierUtils.of(it.getDirection(), festival.endDate); + default -> OrderSpecifierUtils.NULL; + }) + .orElse(OrderSpecifierUtils.NULL); + } +} diff --git a/backend/src/main/java/com/festago/common/querydsl/OrderSpecifierUtils.java b/backend/src/main/java/com/festago/common/querydsl/OrderSpecifierUtils.java new file mode 100644 index 000000000..8c9befb5a --- /dev/null +++ b/backend/src/main/java/com/festago/common/querydsl/OrderSpecifierUtils.java @@ -0,0 +1,20 @@ +package com.festago.common.querydsl; + +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.NullExpression; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import org.springframework.data.domain.Sort; + +public class OrderSpecifierUtils { + + public static final OrderSpecifier NULL = new OrderSpecifier(Order.ASC, NullExpression.DEFAULT, + OrderSpecifier.NullHandling.Default); + + private OrderSpecifierUtils() { + } + + public static OrderSpecifier of(Sort.Direction direction, Expression target) { + return new OrderSpecifier(direction.isAscending() ? Order.ASC : Order.DESC, target); + } +} diff --git a/backend/src/main/java/com/festago/common/querydsl/QueryDslHelper.java b/backend/src/main/java/com/festago/common/querydsl/QueryDslHelper.java new file mode 100644 index 000000000..bc47fdc3e --- /dev/null +++ b/backend/src/main/java/com/festago/common/querydsl/QueryDslHelper.java @@ -0,0 +1,39 @@ +package com.festago.common.querydsl; + +import com.querydsl.core.types.Expression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class QueryDslHelper { + + private final JPAQueryFactory queryFactory; + + public JPAQuery select(Expression expr) { + return queryFactory.select(expr); + } + + public Optional fetchOne(Function> queryFunction) { + JPAQuery query = queryFunction.apply(queryFactory); + return Optional.ofNullable(query.fetchOne()); + } + + public Page applyPagination( + Pageable pageable, + Function> contentQueryFunction, + Function> countQueryFunction + ) { + List content = contentQueryFunction.apply(queryFactory).fetch(); + JPAQuery countQuery = countQueryFunction.apply(queryFactory); + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } +} diff --git a/backend/src/test/java/com/festago/admin/application/integration/AdminFestivalV1QueryServiceIntegrationTest.java b/backend/src/test/java/com/festago/admin/application/integration/AdminFestivalV1QueryServiceIntegrationTest.java new file mode 100644 index 000000000..180cde1f3 --- /dev/null +++ b/backend/src/test/java/com/festago/admin/application/integration/AdminFestivalV1QueryServiceIntegrationTest.java @@ -0,0 +1,269 @@ +package com.festago.admin.application.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.festago.admin.application.AdminFestivalV1QueryService; +import com.festago.admin.dto.AdminFestivalV1Response; +import com.festago.common.querydsl.SearchCondition; +import com.festago.festival.domain.Festival; +import com.festago.festival.repository.FestivalRepository; +import com.festago.school.domain.School; +import com.festago.school.domain.SchoolRegion; +import com.festago.school.repository.SchoolRepository; +import com.festago.stage.domain.Stage; +import com.festago.stage.repository.StageRepository; +import com.festago.support.ApplicationIntegrationTest; +import java.time.LocalDate; +import java.time.LocalDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AdminFestivalV1QueryServiceIntegrationTest extends ApplicationIntegrationTest { + + @Autowired + AdminFestivalV1QueryService adminFestivalV1QueryService; + + @Autowired + FestivalRepository festivalRepository; + + @Autowired + SchoolRepository schoolRepository; + + @Autowired + StageRepository stageRepository; + + LocalDate now = LocalDate.parse("2077-06-30"); + LocalDate tomorrow = now.plusDays(1); + + Festival 테코대학교_축제; + Festival 테코대학교_공연_없는_축제; + Festival 우테대학교_축제; + Stage 테코대학교_공연; + Stage 우테대학교_첫째날_공연; + Stage 우테대학교_둘째날_공연; + + @BeforeEach + void setUp() { + LocalDateTime ticketOpenTime = now.atStartOfDay().minusWeeks(1); + School 테코대학교 = schoolRepository.save(new School("teco.ac.kr", "테코대학교", SchoolRegion.서울)); + School 우테대학교 = schoolRepository.save(new School("wote.ac.kr", "우테대학교", SchoolRegion.서울)); + 테코대학교_축제 = festivalRepository.save(new Festival("테코대학교 축제", now, now, 테코대학교)); + 테코대학교_공연_없는_축제 = festivalRepository.save(new Festival("테코대학교 공연 없는 축제", tomorrow, tomorrow, 테코대학교)); + 우테대학교_축제 = festivalRepository.save(new Festival("우테대학교 축제", now, tomorrow, 우테대학교)); + 테코대학교_공연 = stageRepository.save(new Stage(now.atTime(18, 0), ticketOpenTime, 테코대학교_축제)); + 우테대학교_첫째날_공연 = stageRepository.save(new Stage(now.atTime(18, 0), ticketOpenTime, 우테대학교_축제)); + 우테대학교_둘째날_공연 = stageRepository.save(new Stage(tomorrow.atTime(18, 0), ticketOpenTime, 우테대학교_축제)); + } + + @Nested + class findAll { + + @Test + void 페이지네이션이_적용되어야_한다() { + // given + Pageable pageable = PageRequest.ofSize(2); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + assertSoftly(softly -> { + softly.assertThat(response.getSize()).isEqualTo(2); + softly.assertThat(response.getTotalPages()).isEqualTo(2); + softly.assertThat(response.getTotalElements()).isEqualTo(3); + }); + } + + @Test + void 공연의_수가_정확하게_반환되어야_한다() { + // given + Pageable pageable = PageRequest.ofSize(10); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + // then + assertThat(response.getContent()) + .map(AdminFestivalV1Response::stageCount) + .containsExactly(1L, 0L, 2L); + } + + @Nested + class 정렬 { + + @Test + void 축제의_식별자로_정렬_되어야_한다() { + // given + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "id")); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + // then + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(우테대학교_축제.getId(), 테코대학교_공연_없는_축제.getId(), 테코대학교_축제.getId()); + } + + @Test + void 축제의_이름으로_정렬이_되어야_한다() { + // given + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "name")); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + // then + assertThat(response.getContent()) + .map(AdminFestivalV1Response::name) + .containsExactly(우테대학교_축제.getName(), 테코대학교_공연_없는_축제.getName(), 테코대학교_축제.getName()); + } + + @Test + void 학교의_이름으로_정렬이_되어야_한다() { + // given + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "schoolName")); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + // then + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(우테대학교_축제.getId(), 테코대학교_축제.getId(), 테코대학교_공연_없는_축제.getId()); + } + + @Test + void 축제의_시작일으로_정렬이_되어야_한다() { + // given + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "startDate")); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + // then + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(테코대학교_축제.getId(), 우테대학교_축제.getId(), 테코대학교_공연_없는_축제.getId()); + } + + @Test + void 축제의_종료일으로_정렬이_되어야_한다() { + // given + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "endDate")); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + // then + System.out.println(response.getContent()); + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(테코대학교_공연_없는_축제.getId(), 우테대학교_축제.getId(), 테코대학교_축제.getId()); + } + + @Test + void 정렬_조건에_없으면_식별자의_오름차순으로_정렬이_되어야_한다() { + // given + Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "foo")); + SearchCondition searchCondition = new SearchCondition("", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + // then + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(테코대학교_축제.getId(), 테코대학교_공연_없는_축제.getId(), 우테대학교_축제.getId()); + } + } + + @Nested + class 검색 { + + @Test + void 축제의_식별자로_검색이_되어야_한다() { + // given + Pageable pageable = Pageable.ofSize(10); + SearchCondition searchCondition = new SearchCondition("id", 테코대학교_축제.getId().toString(), pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(테코대학교_축제.getId()); + } + + @Test + void 축제의_이름이_포함된_검색이_되어야_한다() { + // given + Pageable pageable = Pageable.ofSize(10); + SearchCondition searchCondition = new SearchCondition("name", "테코대학교", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(테코대학교_축제.getId(), 테코대학교_공연_없는_축제.getId()); + } + + @Test + void 학교의_이름이_포함된_검색이_되어야_한다() { + // given + Pageable pageable = Pageable.ofSize(10); + SearchCondition searchCondition = new SearchCondition("schoolName", "우테대학교", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + assertThat(response.getContent()) + .map(AdminFestivalV1Response::id) + .containsExactly(우테대학교_축제.getId()); + } + + @Test + void 검색_필터가_비어있으면_필터링이_적용되지_않는다() { + // given + Pageable pageable = Pageable.ofSize(10); + SearchCondition searchCondition = new SearchCondition("", "글렌", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + assertThat(response.getContent()) + .hasSize(3); + } + + @Test + void 검색어가_비어있으면_필터링이_적용되지_않는다() { + // given + Pageable pageable = Pageable.ofSize(10); + SearchCondition searchCondition = new SearchCondition("id", "", pageable); + + // when + var response = adminFestivalV1QueryService.findAll(searchCondition); + + assertThat(response.getContent()) + .hasSize(3); + } + } + } +} diff --git a/backend/src/test/java/com/festago/admin/presentation/v1/AdminFestivalV1ControllerTest.java b/backend/src/test/java/com/festago/admin/presentation/v1/AdminFestivalV1ControllerTest.java index e43585c07..15a9545bd 100644 --- a/backend/src/test/java/com/festago/admin/presentation/v1/AdminFestivalV1ControllerTest.java +++ b/backend/src/test/java/com/festago/admin/presentation/v1/AdminFestivalV1ControllerTest.java @@ -3,14 +3,19 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import com.festago.admin.application.AdminFestivalV1QueryService; +import com.festago.admin.dto.AdminFestivalV1Response; import com.festago.admin.dto.FestivalV1UpdateRequest; import com.festago.auth.domain.Role; +import com.festago.common.querydsl.SearchCondition; import com.festago.festival.application.command.FestivalCommandFacadeService; import com.festago.festival.dto.FestivalCreateRequest; import com.festago.festival.dto.command.FestivalCreateCommand; @@ -18,12 +23,14 @@ import com.festago.support.WithMockAuth; import jakarta.servlet.http.Cookie; import java.time.LocalDate; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -44,6 +51,9 @@ class AdminFestivalV1ControllerTest { @Autowired FestivalCommandFacadeService festivalCommandFacadeService; + @Autowired + AdminFestivalV1QueryService adminFestivalV1QueryService; + @Nested class 축제_생성 { @@ -172,4 +182,49 @@ class 올바른_주소로 { } } } + + @Nested + class 모든_축제_정보_조회 { + + final String uri = "/admin/api/v1/festivals"; + + @Nested + @DisplayName("GET " + uri) + class 올바른_주소로 { + + @Test + @WithMockAuth(role = Role.ADMIN) + void 요청을_하면_200_응답과_학교_정보_목록이_반환된다() throws Exception { + // given + var expected = List.of( + new AdminFestivalV1Response(1L, "테코대학교 축제", "테코대학교", LocalDate.now(), LocalDate.now(), 0) + ); + given(adminFestivalV1QueryService.findAll(any(SearchCondition.class))) + .willReturn(new PageImpl<>(expected)); + + // when & then + mockMvc.perform(get(uri) + .contentType(MediaType.APPLICATION_JSON) + .cookie(TOKEN_COOKIE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content.size()").value(1)); + } + + @Test + void 토큰_없이_보내면_401_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(get(uri)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockAuth(role = Role.MEMBER) + void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(get(uri) + .cookie(TOKEN_COOKIE)) + .andExpect(status().isNotFound()); + } + } + } } From 465a484189a2d6e5b8b35290e44e299ed425a40b Mon Sep 17 00:00:00 2001 From: SeongHoonC <108349655+SeongHoonC@users.noreply.github.com> Date: Wed, 6 Mar 2024 13:27:29 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[AN/USER]=20feat:=20=EB=B0=94=ED=85=80=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=83=81?= =?UTF-8?q?=EB=8B=A8=EC=97=90=20=EC=A4=84=20=EC=B6=94=EA=B0=80(#766)=20(#7?= =?UTF-8?q?67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(HomeActivity): 네비게이션 상단에 줄 추가 * feat(HomeActivity): 네비게이션 상단에 줄 아이디 정의 --- .../presentation/src/main/res/layout/activity_home.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/android/festago/presentation/src/main/res/layout/activity_home.xml b/android/festago/presentation/src/main/res/layout/activity_home.xml index e7ef18b23..984cbe640 100644 --- a/android/festago/presentation/src/main/res/layout/activity_home.xml +++ b/android/festago/presentation/src/main/res/layout/activity_home.xml @@ -25,5 +25,12 @@ app:layout_constraintBottom_toBottomOf="parent" app:menu="@menu/menu_bottom_navigation" /> + + From 0bcd8d73268b446b77c59b845a18f85ddc55b5d9 Mon Sep 17 00:00:00 2001 From: SeongHoonC <108349655+SeongHoonC@users.noreply.github.com> Date: Wed, 6 Mar 2024 13:28:15 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[AN/USER]=20feat:=20=EC=B6=95=EC=A0=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=99=94=EB=A9=B4=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=EC=9E=91=EC=97=85(#673)=20(#724)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(FakeFestivalRepository): 가짜 축제 저장소를 필터링 작업에 용이하게 변경 * feat(FestivalFilterUiState): FestivalFilterUiState 는 tabPosition 을 가진다 * feat(FestivalTabUiState): selected 람다에 포지션이 아닌 FestivalFilterUiState 를 넘긴다 * feat(FakeFestivalRepository): 가짜 축제 저장소는 예정된 축제를 반환할 수 있다 * feat(FestivalListViewModel): 같은 필터가 아니면 축제 목록을 변경한다 * feat(FestivalListMoreItemViewHolder): more item 프로그래스바 뷰 아이템 생성 * feat(FestivalListAdapter): 축제 목록에 더보기 아이템 추가 * feat(FestivalListAdapter): isLast 가 false 일때만 더보기를 추가한다. * feat(FestivalListViewModel): 현재 축제 목록 불러오기 함수 분리 * feat(FestivalList): Success State recyclerView get items 로직 병합 * feat(FestivalListViewModel): 축제 목록 초기화와 축제 목록 추가 로드를 분리한다 --- .../data/repository/FakeFestivalRepository.kt | 21 +++++-- .../festago/data/repository/FakeFestivals.kt | 10 ++-- .../home/festivallist/FestivalListFragment.kt | 55 ++++++++++++------- .../festivallist/FestivalListViewModel.kt | 53 +++++++++++++++--- .../festival/FestivalListAdapter.kt | 10 +++- .../FestivalListMoreItemViewHolder.kt | 24 ++++++++ .../festival/FestivalListTabViewHolder.kt | 28 ++++++---- .../uistate/FestivalFilterUiState.kt | 4 +- .../uistate/FestivalListUiState.kt | 1 + .../uistate/FestivalMoreItemUiState.kt | 3 + .../uistate/FestivalTabUiState.kt | 3 +- .../layout/item_festival_list_more_item.xml | 19 +++++++ 12 files changed, 179 insertions(+), 52 deletions(-) create mode 100644 android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListMoreItemViewHolder.kt create mode 100644 android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalMoreItemUiState.kt create mode 100644 android/festago/presentation/src/main/res/layout/item_festival_list_more_item.xml diff --git a/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivalRepository.kt b/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivalRepository.kt index 1831a1e5c..f17408dd5 100644 --- a/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivalRepository.kt +++ b/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivalRepository.kt @@ -14,7 +14,12 @@ import javax.inject.Inject class FakeFestivalRepository @Inject constructor() : FestivalRepository { override suspend fun loadPopularFestivals(): Result { - return Result.success(PopularFestivals("인기 축제 목록", FakeFestivals.popularFestivals)) + return Result.success( + PopularFestivals( + title = "인기 축제 목록", + festivals = FakeFestivals.popularFestivals, + ), + ) } override suspend fun loadFestivals( @@ -27,18 +32,24 @@ class FakeFestivalRepository @Inject constructor() : FestivalRepository { val notNullSize = size ?: DEFAULT_SIZE val notNullLastFestivalId = lastFestivalId ?: DEFAULT_LAST_FESTIVAL_ID + if (festivalFilter == FestivalFilter.PLANNED) { + return Result.success( + FestivalsPage(isLastPage = true, festivals = FakeFestivals.plannedFestivals), + ) + } + if (notNullLastFestivalId + notNullSize < LAST_ITEM_ID) { return Result.success( FestivalsPage( - false, - getFestivals((notNullLastFestivalId + 1)..(notNullLastFestivalId + notNullSize)), + isLastPage = false, + festivals = getFestivals((notNullLastFestivalId + 1)..(notNullLastFestivalId + notNullSize)), ), ) } return Result.success( FestivalsPage( - true, - getFestivals((notNullLastFestivalId + 1)..LAST_ITEM_ID), + isLastPage = true, + festivals = getFestivals((notNullLastFestivalId + 1)..LAST_ITEM_ID), ), ) } diff --git a/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivals.kt b/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivals.kt index 7e0986b3d..53080dbed 100644 --- a/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivals.kt +++ b/android/festago/data/src/main/java/com/festago/festago/data/repository/FakeFestivals.kt @@ -127,7 +127,7 @@ object FakeFestivals { val plannedFestivals = listOf( Festival( - id = 1, + id = 30, name = "뉴진스 콘서트", startDate = LocalDate.now().plusDays(1L), endDate = LocalDate.MAX, @@ -142,7 +142,7 @@ object FakeFestivals { ), ), Festival( - id = 2, + id = 31, name = "아이브 콘서트", startDate = LocalDate.now().plusDays(3L), endDate = LocalDate.MAX, @@ -177,7 +177,7 @@ object FakeFestivals { ), ), Festival( - id = 3, + id = 32, name = "아이들 콘서트", startDate = LocalDate.now().plusDays(5L), endDate = LocalDate.MAX, @@ -192,7 +192,7 @@ object FakeFestivals { ), ), Festival( - id = 4, + id = 33, name = "뉴진스 콘서트", startDate = LocalDate.now().plusDays(20L), endDate = LocalDate.MAX, @@ -207,7 +207,7 @@ object FakeFestivals { ), ), Festival( - id = 5, + id = 34, name = "아이브 콘서트", startDate = LocalDate.now().plusDays(40L), endDate = LocalDate.MAX, diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt index d0930335c..38213de80 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt @@ -12,13 +12,14 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.festago.festago.presentation.R import com.festago.festago.presentation.databinding.FragmentFestivalListBinding import com.festago.festago.presentation.ui.artistdetail.ArtistDetailFragment import com.festago.festago.presentation.ui.home.festivallist.festival.FestivalListAdapter -import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalFilterUiState import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalListUiState +import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalMoreItemUiState import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalTabUiState import com.festago.festago.presentation.ui.schooldetail.SchoolDetailFragment import com.festago.festago.presentation.util.repeatOnStarted @@ -66,7 +67,7 @@ class FestivalListFragment : Fragment() { } private fun initView() { - vm.loadFestivals() + vm.initFestivalList() initViewPager() initRecyclerView() initRefresh() @@ -74,7 +75,7 @@ class FestivalListFragment : Fragment() { private fun initRefresh() { binding.srlFestivalList.setOnRefreshListener { - vm.loadFestivals() + vm.initFestivalList() binding.srlFestivalList.isRefreshing = false } binding.ivSearch.setOnClickListener { // 임시 연결 @@ -97,6 +98,30 @@ class FestivalListFragment : Fragment() { } private fun initRecyclerView() { + initScrollEvent() + initDecoration() + } + + private fun initScrollEvent() { + binding.rvFestivalList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val festivalListUiState = vm.uiState.value as? FestivalListUiState.Success ?: return + if (festivalListUiState.isLastPage) return + + val lastVisibleItemPosition = + (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition() + + val itemTotalCount = recyclerView.adapter!!.itemCount - 1 + if (lastVisibleItemPosition == itemTotalCount) { + vm.loadFestivals() + } + } + }) + } + + private fun initDecoration() { binding.rvFestivalList.addItemDecoration(object : RecyclerView.ItemDecoration() { override fun getItemOffsets( outRect: Rect, @@ -135,22 +160,14 @@ class FestivalListFragment : Fragment() { } private fun FestivalListUiState.Success.getItems(): List { - val items = mutableListOf() - if (popularFestivalUiState.festivals.isNotEmpty()) { - items.add(popularFestivalUiState) - } - items.add( - FestivalTabUiState { - val festivalFilter = when (it) { - 0 -> FestivalFilterUiState.PROGRESS - 1 -> FestivalFilterUiState.PLANNED - else -> FestivalFilterUiState.PROGRESS - } - vm.loadFestivals(festivalFilter) - }, - ) - items.addAll(festivals) - return items.toList() + return mutableListOf().apply { + if (popularFestivalUiState.festivals.isNotEmpty()) { + add(popularFestivalUiState) + } + add(FestivalTabUiState(festivalFilter) { vm.loadFestivals(it) }) + addAll(festivals) + if (!isLastPage) add(FestivalMoreItemUiState) + }.toList() } private fun showSchoolDetail() { diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt index fb362b89d..77e194129 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListViewModel.kt @@ -32,16 +32,12 @@ class FestivalListViewModel @Inject constructor( private var festivalFilter: FestivalFilter = FestivalFilter.PROGRESS - fun loadFestivals(festivalFilterUiState: FestivalFilterUiState? = null) { + fun initFestivalList() { viewModelScope.launch { - if (festivalFilterUiState != null) { - festivalFilter = festivalFilterUiState.toDomain() - } - val deferredPopularFestivals = async { festivalRepository.loadPopularFestivals() } - val deferredFestivals = - async { festivalRepository.loadFestivals(festivalFilter = festivalFilter) } - + val deferredFestivals = async { + festivalRepository.loadFestivals(festivalFilter = festivalFilter) + } runCatching { val festivalsPage = deferredFestivals.await().getOrThrow() val popularFestivals = deferredPopularFestivals.await().getOrThrow() @@ -52,6 +48,7 @@ class FestivalListViewModel @Inject constructor( festivals = popularFestivals.festivals.map { it.toUiState() }, ), festivals = festivalsPage.festivals.map { it.toUiState() }, + festivalFilter = festivalFilter.toUiState(), isLastPage = festivalsPage.isLastPage, ) }.onFailure { @@ -64,11 +61,51 @@ class FestivalListViewModel @Inject constructor( } } + fun loadFestivals(festivalFilterUiState: FestivalFilterUiState? = null) { + val successUiState = uiState.value as? FestivalListUiState.Success ?: return + + viewModelScope.launch { + val currentFestivals = getCurrentFestivals(festivalFilterUiState) + + festivalRepository.loadFestivals( + festivalFilter = festivalFilter, + lastFestivalId = currentFestivals.lastOrNull()?.id, + lastStartDate = currentFestivals.lastOrNull()?.startDate, + ).onSuccess { festivalsPage -> + _uiState.value = FestivalListUiState.Success( + PopularFestivalUiState( + title = successUiState.popularFestivalUiState.title, + festivals = successUiState.popularFestivalUiState.festivals, + ), + festivals = currentFestivals + festivalsPage.festivals.map { it.toUiState() }, + festivalFilter = festivalFilter.toUiState(), + isLastPage = festivalsPage.isLastPage, + ) + } + } + } + + private fun getCurrentFestivals(festivalFilterUiState: FestivalFilterUiState?): List { + var festivals = (uiState.value as? FestivalListUiState.Success)?.festivals ?: listOf() + + if (festivalFilterUiState != null && festivalFilter != festivalFilterUiState.toDomain()) { + festivalFilter = festivalFilterUiState.toDomain() + festivals = listOf() + } + return festivals + } + private fun FestivalFilterUiState.toDomain() = when (this) { FestivalFilterUiState.PLANNED -> FestivalFilter.PLANNED FestivalFilterUiState.PROGRESS -> FestivalFilter.PROGRESS } + private fun FestivalFilter.toUiState() = when (this) { + FestivalFilter.PLANNED -> FestivalFilterUiState.PLANNED + FestivalFilter.PROGRESS -> FestivalFilterUiState.PROGRESS + else -> FestivalFilterUiState.PLANNED + } + private fun Festival.toUiState() = FestivalItemUiState( id = id, name = name, diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListAdapter.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListAdapter.kt index 4215c3fb2..abbb0c0b3 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListAdapter.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListAdapter.kt @@ -4,6 +4,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalItemUiState +import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalMoreItemUiState import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalTabUiState import com.festago.festago.presentation.ui.home.festivallist.uistate.PopularFestivalUiState @@ -16,6 +17,7 @@ class FestivalListAdapter( 1 -> FestivalListPopularViewHolder.of(parent) 2 -> FestivalListFestivalViewHolder.of(parent, onArtistClick) 3 -> FestivalListTabViewHolder.of(parent) + 4 -> FestivalListMoreItemViewHolder.of(parent) else -> throw IllegalArgumentException("Invalid viewType") } } @@ -26,6 +28,7 @@ class FestivalListAdapter( is FestivalListPopularViewHolder -> holder.bind(item as PopularFestivalUiState) is FestivalListFestivalViewHolder -> holder.bind(item as FestivalItemUiState) is FestivalListTabViewHolder -> holder.bind(item as FestivalTabUiState) + is FestivalListMoreItemViewHolder -> holder.bind(item as FestivalMoreItemUiState) } } @@ -34,6 +37,7 @@ class FestivalListAdapter( is PopularFestivalUiState -> 1 is FestivalItemUiState -> 2 is FestivalTabUiState -> 3 + is FestivalMoreItemUiState -> 4 else -> throw IllegalArgumentException("Invalid item") } } @@ -44,6 +48,7 @@ class FestivalListAdapter( oldItem is PopularFestivalUiState && newItem is PopularFestivalUiState -> true oldItem is FestivalItemUiState && newItem is FestivalItemUiState -> oldItem.id == newItem.id oldItem is FestivalTabUiState && newItem is FestivalTabUiState -> true + oldItem is FestivalMoreItemUiState && newItem is FestivalMoreItemUiState -> true else -> false } @@ -54,8 +59,9 @@ class FestivalListAdapter( oldItem is FestivalItemUiState && newItem is FestivalItemUiState -> oldItem as FestivalItemUiState == newItem - oldItem is FestivalTabUiState && newItem is FestivalTabUiState - -> true + oldItem is FestivalTabUiState && newItem is FestivalTabUiState -> true + + oldItem is FestivalMoreItemUiState && newItem is FestivalMoreItemUiState -> true else -> false } diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListMoreItemViewHolder.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListMoreItemViewHolder.kt new file mode 100644 index 000000000..3d5000844 --- /dev/null +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListMoreItemViewHolder.kt @@ -0,0 +1,24 @@ +package com.festago.festago.presentation.ui.home.festivallist.festival + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.festago.festago.presentation.databinding.ItemFestivalListMoreItemBinding +import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalMoreItemUiState + +class FestivalListMoreItemViewHolder(val binding: ItemFestivalListMoreItemBinding) : + FestivalListViewHolder(binding) { + + fun bind(festivalMoreItemUiState: FestivalMoreItemUiState) { + } + + companion object { + fun of(parent: ViewGroup): FestivalListMoreItemViewHolder { + val binding = ItemFestivalListMoreItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ) + return FestivalListMoreItemViewHolder(binding) + } + } +} diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListTabViewHolder.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListTabViewHolder.kt index dcfd2f13f..9db20206a 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListTabViewHolder.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/festival/FestivalListTabViewHolder.kt @@ -4,6 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import com.festago.festago.presentation.R import com.festago.festago.presentation.databinding.ItemFestivalListTabBinding +import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalFilterUiState import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalTabUiState import com.google.android.material.tabs.TabLayout @@ -23,19 +24,26 @@ class FestivalListTabViewHolder(val binding: ItemFestivalListTabBinding) : } fun bind(festivalTabUiState: FestivalTabUiState) { - binding.tlFestivalListTab.clearOnTabSelectedListeners() - binding.tlFestivalListTab.addOnTabSelectedListener( - object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - festivalTabUiState.onClick(tab.position) - } + with(binding.tlFestivalListTab) { + clearOnTabSelectedListeners() + selectTab(getTabAt(festivalTabUiState.selectedFilter.tabPosition)) + addOnTabSelectedListener( + object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + festivalTabUiState.onFilterSelected(getFestivalFilterAt(tab.position)) + } - override fun onTabUnselected(tab: TabLayout.Tab) = Unit - override fun onTabReselected(tab: TabLayout.Tab) = Unit - }, - ) + override fun onTabUnselected(tab: TabLayout.Tab) = Unit + override fun onTabReselected(tab: TabLayout.Tab) = Unit + }, + ) + } } + private fun getFestivalFilterAt(position: Int): FestivalFilterUiState = + FestivalFilterUiState.values().find { it.tabPosition == position } + ?: FestivalFilterUiState.PROGRESS + companion object { fun of(parent: ViewGroup): FestivalListTabViewHolder { val binding = ItemFestivalListTabBinding.inflate( diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalFilterUiState.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalFilterUiState.kt index 44ade4542..092ad203c 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalFilterUiState.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalFilterUiState.kt @@ -1,5 +1,5 @@ package com.festago.festago.presentation.ui.home.festivallist.uistate -enum class FestivalFilterUiState { - PROGRESS, PLANNED +enum class FestivalFilterUiState(val tabPosition: Int) { + PROGRESS(0), PLANNED(1) } diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt index 0a4ef1618..0d90779c1 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalListUiState.kt @@ -6,6 +6,7 @@ sealed interface FestivalListUiState { data class Success( val popularFestivalUiState: PopularFestivalUiState, val festivals: List, + val festivalFilter: FestivalFilterUiState, val isLastPage: Boolean, ) : FestivalListUiState diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalMoreItemUiState.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalMoreItemUiState.kt new file mode 100644 index 000000000..1cc932b64 --- /dev/null +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalMoreItemUiState.kt @@ -0,0 +1,3 @@ +package com.festago.festago.presentation.ui.home.festivallist.uistate + +object FestivalMoreItemUiState diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalTabUiState.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalTabUiState.kt index fbdfb7147..214a38777 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalTabUiState.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/uistate/FestivalTabUiState.kt @@ -1,5 +1,6 @@ package com.festago.festago.presentation.ui.home.festivallist.uistate data class FestivalTabUiState( - val onClick: (Int) -> Unit, + val selectedFilter: FestivalFilterUiState, + val onFilterSelected: (FestivalFilterUiState) -> Unit, ) diff --git a/android/festago/presentation/src/main/res/layout/item_festival_list_more_item.xml b/android/festago/presentation/src/main/res/layout/item_festival_list_more_item.xml new file mode 100644 index 000000000..41942b174 --- /dev/null +++ b/android/festago/presentation/src/main/res/layout/item_festival_list_more_item.xml @@ -0,0 +1,19 @@ + + + + + + + + + From 1f3cf5181b90de283632b8b79dc48cd20f04f9c8 Mon Sep 17 00:00:00 2001 From: SeongHoonC <108349655+SeongHoonC@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:13:49 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[AN/USER]=20feat:=20=EC=B6=95=EC=A0=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=99=94=EB=A9=B4=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EB=AF=BC=EA=B0=90=EB=8F=84=EB=A5=BC=20=EC=A4=84?= =?UTF-8?q?=EC=9D=B8=EB=8B=A4(#=08764)=20(#765)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(customView): 가로 스크롤 시 세로 스크롤과 중첩되지 않는 커스텀 리사이클러뷰 작성 * feat(FestivalList): 축제 목록 리사이클러뷰의 민감도를 줄인다 * feat(FestivalList): 스와이프 새로고침의 민감도를 줄인다 * feat(FestivalList): 축제 목록 tab 디자인 수정 * feat(FestivalList): 축제 이름에서 대학교 이름을 뺀다 --- .../OrientationAwareRecyclerView.kt | 59 +++++++++++++++++++ .../home/festivallist/FestivalListFragment.kt | 1 + .../res/layout/fragment_festival_list.xml | 2 +- .../res/layout/item_festival_list_popular.xml | 6 +- .../res/layout/item_festival_list_tab.xml | 7 ++- 5 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/customview/OrientationAwareRecyclerView.kt diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/customview/OrientationAwareRecyclerView.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/customview/OrientationAwareRecyclerView.kt new file mode 100644 index 000000000..6cadf13f8 --- /dev/null +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/customview/OrientationAwareRecyclerView.kt @@ -0,0 +1,59 @@ +package com.festago.festago.presentation.ui.customview + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.recyclerview.widget.RecyclerView + +/** + * A RecyclerView that only handles scroll events with the same orientation of its LayoutManager. + * Avoids situations where nested recyclerviews don't receive touch events properly: + */ +class OrientationAwareRecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int = 0, +) : RecyclerView(context, attrs, defStyleAttr) { + + private var lastX = 0.0f + private var lastY = 0.0f + private var scrolling = false + + init { + addOnScrollListener(object : OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + scrolling = newState != SCROLL_STATE_IDLE + } + }) + } + + override fun onInterceptTouchEvent(e: MotionEvent): Boolean { + val lm = layoutManager ?: return super.onInterceptTouchEvent(e) + var allowScroll = true + when (e.actionMasked) { + MotionEvent.ACTION_DOWN -> { + lastX = e.x + lastY = e.y + // If we were scrolling, stop now by faking a touch release + if (scrolling) { + val newEvent = MotionEvent.obtain(e) + newEvent.action = MotionEvent.ACTION_UP + return super.onInterceptTouchEvent(newEvent) + } + } + + MotionEvent.ACTION_MOVE -> { + // We're moving, so check if we're trying + // to scroll vertically or horizontally so we don't intercept the wrong event. + val currentX = e.x + val currentY = e.y + val dx = Math.abs(currentX - lastX) + val dy = Math.abs(currentY - lastY) + allowScroll = if (dy > dx) lm.canScrollVertically() else lm.canScrollHorizontally() + } + } + if (!allowScroll) return false + return super.onInterceptTouchEvent(e) + } +} diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt index 38213de80..47ebe1b2d 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt @@ -78,6 +78,7 @@ class FestivalListFragment : Fragment() { vm.initFestivalList() binding.srlFestivalList.isRefreshing = false } + binding.srlFestivalList.setDistanceToTriggerSync(400) binding.ivSearch.setOnClickListener { // 임시 연결 showSchoolDetail() } diff --git a/android/festago/presentation/src/main/res/layout/fragment_festival_list.xml b/android/festago/presentation/src/main/res/layout/fragment_festival_list.xml index 2020f8f84..70e55fb23 100644 --- a/android/festago/presentation/src/main/res/layout/fragment_festival_list.xml +++ b/android/festago/presentation/src/main/res/layout/fragment_festival_list.xml @@ -69,7 +69,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/containerAppBarFestivalList"> - + tools:text="연세대 아카라카 연세대 아카라카 연세대 아카라카" /> + + From ee2cfc9b529ce3d10a038225397ee7e232d1d074 Mon Sep 17 00:00:00 2001 From: SeongHoonC <108349655+SeongHoonC@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:14:22 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[AN/USER]=20feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=9C?= =?UTF-8?q?=EB=8B=A4(#771)=20(#772)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(NotificationList): 알림 목록 화면 생성 * feat(NotificationList): 뒤로가기 클릭 시 알림 목록 종료 * feat(NotificationList): 알림 목록 뷰모델 생성 * reactor(NotificationList): 뒤로가기 클릭 함수 분리 --- .../presentation/src/main/AndroidManifest.xml | 5 +- .../home/festivallist/FestivalListFragment.kt | 8 +++ .../NotificationListActivity.kt | 35 ++++++++++ .../NotificationListViewModel.kt | 5 ++ .../res/layout/activity_notification_list.xml | 69 +++++++++++++++++++ .../src/main/res/values/strings.xml | 5 ++ 6 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListActivity.kt create mode 100644 android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListViewModel.kt create mode 100644 android/festago/presentation/src/main/res/layout/activity_notification_list.xml diff --git a/android/festago/presentation/src/main/AndroidManifest.xml b/android/festago/presentation/src/main/AndroidManifest.xml index 971514f53..4fa2771c1 100644 --- a/android/festago/presentation/src/main/AndroidManifest.xml +++ b/android/festago/presentation/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + - diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt index 47ebe1b2d..89105b822 100644 --- a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/home/festivallist/FestivalListFragment.kt @@ -21,6 +21,7 @@ import com.festago.festago.presentation.ui.home.festivallist.festival.FestivalLi import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalListUiState import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalMoreItemUiState import com.festago.festago.presentation.ui.home.festivallist.uistate.FestivalTabUiState +import com.festago.festago.presentation.ui.notificationlist.NotificationListActivity import com.festago.festago.presentation.ui.schooldetail.SchoolDetailFragment import com.festago.festago.presentation.util.repeatOnStarted import com.festago.festago.presentation.util.setOnApplyWindowInsetsCompatListener @@ -82,6 +83,9 @@ class FestivalListFragment : Fragment() { binding.ivSearch.setOnClickListener { // 임시 연결 showSchoolDetail() } + binding.ivAlarm.setOnClickListener { + showNotificationList() + } } private fun initViewPager() { @@ -178,6 +182,10 @@ class FestivalListFragment : Fragment() { .commit() } + private fun showNotificationList() { + startActivity(NotificationListActivity.getIntent(requireContext())) + } + override fun onDestroyView() { _binding = null super.onDestroyView() diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListActivity.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListActivity.kt new file mode 100644 index 000000000..5706c9d02 --- /dev/null +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListActivity.kt @@ -0,0 +1,35 @@ +package com.festago.festago.presentation.ui.notificationlist + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.festago.festago.presentation.databinding.ActivityNotificationListBinding + +class NotificationListActivity : AppCompatActivity() { + + private val binding: ActivityNotificationListBinding by lazy { + ActivityNotificationListBinding.inflate(layoutInflater) + } + + private val vm: NotificationListViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + initBackPressed() + } + + private fun initBackPressed() { + binding.ivBack.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + } + + companion object { + fun getIntent(context: Context): Intent { + return Intent(context, NotificationListActivity::class.java) + } + } +} diff --git a/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListViewModel.kt b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListViewModel.kt new file mode 100644 index 000000000..e00cd54e6 --- /dev/null +++ b/android/festago/presentation/src/main/java/com/festago/festago/presentation/ui/notificationlist/NotificationListViewModel.kt @@ -0,0 +1,5 @@ +package com.festago.festago.presentation.ui.notificationlist + +import androidx.lifecycle.ViewModel + +class NotificationListViewModel : ViewModel() diff --git a/android/festago/presentation/src/main/res/layout/activity_notification_list.xml b/android/festago/presentation/src/main/res/layout/activity_notification_list.xml new file mode 100644 index 000000000..c637ebb1a --- /dev/null +++ b/android/festago/presentation/src/main/res/layout/activity_notification_list.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/festago/presentation/src/main/res/values/strings.xml b/android/festago/presentation/src/main/res/values/strings.xml index 031392955..1af05206f 100644 --- a/android/festago/presentation/src/main/res/values/strings.xml +++ b/android/festago/presentation/src/main/res/values/strings.xml @@ -22,4 +22,9 @@ D%1$s + + 알림 + 아직 도착한 알림이 없어요 + 새로운 소식이 도착하면 알려드릴게요 +