From b3b1537131e8e3fd7ed18fa121275b3ad01fc809 Mon Sep 17 00:00:00 2001 From: WooJJam <111514410+WooJJam@users.noreply.github.com> Date: Wed, 13 May 2026 16:37:45 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20[NDGL-133]=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=91=EB=8B=B5=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ndgl/common/response/PageResponse.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 common/src/main/java/com/yapp/ndgl/common/response/PageResponse.java diff --git a/common/src/main/java/com/yapp/ndgl/common/response/PageResponse.java b/common/src/main/java/com/yapp/ndgl/common/response/PageResponse.java new file mode 100644 index 0000000..3fa8655 --- /dev/null +++ b/common/src/main/java/com/yapp/ndgl/common/response/PageResponse.java @@ -0,0 +1,62 @@ +package com.yapp.ndgl.common.response; + +import java.util.List; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PageResponse { + + private List content; + private int page; + private int size; + private long totalElements; + private int totalPages; + private boolean hasNext; + private boolean hasPrevious; + + @Builder + private PageResponse( + final List content, + final int page, + final int size, + final long totalElements, + final int totalPages, + final boolean hasNext, + final boolean hasPrevious + ) { + this.content = content; + this.page = page; + this.size = size; + this.totalElements = totalElements; + this.totalPages = totalPages; + this.hasNext = hasNext; + this.hasPrevious = hasPrevious; + } + + public static PageResponse of( + final List content, + final int page, + final int size, + final long totalElements + ) { + + int totalPages = (size == 0) ? 0 : (int) Math.ceil((double) totalElements / size); + boolean hasNext = page + 1 < totalPages; + boolean hasPrevious = page > 0; + + return PageResponse.builder() + .content(content) + .page(page) + .size(size) + .totalElements(totalElements) + .totalPages(totalPages) + .hasNext(hasNext) + .hasPrevious(hasPrevious) + .build(); + } +} From fb6abd1bacc9cbd14356d77f8df9645b128a9ded Mon Sep 17 00:00:00 2001 From: WooJJam <111514410+WooJJam@users.noreply.github.com> Date: Wed, 13 May 2026 16:37:52 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20[NDGL-133]=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A0=9C=EC=95=88=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=B3=84=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserSuggestedTemplateRepository.java | 2 +- ...UserSuggestedTemplateRepositoryCustom.java | 12 +++++ .../UserSuggestedTemplateRepositoryImpl.java | 49 +++++++++++++++++++ .../domain/travel/UserSuggestedTemplate.java | 3 ++ .../mapper/UserSuggestedTemplateMapper.java | 1 + .../UserSuggestedTemplateDomainService.java | 12 +++++ 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryCustom.java create mode 100644 domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryImpl.java diff --git a/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepository.java b/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepository.java index 80b4c0b..8974d0e 100644 --- a/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepository.java +++ b/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepository.java @@ -8,7 +8,7 @@ import com.yapp.ndgl.domain.travel.entity.UserSuggestedTemplateEntity; public interface UserSuggestedTemplateRepository - extends JpaRepository { + extends JpaRepository, UserSuggestedTemplateRepositoryCustom { Optional findFirstByVideoIdAndStatus(String videoId, SuggestionStatus status); } diff --git a/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryCustom.java b/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryCustom.java new file mode 100644 index 0000000..4d4c2e5 --- /dev/null +++ b/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryCustom.java @@ -0,0 +1,12 @@ +package com.yapp.ndgl.domain.travel.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.yapp.ndgl.common.type.SuggestionStatus; +import com.yapp.ndgl.domain.travel.entity.UserSuggestedTemplateEntity; + +public interface UserSuggestedTemplateRepositoryCustom { + + Page findByStatus(SuggestionStatus status, Pageable pageable); +} diff --git a/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryImpl.java b/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryImpl.java new file mode 100644 index 0000000..e74951e --- /dev/null +++ b/domain/domain-rdb/src/main/java/com/yapp/ndgl/domain/travel/repository/UserSuggestedTemplateRepositoryImpl.java @@ -0,0 +1,49 @@ +package com.yapp.ndgl.domain.travel.repository; + +import static com.yapp.ndgl.domain.travel.entity.QUserSuggestedTemplateEntity.*; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.yapp.ndgl.common.type.SuggestionStatus; +import com.yapp.ndgl.domain.travel.entity.UserSuggestedTemplateEntity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class UserSuggestedTemplateRepositoryImpl implements UserSuggestedTemplateRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findByStatus( + final SuggestionStatus status, + final Pageable pageable + ) { + BooleanBuilder where = new BooleanBuilder(); + if (status != null) { + where.and(userSuggestedTemplateEntity.status.eq(status)); + } + + List content = queryFactory + .selectFrom(userSuggestedTemplateEntity) + .where(where) + .orderBy(userSuggestedTemplateEntity.createdAt.desc(), userSuggestedTemplateEntity.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(userSuggestedTemplateEntity.count()) + .from(userSuggestedTemplateEntity) + .where(where) + .fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0L : total); + } +} diff --git a/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/UserSuggestedTemplate.java b/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/UserSuggestedTemplate.java index 759f834..72b8c56 100644 --- a/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/UserSuggestedTemplate.java +++ b/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/UserSuggestedTemplate.java @@ -1,5 +1,7 @@ package com.yapp.ndgl.domain.travel; +import java.time.LocalDateTime; + import com.yapp.ndgl.common.type.DomesticRegion; import com.yapp.ndgl.common.type.SuggestionStatus; import com.yapp.ndgl.common.type.TravelCategory; @@ -19,6 +21,7 @@ public class UserSuggestedTemplate { private TravelCategory category; private DomesticRegion region; private SuggestionStatus status; + private LocalDateTime createdAt; public static UserSuggestedTemplate of( final String videoId, diff --git a/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/mapper/UserSuggestedTemplateMapper.java b/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/mapper/UserSuggestedTemplateMapper.java index cc16222..efd09e2 100644 --- a/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/mapper/UserSuggestedTemplateMapper.java +++ b/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/mapper/UserSuggestedTemplateMapper.java @@ -18,6 +18,7 @@ public static UserSuggestedTemplate toDomain(final UserSuggestedTemplateEntity e .category(entity.getCategory()) .region(entity.getRegion()) .status(entity.getStatus()) + .createdAt(entity.getCreatedAt()) .build(); } diff --git a/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/service/UserSuggestedTemplateDomainService.java b/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/service/UserSuggestedTemplateDomainService.java index 2014d21..a52ffb2 100644 --- a/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/service/UserSuggestedTemplateDomainService.java +++ b/domain/domain-service/src/main/java/com/yapp/ndgl/domain/travel/service/UserSuggestedTemplateDomainService.java @@ -2,6 +2,9 @@ import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -70,4 +73,13 @@ public Optional findByVideoIdAndStatus(final String video return userSuggestedTemplateRepository.findFirstByVideoIdAndStatus(videoId, status) .map(UserSuggestedTemplateMapper::toDomain); } + + @Transactional(readOnly = true) + public Page findUserSuggestedTemplates( + final SuggestionStatus status, final int page, final int size + ) { + Pageable pageable = PageRequest.of(page, size); + return userSuggestedTemplateRepository.findByStatus(status, pageable) + .map(UserSuggestedTemplateMapper::toDomain); + } } From 6020b1ac5865de08ed92b544e1969e544987f032 Mon Sep 17 00:00:00 2001 From: WooJJam <111514410+WooJJam@users.noreply.github.com> Date: Wed, 13 May 2026 16:37:59 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20[NDGL-133]=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=9C=EC=95=88=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/build.gradle | 3 + ...inUserSuggestedTemplateViewController.java | 61 +++++ .../AdminUserSuggestedTemplateResponse.java | 35 +++ .../facade/UserSuggestedTemplateFacade.java | 12 + .../service/UserSuggestedTemplateService.java | 18 ++ .../templates/admin/travel-template-list.html | 1 + .../admin/user-suggested-template-list.html | 248 ++++++++++++++++++ 7 files changed, 378 insertions(+) create mode 100644 application/src/main/java/com/yapp/ndgl/application/domains/admin/controller/AdminUserSuggestedTemplateViewController.java create mode 100644 application/src/main/java/com/yapp/ndgl/application/domains/travel/controller/dto/AdminUserSuggestedTemplateResponse.java create mode 100644 application/src/main/resources/templates/admin/user-suggested-template-list.html diff --git a/application/build.gradle b/application/build.gradle index 893cbb2..b831cbe 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -27,6 +27,9 @@ dependencies { // Transaction implementation 'org.springframework:spring-tx' + // Spring Data (Page, Pageable) + implementation 'org.springframework.data:spring-data-commons' + // Thymeleaf implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' diff --git a/application/src/main/java/com/yapp/ndgl/application/domains/admin/controller/AdminUserSuggestedTemplateViewController.java b/application/src/main/java/com/yapp/ndgl/application/domains/admin/controller/AdminUserSuggestedTemplateViewController.java new file mode 100644 index 0000000..0b0e78f --- /dev/null +++ b/application/src/main/java/com/yapp/ndgl/application/domains/admin/controller/AdminUserSuggestedTemplateViewController.java @@ -0,0 +1,61 @@ +package com.yapp.ndgl.application.domains.admin.controller; + +import java.util.List; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import com.yapp.ndgl.application.domains.travel.controller.dto.AdminUserSuggestedTemplateResponse; +import com.yapp.ndgl.application.domains.travel.facade.UserSuggestedTemplateFacade; +import com.yapp.ndgl.common.response.PageResponse; +import com.yapp.ndgl.common.type.SuggestionStatus; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequestMapping("/admin/user-suggested-templates") +@RequiredArgsConstructor +public class AdminUserSuggestedTemplateViewController { + + private final UserSuggestedTemplateFacade userSuggestedTemplateFacade; + + @GetMapping + public String listPage( + @RequestParam(value = "status", required = false) SuggestionStatus status, + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "12") int size, + Model model + ) { + try { + PageResponse result = + userSuggestedTemplateFacade.readUserSuggestedTemplatesForAdmin(status, page, size); + model.addAttribute("templates", result.getContent()); + model.addAttribute("hasNext", result.isHasNext()); + model.addAttribute("hasPrevious", result.isHasPrevious()); + model.addAttribute("totalElements", result.getTotalElements()); + model.addAttribute("totalPages", result.getTotalPages()); + model.addAttribute("currentPage", page); + model.addAttribute("size", size); + model.addAttribute("status", status); + model.addAttribute("statuses", SuggestionStatus.values()); + } catch (Exception e) { + log.error("사용자 제안 템플릿 목록 조회 실패", e); + model.addAttribute("errorMessage", "목록을 불러오는 중 오류가 발생했습니다."); + model.addAttribute("templates", List.of()); + model.addAttribute("hasNext", false); + model.addAttribute("hasPrevious", false); + model.addAttribute("totalElements", 0L); + model.addAttribute("totalPages", 0); + model.addAttribute("currentPage", page); + model.addAttribute("size", size); + model.addAttribute("status", status); + model.addAttribute("statuses", SuggestionStatus.values()); + } + return "admin/user-suggested-template-list"; + } +} diff --git a/application/src/main/java/com/yapp/ndgl/application/domains/travel/controller/dto/AdminUserSuggestedTemplateResponse.java b/application/src/main/java/com/yapp/ndgl/application/domains/travel/controller/dto/AdminUserSuggestedTemplateResponse.java new file mode 100644 index 0000000..190d603 --- /dev/null +++ b/application/src/main/java/com/yapp/ndgl/application/domains/travel/controller/dto/AdminUserSuggestedTemplateResponse.java @@ -0,0 +1,35 @@ +package com.yapp.ndgl.application.domains.travel.controller.dto; + +import java.time.LocalDateTime; + +import com.yapp.ndgl.common.type.DomesticRegion; +import com.yapp.ndgl.common.type.SuggestionStatus; +import com.yapp.ndgl.common.type.TravelCategory; +import com.yapp.ndgl.domain.travel.UserSuggestedTemplate; + +public record AdminUserSuggestedTemplateResponse( + Long id, + String videoId, + String videoLink, + String recommendReason, + String suggesterUuid, + TravelCategory category, + DomesticRegion region, + SuggestionStatus status, + LocalDateTime createdAt +) { + + public static AdminUserSuggestedTemplateResponse toResponse(final UserSuggestedTemplate domain) { + return new AdminUserSuggestedTemplateResponse( + domain.getId(), + domain.getVideoId(), + domain.getVideoLink(), + domain.getRecommendReason(), + domain.getSuggesterUuid(), + domain.getCategory(), + domain.getRegion(), + domain.getStatus(), + domain.getCreatedAt() + ); + } +} diff --git a/application/src/main/java/com/yapp/ndgl/application/domains/travel/facade/UserSuggestedTemplateFacade.java b/application/src/main/java/com/yapp/ndgl/application/domains/travel/facade/UserSuggestedTemplateFacade.java index 53b1026..4e99ae6 100644 --- a/application/src/main/java/com/yapp/ndgl/application/domains/travel/facade/UserSuggestedTemplateFacade.java +++ b/application/src/main/java/com/yapp/ndgl/application/domains/travel/facade/UserSuggestedTemplateFacade.java @@ -1,13 +1,18 @@ package com.yapp.ndgl.application.domains.travel.facade; import com.yapp.ndgl.application.common.annotation.Facade; +import com.yapp.ndgl.application.domains.travel.controller.dto.AdminUserSuggestedTemplateResponse; import com.yapp.ndgl.application.domains.travel.controller.dto.CreateUserSuggestedTemplateRequest; import com.yapp.ndgl.application.domains.travel.event.publisher.UserSuggestedTemplateEventPublisher; import com.yapp.ndgl.application.domains.travel.service.UserSuggestedTemplateService; import com.yapp.ndgl.application.utils.YoutubeUrlParser; +import com.yapp.ndgl.common.response.PageResponse; +import com.yapp.ndgl.common.type.SuggestionStatus; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Facade @RequiredArgsConstructor public class UserSuggestedTemplateFacade { @@ -28,4 +33,11 @@ public void createUserSuggestedTemplate( public void subscribe(final Long templateId, final String uuid) { userSuggestedTemplateService.subscribe(templateId, uuid); } + + public PageResponse readUserSuggestedTemplatesForAdmin( + final SuggestionStatus status, final int page, final int size + ) { + log.info("어드민 사용자 제안 템플릿 목록을 조회합니다. status = {}, page = {}, size = {}", status, page, size); + return userSuggestedTemplateService.readUserSuggestedTemplatesForAdmin(status, page, size); + } } diff --git a/application/src/main/java/com/yapp/ndgl/application/domains/travel/service/UserSuggestedTemplateService.java b/application/src/main/java/com/yapp/ndgl/application/domains/travel/service/UserSuggestedTemplateService.java index b37870a..d139e68 100644 --- a/application/src/main/java/com/yapp/ndgl/application/domains/travel/service/UserSuggestedTemplateService.java +++ b/application/src/main/java/com/yapp/ndgl/application/domains/travel/service/UserSuggestedTemplateService.java @@ -1,11 +1,14 @@ package com.yapp.ndgl.application.domains.travel.service; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.yapp.ndgl.application.domains.travel.controller.dto.AdminUserSuggestedTemplateResponse; import com.yapp.ndgl.application.domains.travel.controller.dto.CreateUserSuggestedTemplateRequest; import com.yapp.ndgl.common.exception.GlobalException; import com.yapp.ndgl.common.exception.TravelErrorCode; +import com.yapp.ndgl.common.response.PageResponse; import com.yapp.ndgl.common.type.SuggestionStatus; import com.yapp.ndgl.domain.travel.UserSuggestedTemplate; import com.yapp.ndgl.application.common.annotation.DistributedLock; @@ -64,4 +67,19 @@ public void subscribe(final Long templateId, final String uuid) { log.info("사용자 제안 여행 템플릿 구독을 신청합니다. templateId={}, subscriberUuid={}", templateId, uuid); userSuggestedTemplateDomainService.subscribe(templateId, uuid); } + + public PageResponse readUserSuggestedTemplatesForAdmin( + final SuggestionStatus status, final int page, final int size + ) { + Page result = userSuggestedTemplateDomainService + .findUserSuggestedTemplates(status, page, size) + .map(AdminUserSuggestedTemplateResponse::toResponse); + + return PageResponse.of( + result.getContent(), + result.getNumber(), + result.getSize(), + result.getTotalElements() + ); + } } diff --git a/application/src/main/resources/templates/admin/travel-template-list.html b/application/src/main/resources/templates/admin/travel-template-list.html index 834cbfe..ba05b74 100644 --- a/application/src/main/resources/templates/admin/travel-template-list.html +++ b/application/src/main/resources/templates/admin/travel-template-list.html @@ -162,6 +162,7 @@

여행 템플릿 목록