From 6f88c819eda693c7f7a29b496239995675e28b1d Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:06:15 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20users=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=EC=97=90=20is=5Fprivate=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/db/migration/V32__add_is_private_to_users.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/db/migration/V32__add_is_private_to_users.sql diff --git a/src/main/resources/db/migration/V32__add_is_private_to_users.sql b/src/main/resources/db/migration/V32__add_is_private_to_users.sql new file mode 100644 index 000000000..b657d0ee8 --- /dev/null +++ b/src/main/resources/db/migration/V32__add_is_private_to_users.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN is_private BOOLEAN NOT NULL DEFAULT FALSE; From 48dc3493ac439891794479e3f61c59a1ff34f67a Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:06:57 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20User=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20isPrivate=20=ED=95=84=EB=93=9C=20=EB=B0=8F?= =?UTF-8?q?=20updatePrivacy()=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/user/user/UserJpaEntity.java | 3 +++ .../persistence/user/user/UserJpaMapper.java | 2 ++ .../clash/domain/user/user/entity/User.java | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaEntity.java b/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaEntity.java index dba0ae005..3a6715084 100644 --- a/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaEntity.java +++ b/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaEntity.java @@ -72,6 +72,9 @@ public class UserJpaEntity { private Instant deletedAt; + @Column(nullable = false) + private boolean isPrivate; + @Enumerated(EnumType.STRING) @Column(nullable = false) private RankTier currentRankTier; diff --git a/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaMapper.java b/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaMapper.java index 38f199d16..8aac658a7 100644 --- a/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaMapper.java +++ b/src/main/java/com/process/clash/adapter/persistence/user/user/UserJpaMapper.java @@ -22,6 +22,7 @@ public UserJpaEntity toJpaEntity(User user) { user.major(), user.userStatus(), user.deletedAt(), + user.isPrivate(), user.currentRankTier(), user.currentExpTier() ); @@ -43,6 +44,7 @@ public User toDomain(UserJpaEntity userJpaEntity) { userJpaEntity.getMajor(), userJpaEntity.getUserStatus(), userJpaEntity.getDeletedAt(), + userJpaEntity.isPrivate(), userJpaEntity.getCurrentRankTier(), userJpaEntity.getCurrentExpTier() ); diff --git a/src/main/java/com/process/clash/domain/user/user/entity/User.java b/src/main/java/com/process/clash/domain/user/user/entity/User.java index 11787cbd0..e87544abf 100644 --- a/src/main/java/com/process/clash/domain/user/user/entity/User.java +++ b/src/main/java/com/process/clash/domain/user/user/entity/User.java @@ -23,6 +23,7 @@ public record User( Major major, UserStatus userStatus, Instant deletedAt, + boolean isPrivate, RankTier currentRankTier, ExpTier currentExpTier ) { @@ -50,6 +51,7 @@ public static User createDefault(String username, String email, String name, Str Major.NONE, UserStatus.PENDING, null, + false, RankTier.NONE, ExpTier.UNRANKED ); @@ -71,6 +73,7 @@ public User submitMajor(Major major) { major, this.userStatus, this.deletedAt, + this.isPrivate, this.currentRankTier, this.currentExpTier ); @@ -92,6 +95,7 @@ public User active() { this.major, UserStatus.ACTIVE, this.deletedAt, + this.isPrivate, this.currentRankTier, this.currentExpTier ); @@ -117,6 +121,7 @@ public User updateSignupInfo(String username, String email, String name, String this.major, this.userStatus, this.deletedAt, + this.isPrivate, this.currentRankTier, this.currentExpTier ); @@ -140,6 +145,7 @@ public User spendCookie(int amount) { this.major, this.userStatus, this.deletedAt, + this.isPrivate, this.currentRankTier, this.currentExpTier ); @@ -163,6 +169,7 @@ public User addCookie(int amount) { this.major, this.userStatus, this.deletedAt, + this.isPrivate, this.currentRankTier, this.currentExpTier ); @@ -184,6 +191,7 @@ public User updateProfileImage(String profileImage) { this.major, this.userStatus, this.deletedAt, + this.isPrivate, this.currentRankTier, this.currentExpTier ); @@ -205,6 +213,7 @@ public User withPassword(String encodedPassword) { this.major, this.userStatus, this.deletedAt, + this.isPrivate, this.currentRankTier, this.currentExpTier ); @@ -227,6 +236,7 @@ public User addExp(int delta) { this.major, this.userStatus, this.deletedAt, + this.isPrivate, this.currentRankTier, ExpTier.fromExp(newTotalExp) ); @@ -248,6 +258,7 @@ public User withRankTier(RankTier rankTier) { this.major, this.userStatus, this.deletedAt, + this.isPrivate, rankTier, this.currentExpTier ); @@ -269,8 +280,18 @@ public User seasonReset(int cookieReward) { this.major, this.userStatus, this.deletedAt, + this.isPrivate, RankTier.NONE, ExpTier.UNRANKED ); } + + public User updatePrivacy(boolean isPrivate) { + return new User( + this.id, this.createdAt, Instant.now(), + this.username, this.email, this.name, this.password, this.role, + this.profileImage, this.totalExp, this.totalCookie, this.major, + this.userStatus, this.deletedAt, isPrivate, this.currentRankTier, this.currentExpTier + ); + } } From 27b4cf694684ba0270c15012862dcf419d659d37 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:07:29 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=91=EA=B7=BC=20=EC=98=88?= =?UTF-8?q?=EC=99=B8(ProfilePrivatedException)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/forbidden/ProfilePrivatedException.java | 10 ++++++++++ .../exception/statuscode/ProfileStatusCode.java | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/process/clash/application/profile/exception/exception/forbidden/ProfilePrivatedException.java diff --git a/src/main/java/com/process/clash/application/profile/exception/exception/forbidden/ProfilePrivatedException.java b/src/main/java/com/process/clash/application/profile/exception/exception/forbidden/ProfilePrivatedException.java new file mode 100644 index 000000000..ad71f9192 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/exception/exception/forbidden/ProfilePrivatedException.java @@ -0,0 +1,10 @@ +package com.process.clash.application.profile.exception.exception.forbidden; + +import com.process.clash.application.common.exception.exception.ForbiddenException; +import com.process.clash.application.profile.exception.statuscode.ProfileStatusCode; + +public class ProfilePrivatedException extends ForbiddenException { + public ProfilePrivatedException() { + super(ProfileStatusCode.PROFILE_PRIVATED); + } +} diff --git a/src/main/java/com/process/clash/application/profile/exception/statuscode/ProfileStatusCode.java b/src/main/java/com/process/clash/application/profile/exception/statuscode/ProfileStatusCode.java index 4df07e3fb..5f347cbd8 100644 --- a/src/main/java/com/process/clash/application/profile/exception/statuscode/ProfileStatusCode.java +++ b/src/main/java/com/process/clash/application/profile/exception/statuscode/ProfileStatusCode.java @@ -11,7 +11,8 @@ public enum ProfileStatusCode implements StatusCode { INVALID_PERIOD_CATEGORY("INVALID_PERIOD_CATEGORY", "잘못된 PeriodCategory입니다.", HttpStatus.BAD_REQUEST), ITEM_NOT_OWNED("ITEM_NOT_OWNED", "보유하지 않은 아이템입니다.", HttpStatus.FORBIDDEN), - INVALID_EQUIPPABLE_ITEM_CATEGORY("INVALID_EQUIPPABLE_ITEM_CATEGORY", "장착 가능한 아이템 카테고리가 아닙니다.", HttpStatus.BAD_REQUEST); + INVALID_EQUIPPABLE_ITEM_CATEGORY("INVALID_EQUIPPABLE_ITEM_CATEGORY", "장착 가능한 아이템 카테고리가 아닙니다.", HttpStatus.BAD_REQUEST), + PROFILE_PRIVATED("PROFILE_PRIVATED", "비공개 프로필입니다.", HttpStatus.FORBIDDEN); private final String code; private final String message; From d02f33f5c3c675b528f605c72036106939de6cbc Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:08:27 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20(GET=20/api/users/{userId})?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/data/GetUserProfileData.java | 42 +++++++++++++++++++ .../port/in/GetUserProfileUsecase.java | 7 ++++ .../service/GetUserProfileService.java | 35 ++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/main/java/com/process/clash/application/profile/data/GetUserProfileData.java create mode 100644 src/main/java/com/process/clash/application/profile/port/in/GetUserProfileUsecase.java create mode 100644 src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java diff --git a/src/main/java/com/process/clash/application/profile/data/GetUserProfileData.java b/src/main/java/com/process/clash/application/profile/data/GetUserProfileData.java new file mode 100644 index 000000000..3dcbe25a9 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/data/GetUserProfileData.java @@ -0,0 +1,42 @@ +package com.process.clash.application.profile.data; + +import com.process.clash.application.realtime.data.UserActivityStatus; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.user.user.entity.User; + +public class GetUserProfileData { + + public record Command( + Long targetUserId + ) {} + + public record Result( + Long id, + String username, + String name, + String profileImage, + Major major, + String tier, + UserActivityStatus activityStatus, + boolean isPrivate, + EquippedItemsData equippedItems + ) { + public static Result from( + User user, + UserActivityStatus activityStatus, + EquippedItemsData equippedItems + ) { + return new Result( + user.id(), + user.username(), + user.name(), + user.profileImage(), + user.major(), + user.tier(), + activityStatus, + user.isPrivate(), + equippedItems + ); + } + } +} diff --git a/src/main/java/com/process/clash/application/profile/port/in/GetUserProfileUsecase.java b/src/main/java/com/process/clash/application/profile/port/in/GetUserProfileUsecase.java new file mode 100644 index 000000000..32faa779b --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/port/in/GetUserProfileUsecase.java @@ -0,0 +1,7 @@ +package com.process.clash.application.profile.port.in; + +import com.process.clash.application.profile.data.GetUserProfileData; + +public interface GetUserProfileUsecase { + GetUserProfileData.Result execute(GetUserProfileData.Command command); +} diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java b/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java new file mode 100644 index 000000000..2013a22af --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java @@ -0,0 +1,35 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.profile.data.GetUserProfileData; +import com.process.clash.application.profile.port.in.GetUserProfileUsecase; +import com.process.clash.application.realtime.data.UserActivityStatus; +import com.process.clash.application.realtime.port.out.UserPresencePort; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.user.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class GetUserProfileService implements GetUserProfileUsecase { + + private final UserRepositoryPort userRepositoryPort; + private final UserPresencePort userPresencePort; + private final EquippedItemsAssembler equippedItemsAssembler; + + @Override + public GetUserProfileData.Result execute(GetUserProfileData.Command command) { + User user = userRepositoryPort.findById(command.targetUserId()) + .orElseThrow(UserNotFoundException::new); + + UserActivityStatus activityStatus = userPresencePort.getStatus(user.id()); + return GetUserProfileData.Result.from( + user, + activityStatus, + equippedItemsAssembler.loadByUserId(user.id()) + ); + } +} From d3c372c14f7d1b842f711699ed3fa5748b075c70 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:08:58 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EB=B3=B4=EC=9C=A0=20=EC=95=84=EC=9D=B4=ED=85=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20(GET=20/api/use?= =?UTF-8?q?rs/{userId}/items)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/data/GetUserItemsData.java | 11 +++ .../profile/port/in/GetUserItemsUsecase.java | 8 ++ .../profile/service/GetUserItemsService.java | 89 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/main/java/com/process/clash/application/profile/data/GetUserItemsData.java create mode 100644 src/main/java/com/process/clash/application/profile/port/in/GetUserItemsUsecase.java create mode 100644 src/main/java/com/process/clash/application/profile/service/GetUserItemsService.java diff --git a/src/main/java/com/process/clash/application/profile/data/GetUserItemsData.java b/src/main/java/com/process/clash/application/profile/data/GetUserItemsData.java new file mode 100644 index 000000000..5414092af --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/data/GetUserItemsData.java @@ -0,0 +1,11 @@ +package com.process.clash.application.profile.data; + +import com.process.clash.domain.common.enums.UserItemCategory; + +public class GetUserItemsData { + + public record Command( + Long targetUserId, + UserItemCategory category + ) {} +} diff --git a/src/main/java/com/process/clash/application/profile/port/in/GetUserItemsUsecase.java b/src/main/java/com/process/clash/application/profile/port/in/GetUserItemsUsecase.java new file mode 100644 index 000000000..b945b23e5 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/port/in/GetUserItemsUsecase.java @@ -0,0 +1,8 @@ +package com.process.clash.application.profile.port.in; + +import com.process.clash.application.profile.data.GetMyItemsData; +import com.process.clash.application.profile.data.GetUserItemsData; + +public interface GetUserItemsUsecase { + GetMyItemsData.Result execute(GetUserItemsData.Command command); +} diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserItemsService.java b/src/main/java/com/process/clash/application/profile/service/GetUserItemsService.java new file mode 100644 index 000000000..0b54c5914 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/service/GetUserItemsService.java @@ -0,0 +1,89 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.profile.data.GetMyItemsData; +import com.process.clash.application.profile.data.GetUserItemsData; +import com.process.clash.application.profile.exception.exception.forbidden.ProfilePrivatedException; +import com.process.clash.application.profile.port.in.GetUserItemsUsecase; +import com.process.clash.application.shop.product.port.out.ProductRepositoryPort; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.application.user.useritem.port.out.UserItemRepositoryPort; +import com.process.clash.domain.common.enums.UserItemCategory; +import com.process.clash.domain.shop.product.entity.Product; +import com.process.clash.domain.shop.product.enums.ProductCategory; +import com.process.clash.domain.shop.season.entity.Season; +import com.process.clash.domain.user.user.entity.User; +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GetUserItemsService implements GetUserItemsUsecase { + + private final UserRepositoryPort userRepositoryPort; + private final UserItemRepositoryPort userItemRepositoryPort; + private final ProductRepositoryPort productRepositoryPort; + + @Override + public GetMyItemsData.Result execute(GetUserItemsData.Command command) { + User user = userRepositoryPort.findById(command.targetUserId()) + .orElseThrow(UserNotFoundException::new); + + if (user.isPrivate()) { + throw new ProfilePrivatedException(); + } + + List productIds = userItemRepositoryPort.findProductIdsByUserId(command.targetUserId()); + + if (productIds.isEmpty()) { + return GetMyItemsData.Result.from(List.of()); + } + + List products = productRepositoryPort.findAllByIdIn(productIds) + .stream() + .filter(product -> matchesCategory(command.category(), product.category())) + .sorted(Comparator.comparing(Product::id)) + .toList(); + + List items = products.stream() + .map(this::toItem) + .toList(); + + return GetMyItemsData.Result.from(items); + } + + private boolean matchesCategory(UserItemCategory category, ProductCategory productCategory) { + if (category == null || category == UserItemCategory.ALL) { + return true; + } + if (productCategory == null) { + return false; + } + return switch (category) { + case INSIGMA, INSIGNIA -> productCategory == ProductCategory.INSIGNIA; + case NAMEPLATE -> productCategory == ProductCategory.NAMEPLATE; + case BANNER -> productCategory == ProductCategory.BANNER; + default -> true; + }; + } + + private GetMyItemsData.Item toItem(Product product) { + GetMyItemsData.Season season = toSeason(product.isSeasonal(), product.season()); + return GetMyItemsData.Item.of( + product.id(), product.title(), product.image(), product.description(), + product.category(), product.price(), product.discount(), product.popularity(), + product.isSeasonal(), season + ); + } + + private GetMyItemsData.Season toSeason(Boolean isSeasonal, Season season) { + if (isSeasonal == null || !isSeasonal || season == null) { + return null; + } + return GetMyItemsData.Season.of(season.id(), season.name(), season.startDate(), season.endDate()); + } +} From 8ef9f2e9986c5b8ca18b9e535328efd0ef167605 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:09:17 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20GitHub=20=ED=99=9C=EB=8F=99=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20(GET=20/api/users/{userId}/github)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/GetUserGitHubActivityData.java | 11 +++ .../port/in/GetUserGitHubActivityUsecase.java | 8 ++ .../service/GetUserGitHubActivityService.java | 77 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/main/java/com/process/clash/application/profile/data/GetUserGitHubActivityData.java create mode 100644 src/main/java/com/process/clash/application/profile/port/in/GetUserGitHubActivityUsecase.java create mode 100644 src/main/java/com/process/clash/application/profile/service/GetUserGitHubActivityService.java diff --git a/src/main/java/com/process/clash/application/profile/data/GetUserGitHubActivityData.java b/src/main/java/com/process/clash/application/profile/data/GetUserGitHubActivityData.java new file mode 100644 index 000000000..1be036e29 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/data/GetUserGitHubActivityData.java @@ -0,0 +1,11 @@ +package com.process.clash.application.profile.data; + +import com.process.clash.domain.common.enums.PeriodCategory; + +public class GetUserGitHubActivityData { + + public record Command( + Long targetUserId, + PeriodCategory period + ) {} +} diff --git a/src/main/java/com/process/clash/application/profile/port/in/GetUserGitHubActivityUsecase.java b/src/main/java/com/process/clash/application/profile/port/in/GetUserGitHubActivityUsecase.java new file mode 100644 index 000000000..97f8eaba8 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/port/in/GetUserGitHubActivityUsecase.java @@ -0,0 +1,8 @@ +package com.process.clash.application.profile.port.in; + +import com.process.clash.application.profile.data.GetMyGitHubActivityData; +import com.process.clash.application.profile.data.GetUserGitHubActivityData; + +public interface GetUserGitHubActivityUsecase { + GetMyGitHubActivityData.Result execute(GetUserGitHubActivityData.Command command); +} diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserGitHubActivityService.java b/src/main/java/com/process/clash/application/profile/service/GetUserGitHubActivityService.java new file mode 100644 index 000000000..e80121511 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/service/GetUserGitHubActivityService.java @@ -0,0 +1,77 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.github.data.GitHubDailyContributionDto; +import com.process.clash.application.github.port.out.GitHubDailyStatsQueryPort; +import com.process.clash.application.profile.data.GetMyGitHubActivityData; +import com.process.clash.application.profile.data.GetUserGitHubActivityData; +import com.process.clash.application.profile.exception.exception.forbidden.ProfilePrivatedException; +import com.process.clash.application.profile.exception.exception.badrequest.InvalidPeriodCategoryException; +import com.process.clash.application.profile.policy.ProfilePolicy; +import com.process.clash.application.profile.port.in.GetUserGitHubActivityUsecase; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.common.enums.PeriodCategory; +import com.process.clash.domain.user.user.entity.User; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GetUserGitHubActivityService implements GetUserGitHubActivityUsecase { + + private final UserRepositoryPort userRepositoryPort; + private final GitHubDailyStatsQueryPort gitHubDailyStatsQueryPort; + private final ProfilePolicy profilePolicy; + + @Override + public GetMyGitHubActivityData.Result execute(GetUserGitHubActivityData.Command command) { + User user = userRepositoryPort.findById(command.targetUserId()) + .orElseThrow(UserNotFoundException::new); + + if (user.isPrivate()) { + throw new ProfilePrivatedException(); + } + + profilePolicy.validateGithubPeriod(command.period()); + + LocalDate endDate = LocalDate.now(ZoneId.of("Asia/Seoul")).plusDays(1); + LocalDate startDate = resolveStartDate(command.period(), endDate); + int limit = (int) ChronoUnit.DAYS.between(startDate, endDate); + + List rows = gitHubDailyStatsQueryPort.findDailyContributionsByUserId( + command.targetUserId(), + startDate, + endDate, + PageRequest.of(0, limit) + ); + + List contributions = rows.stream() + .map(row -> { + int level = GitHubContributionLevelCalculator.fromCount(row.count()); + return GetMyGitHubActivityData.Contribution.of(row.date().toString(), row.count(), level); + }) + .toList(); + + int totalContributions = contributions.stream() + .mapToInt(GetMyGitHubActivityData.Contribution::contributionCount) + .sum(); + + return GetMyGitHubActivityData.Result.from(totalContributions, contributions); + } + + private LocalDate resolveStartDate(PeriodCategory period, LocalDate endDate) { + return switch (period) { + case WEEK -> endDate.minusWeeks(1); + case MONTH -> endDate.minusMonths(1); + case YEAR -> endDate.minusYears(1); + default -> throw new InvalidPeriodCategoryException(); + }; + } +} From a31eea3d0c3758f57f7d28f61186f8584b431ac2 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:09:34 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EB=9D=BC=EC=9D=B4=EB=B2=8C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20(GET=20/api/users/{userId}/rivals)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/data/GetUserRivalsData.java | 8 ++ .../profile/port/in/GetUserRivalsUsecase.java | 8 ++ .../profile/service/GetUserRivalsService.java | 134 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 src/main/java/com/process/clash/application/profile/data/GetUserRivalsData.java create mode 100644 src/main/java/com/process/clash/application/profile/port/in/GetUserRivalsUsecase.java create mode 100644 src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java diff --git a/src/main/java/com/process/clash/application/profile/data/GetUserRivalsData.java b/src/main/java/com/process/clash/application/profile/data/GetUserRivalsData.java new file mode 100644 index 000000000..69b4ecc96 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/data/GetUserRivalsData.java @@ -0,0 +1,8 @@ +package com.process.clash.application.profile.data; + +public class GetUserRivalsData { + + public record Command( + Long targetUserId + ) {} +} diff --git a/src/main/java/com/process/clash/application/profile/port/in/GetUserRivalsUsecase.java b/src/main/java/com/process/clash/application/profile/port/in/GetUserRivalsUsecase.java new file mode 100644 index 000000000..eea498bf1 --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/port/in/GetUserRivalsUsecase.java @@ -0,0 +1,8 @@ +package com.process.clash.application.profile.port.in; + +import com.process.clash.application.compete.rival.rival.data.GetMyRivalActingData; +import com.process.clash.application.profile.data.GetUserRivalsData; + +public interface GetUserRivalsUsecase { + GetMyRivalActingData.Result execute(GetUserRivalsData.Command command); +} diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java b/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java new file mode 100644 index 000000000..a5c2592eb --- /dev/null +++ b/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java @@ -0,0 +1,134 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.compete.rival.rival.data.GetMyRivalActingData; +import com.process.clash.application.compete.rival.rival.port.out.RivalRepositoryPort; +import com.process.clash.application.profile.data.EquippedItemsData; +import com.process.clash.application.profile.data.GetUserRivalsData; +import com.process.clash.application.profile.exception.exception.forbidden.ProfilePrivatedException; +import com.process.clash.application.profile.port.in.GetUserRivalsUsecase; +import com.process.clash.application.realtime.data.UserActivityStatus; +import com.process.clash.application.realtime.port.out.UserPresencePort; +import com.process.clash.application.record.v2.port.out.RecordSessionV2RepositoryPort; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.record.v2.entity.RecordSessionV2; +import com.process.clash.domain.record.v2.enums.RecordSessionTypeV2; +import com.process.clash.domain.rival.rival.entity.Rival; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.infrastructure.config.record.RecordProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GetUserRivalsService implements GetUserRivalsUsecase { + + private final UserRepositoryPort userRepositoryPort; + private final RivalRepositoryPort rivalRepositoryPort; + private final RecordSessionV2RepositoryPort recordSessionRepositoryPort; + private final UserPresencePort userPresencePort; + private final RecordProperties recordProperties; + private final ZoneId recordZoneId; + private final EquippedItemsAssembler equippedItemsAssembler; + + @Override + public GetMyRivalActingData.Result execute(GetUserRivalsData.Command command) { + User targetUser = userRepositoryPort.findById(command.targetUserId()) + .orElseThrow(UserNotFoundException::new); + + if (targetUser.isPrivate()) { + throw new ProfilePrivatedException(); + } + + List rivals = rivalRepositoryPort.findAllByUserId(command.targetUserId()); + + if (rivals.isEmpty()) { + return GetMyRivalActingData.Result.from(List.of()); + } + + int boundaryHour = recordProperties.dayBoundaryHour(); + ZonedDateTime nowZoned = ZonedDateTime.now(recordZoneId); + LocalDate recordDate = nowZoned.toLocalDate(); + if (nowZoned.getHour() < boundaryHour) { + recordDate = recordDate.minusDays(1); + } + LocalDateTime startOfDay = recordDate.atTime(boundaryHour, 0); + LocalDateTime endOfDay = startOfDay.plusDays(1); + + List opponentIds = rivals.stream() + .map(rival -> { + if (rival.firstUserId().equals(command.targetUserId())) { + return rival.secondUserId(); + } + return rival.firstUserId(); + }) + .distinct() + .toList(); + + Map opponentMap = userRepositoryPort.findAllByIds(opponentIds) + .stream() + .collect(Collectors.toMap(User::id, user -> user)); + + Map studyTimeMap = recordSessionRepositoryPort + .getTotalStudyTimeInSecondsByUserIds(opponentIds, startOfDay, endOfDay); + + Map activeSessionByUserId = recordSessionRepositoryPort + .findAllActiveSessionsByUserIds(opponentIds) + .stream() + .collect(Collectors.toMap( + RecordSessionV2::userId, + session -> session, + (first, second) -> first + )); + + Map activityStatusByUserId = userPresencePort.getStatuses(opponentIds); + + Map equippedItemsMap = equippedItemsAssembler.loadByUserIds(opponentIds); + + List myRivals = rivals.stream() + .map(rival -> { + Long opponentId; + if (rival.firstUserId().equals(command.targetUserId())) { + opponentId = rival.secondUserId(); + } else { + opponentId = rival.firstUserId(); + } + + User opponent = opponentMap.get(opponentId); + if (opponent == null) { + throw new UserNotFoundException(); + } + + Long activeTime = studyTimeMap.getOrDefault(opponentId, 0L); + UserActivityStatus activityStatus = activityStatusByUserId.getOrDefault(opponentId, UserActivityStatus.OFFLINE); + RecordSessionV2 activeSession = activeSessionByUserId.get(opponentId); + String usingApp = resolveUsingApp(activeSession); + boolean isStudying = activeSession != null; + EquippedItemsData equippedItems = equippedItemsMap.getOrDefault(opponentId, EquippedItemsData.empty()); + + return GetMyRivalActingData.MyRival.of( + rival.id(), opponent, activeTime, usingApp, isStudying, activityStatus, equippedItems + ); + }) + .toList(); + + return GetMyRivalActingData.Result.from(myRivals); + } + + private String resolveUsingApp(RecordSessionV2 activeSession) { + if (activeSession == null) return null; + if (activeSession.sessionType() != RecordSessionTypeV2.DEVELOP) return null; + if (activeSession.appId() == null) return null; + return activeSession.appId().name(); + } +} From 3fd8988dba21b0f1d978c8430f84e9803b5e791a Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:09:58 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=B9=84=EA=B3=B5=EA=B0=9C=20=EC=84=A4=EC=A0=95=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(PATCH=20/api/users/me/privacy)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/user/dto/UpdateMyPrivacyDto.java | 24 +++++++++++++++ .../user/user/data/UpdateMyPrivacyData.java | 17 +++++++++++ .../user/port/in/UpdateMyPrivacyUseCase.java | 7 +++++ .../user/service/UpdateMyPrivacyService.java | 29 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/main/java/com/process/clash/adapter/web/user/dto/UpdateMyPrivacyDto.java create mode 100644 src/main/java/com/process/clash/application/user/user/data/UpdateMyPrivacyData.java create mode 100644 src/main/java/com/process/clash/application/user/user/port/in/UpdateMyPrivacyUseCase.java create mode 100644 src/main/java/com/process/clash/application/user/user/service/UpdateMyPrivacyService.java diff --git a/src/main/java/com/process/clash/adapter/web/user/dto/UpdateMyPrivacyDto.java b/src/main/java/com/process/clash/adapter/web/user/dto/UpdateMyPrivacyDto.java new file mode 100644 index 000000000..313e21a0c --- /dev/null +++ b/src/main/java/com/process/clash/adapter/web/user/dto/UpdateMyPrivacyDto.java @@ -0,0 +1,24 @@ +package com.process.clash.adapter.web.user.dto; + +import com.process.clash.application.common.actor.Actor; +import com.process.clash.application.user.user.data.UpdateMyPrivacyData; +import jakarta.validation.constraints.NotNull; + +public class UpdateMyPrivacyDto { + + public record Request( + @NotNull Boolean isPrivate + ) { + public UpdateMyPrivacyData.Command toCommand(Actor actor) { + return new UpdateMyPrivacyData.Command(actor, isPrivate); + } + } + + public record Response( + boolean isPrivate + ) { + public static Response from(UpdateMyPrivacyData.Result result) { + return new Response(result.isPrivate()); + } + } +} diff --git a/src/main/java/com/process/clash/application/user/user/data/UpdateMyPrivacyData.java b/src/main/java/com/process/clash/application/user/user/data/UpdateMyPrivacyData.java new file mode 100644 index 000000000..15bb7fe6a --- /dev/null +++ b/src/main/java/com/process/clash/application/user/user/data/UpdateMyPrivacyData.java @@ -0,0 +1,17 @@ +package com.process.clash.application.user.user.data; + +import com.process.clash.application.common.actor.Actor; + +public final class UpdateMyPrivacyData { + + private UpdateMyPrivacyData() {} + + public record Command( + Actor actor, + boolean isPrivate + ) {} + + public record Result( + boolean isPrivate + ) {} +} diff --git a/src/main/java/com/process/clash/application/user/user/port/in/UpdateMyPrivacyUseCase.java b/src/main/java/com/process/clash/application/user/user/port/in/UpdateMyPrivacyUseCase.java new file mode 100644 index 000000000..8d7838977 --- /dev/null +++ b/src/main/java/com/process/clash/application/user/user/port/in/UpdateMyPrivacyUseCase.java @@ -0,0 +1,7 @@ +package com.process.clash.application.user.user.port.in; + +import com.process.clash.application.user.user.data.UpdateMyPrivacyData; + +public interface UpdateMyPrivacyUseCase { + UpdateMyPrivacyData.Result execute(UpdateMyPrivacyData.Command command); +} diff --git a/src/main/java/com/process/clash/application/user/user/service/UpdateMyPrivacyService.java b/src/main/java/com/process/clash/application/user/user/service/UpdateMyPrivacyService.java new file mode 100644 index 000000000..137c65d27 --- /dev/null +++ b/src/main/java/com/process/clash/application/user/user/service/UpdateMyPrivacyService.java @@ -0,0 +1,29 @@ +package com.process.clash.application.user.user.service; + +import com.process.clash.application.user.user.data.UpdateMyPrivacyData; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.in.UpdateMyPrivacyUseCase; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.user.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UpdateMyPrivacyService implements UpdateMyPrivacyUseCase { + + private final UserRepositoryPort userRepositoryPort; + + @Override + @Transactional + public UpdateMyPrivacyData.Result execute(UpdateMyPrivacyData.Command command) { + User user = userRepositoryPort.findById(command.actor().id()) + .orElseThrow(UserNotFoundException::new); + + User updatedUser = user.updatePrivacy(command.isPrivate()); + User savedUser = userRepositoryPort.save(updatedUser); + + return new UpdateMyPrivacyData.Result(savedUser.isPrivate()); + } +} From 7847196ea8941b40e8cadec4d67dabb3a4e133af Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:11:29 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?dto=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/user/dto/GetUserProfileDto.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/com/process/clash/adapter/web/user/dto/GetUserProfileDto.java diff --git a/src/main/java/com/process/clash/adapter/web/user/dto/GetUserProfileDto.java b/src/main/java/com/process/clash/adapter/web/user/dto/GetUserProfileDto.java new file mode 100644 index 000000000..b7bfacf24 --- /dev/null +++ b/src/main/java/com/process/clash/adapter/web/user/dto/GetUserProfileDto.java @@ -0,0 +1,34 @@ +package com.process.clash.adapter.web.user.dto; + +import com.process.clash.application.profile.data.GetUserProfileData; +import com.process.clash.application.realtime.data.UserActivityStatus; +import com.process.clash.domain.common.enums.Major; + +public class GetUserProfileDto { + + public record Response( + Long id, + String username, + String name, + String profileImage, + Major major, + String tier, + UserActivityStatus activityStatus, + boolean isPrivate, + EquippedItemsDto.Response equippedItems + ) { + public static Response from(GetUserProfileData.Result result) { + return new Response( + result.id(), + result.username(), + result.name(), + result.profileImage(), + result.major(), + result.tier(), + result.activityStatus(), + result.isPrivate(), + EquippedItemsDto.Response.from(result.equippedItems()) + ); + } + } +} From 2825c721233f346b55b6a9d9551910dbd2bbe76b Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:11:55 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20UserController=EC=97=90=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EC=9C=A0=EC=A0=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/user/controller/UserController.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/main/java/com/process/clash/adapter/web/user/controller/UserController.java b/src/main/java/com/process/clash/adapter/web/user/controller/UserController.java index b02631d15..b2599f1fa 100644 --- a/src/main/java/com/process/clash/adapter/web/user/controller/UserController.java +++ b/src/main/java/com/process/clash/adapter/web/user/controller/UserController.java @@ -9,17 +9,25 @@ import com.process.clash.adapter.web.user.dto.GetMyActivityCalendarDto; import com.process.clash.adapter.web.user.dto.GetMyItemsDto; import com.process.clash.adapter.web.user.dto.GetMyProfileDto; +import com.process.clash.adapter.web.user.dto.GetUserProfileDto; +import com.process.clash.adapter.web.user.dto.UpdateMyPrivacyDto; import com.process.clash.adapter.web.user.dto.IssueProfileImageUploadUrlDto; import com.process.clash.adapter.web.user.dto.LinkGitHubOAuthDto; import com.process.clash.adapter.web.user.dto.UpdateMyProfileImageDto; import com.process.clash.adapter.web.security.AuthenticatedActor; +import com.process.clash.adapter.web.compete.rival.rival.dto.GetMyRivalActingDto; import com.process.clash.application.common.actor.Actor; +import com.process.clash.application.compete.rival.rival.data.GetMyRivalActingData; import com.process.clash.application.profile.data.EquipMyItemData; import com.process.clash.application.profile.data.GetMyGitHubActivityDetailData; import com.process.clash.application.profile.data.GetMyGitHubActivityData; import com.process.clash.application.profile.data.GetMyActivityCalendarData; import com.process.clash.application.profile.data.GetMyItemsData; import com.process.clash.application.profile.data.GetMyProfileData; +import com.process.clash.application.profile.data.GetUserProfileData; +import com.process.clash.application.profile.data.GetUserItemsData; +import com.process.clash.application.profile.data.GetUserGitHubActivityData; +import com.process.clash.application.profile.data.GetUserRivalsData; import com.process.clash.application.profile.data.UnequipMyItemData; import com.process.clash.application.profile.port.in.EquipMyItemUsecase; import com.process.clash.application.profile.port.in.GetMyGitHubActivityDetailUsecase; @@ -27,10 +35,16 @@ import com.process.clash.application.profile.port.in.GetMyActivityCalendarUsecase; import com.process.clash.application.profile.port.in.GetMyItemsUsecase; import com.process.clash.application.profile.port.in.GetMyProfileUsecase; +import com.process.clash.application.profile.port.in.GetUserProfileUsecase; +import com.process.clash.application.profile.port.in.GetUserItemsUsecase; +import com.process.clash.application.profile.port.in.GetUserGitHubActivityUsecase; +import com.process.clash.application.profile.port.in.GetUserRivalsUsecase; import com.process.clash.application.profile.port.in.UnequipMyItemUsecase; import com.process.clash.application.user.user.data.IssueProfileImageUploadUrlData; +import com.process.clash.application.user.user.data.UpdateMyPrivacyData; import com.process.clash.application.user.user.data.UpdateMyProfileImageData; import com.process.clash.application.user.user.port.in.IssueProfileImageUploadUrlUseCase; +import com.process.clash.application.user.user.port.in.UpdateMyPrivacyUseCase; import com.process.clash.application.user.user.port.in.UpdateMyProfileImageUseCase; import com.process.clash.application.user.user.port.in.WithdrawUseCase; import com.process.clash.application.user.usergithub.data.GetMyGitHubLinkStatusData; @@ -71,6 +85,11 @@ public class UserController implements UserControllerDocument { private final IssueProfileImageUploadUrlUseCase issueProfileImageUploadUrlUseCase; private final UpdateMyProfileImageUseCase updateMyProfileImageUseCase; private final WithdrawUseCase withdrawUseCase; + private final GetUserProfileUsecase getUserProfileUsecase; + private final GetUserItemsUsecase getUserItemsUsecase; + private final GetUserGitHubActivityUsecase getUserGitHubActivityUsecase; + private final GetUserRivalsUsecase getUserRivalsUsecase; + private final UpdateMyPrivacyUseCase updateMyPrivacyUseCase; @DeleteMapping("/me") public ApiResponse withdraw( @@ -198,4 +217,61 @@ public ApiResponse updateMyProfileImage( UpdateMyProfileImageDto.Response response = UpdateMyProfileImageDto.Response.from(result); return ApiResponse.success(response, "프로필 이미지 수정을 성공했습니다."); } + + @GetMapping("/{userId}") + public ApiResponse getUserProfile( + @AuthenticatedActor Actor actor, + @PathVariable Long userId + ) { + GetUserProfileData.Command command = new GetUserProfileData.Command(userId); + GetUserProfileData.Result result = getUserProfileUsecase.execute(command); + GetUserProfileDto.Response response = GetUserProfileDto.Response.from(result); + return ApiResponse.success(response, "유저 프로필을 성공적으로 조회했습니다."); + } + + @GetMapping("/{userId}/items") + public ApiResponse getUserItems( + @AuthenticatedActor Actor actor, + @PathVariable Long userId, + @RequestParam(name = "category", defaultValue = "ALL") UserItemCategory category + ) { + GetUserItemsData.Command command = new GetUserItemsData.Command(userId, category); + GetMyItemsData.Result result = getUserItemsUsecase.execute(command); + GetMyItemsDto.Response response = GetMyItemsDto.Response.from(result); + return ApiResponse.success(response, "유저 아이템 목록을 성공적으로 조회했습니다."); + } + + @GetMapping("/{userId}/github") + public ApiResponse getUserGitHubActivity( + @AuthenticatedActor Actor actor, + @PathVariable Long userId, + @RequestParam PeriodCategory period + ) { + GetUserGitHubActivityData.Command command = new GetUserGitHubActivityData.Command(userId, period); + GetMyGitHubActivityData.Result result = getUserGitHubActivityUsecase.execute(command); + GetMyGitHubActivityDto.Response response = GetMyGitHubActivityDto.Response.from(result); + return ApiResponse.success(response, "유저 깃허브 활동을 성공적으로 조회했습니다."); + } + + @GetMapping("/{userId}/rivals") + public ApiResponse getUserRivals( + @AuthenticatedActor Actor actor, + @PathVariable Long userId + ) { + GetUserRivalsData.Command command = new GetUserRivalsData.Command(userId); + GetMyRivalActingData.Result result = getUserRivalsUsecase.execute(command); + GetMyRivalActingDto.Response response = GetMyRivalActingDto.Response.from(result); + return ApiResponse.success(response, "유저 라이벌 목록을 성공적으로 조회했습니다."); + } + + @PatchMapping("/me/privacy") + public ApiResponse updateMyPrivacy( + @AuthenticatedActor Actor actor, + @Valid @RequestBody UpdateMyPrivacyDto.Request request + ) { + UpdateMyPrivacyData.Command command = request.toCommand(actor); + UpdateMyPrivacyData.Result result = updateMyPrivacyUseCase.execute(command); + UpdateMyPrivacyDto.Response response = UpdateMyPrivacyDto.Response.from(result); + return ApiResponse.success(response, "프로필 공개 설정을 성공적으로 변경했습니다."); + } } From 5bc43fa5b2531d0318030a0f6486f053de6096ed Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:12:27 +0900 Subject: [PATCH 11/20] =?UTF-8?q?docs:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EB=AC=B8=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/UserControllerDocument.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/main/java/com/process/clash/adapter/web/user/docs/controller/UserControllerDocument.java b/src/main/java/com/process/clash/adapter/web/user/docs/controller/UserControllerDocument.java index 76c2f52a1..ce6881e0a 100644 --- a/src/main/java/com/process/clash/adapter/web/user/docs/controller/UserControllerDocument.java +++ b/src/main/java/com/process/clash/adapter/web/user/docs/controller/UserControllerDocument.java @@ -10,6 +10,7 @@ import com.process.clash.adapter.web.user.docs.response.IssueProfileImageUploadUrlResponseDocument; import com.process.clash.adapter.web.user.docs.response.LinkGitHubOAuthResponseDocument; import com.process.clash.adapter.web.user.docs.response.UpdateMyProfileImageResponseDocument; +import com.process.clash.adapter.web.compete.rival.rival.dto.GetMyRivalActingDto; import com.process.clash.adapter.web.user.dto.EquippedItemsDto; import com.process.clash.adapter.web.user.dto.GetMyGitHubLinkStatusDto; import com.process.clash.adapter.web.user.dto.GetMyGitHubActivityDto; @@ -17,6 +18,8 @@ import com.process.clash.adapter.web.user.dto.GetMyItemsDto; import com.process.clash.adapter.web.user.dto.GetMyActivityCalendarDto; import com.process.clash.adapter.web.user.dto.GetMyProfileDto; +import com.process.clash.adapter.web.user.dto.GetUserProfileDto; +import com.process.clash.adapter.web.user.dto.UpdateMyPrivacyDto; import com.process.clash.adapter.web.user.dto.IssueProfileImageUploadUrlDto; import com.process.clash.adapter.web.user.dto.LinkGitHubOAuthDto; import com.process.clash.adapter.web.user.dto.UpdateMyProfileImageDto; @@ -252,4 +255,75 @@ com.process.clash.adapter.web.common.ApiResponse getUserProfile( + @Parameter(hidden = true) Actor actor, + @Parameter(description = "유저 ID", required = true) Long userId + ); + + @Operation(summary = "다른 유저 보유 아이템 조회", description = "특정 유저의 보유 아이템 목록을 조회합니다. 비공개 프로필이면 403.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = GetMyItemsDto.Response.class))), + @ApiResponse(responseCode = "403", description = "비공개 프로필") + }) + com.process.clash.adapter.web.common.ApiResponse getUserItems( + @Parameter(hidden = true) Actor actor, + @Parameter(description = "유저 ID", required = true) Long userId, + @Parameter(description = "카테고리 (ALL, INSIGNIA, NAMEPLATE, BANNER)", required = false) UserItemCategory category + ); + + @Operation(summary = "다른 유저 GitHub 활동 조회", description = "특정 유저의 GitHub 활동을 조회합니다. 비공개 프로필이면 403.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = GetMyGitHubActivityDto.Response.class))), + @ApiResponse(responseCode = "403", description = "비공개 프로필") + }) + com.process.clash.adapter.web.common.ApiResponse getUserGitHubActivity( + @Parameter(hidden = true) Actor actor, + @Parameter(description = "유저 ID", required = true) Long userId, + @Parameter(description = "기간 (YEAR, MONTH, WEEK)", required = true) PeriodCategory period + ); + + @Operation(summary = "다른 유저 라이벌 조회", description = "특정 유저의 라이벌 목록을 조회합니다. 비공개 프로필이면 403.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = GetMyRivalActingDto.Response.class))), + @ApiResponse(responseCode = "403", description = "비공개 프로필") + }) + com.process.clash.adapter.web.common.ApiResponse getUserRivals( + @Parameter(hidden = true) Actor actor, + @Parameter(description = "유저 ID", required = true) Long userId + ); + + @Operation(summary = "프로필 공개 설정 변경", description = "내 프로필의 공개/비공개 여부를 설정합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "변경 성공", + content = @Content(schema = @Schema(implementation = UpdateMyPrivacyDto.Response.class), + examples = @ExampleObject(value = """ + { + "success": true, + "message": "프로필 공개 설정을 성공적으로 변경했습니다.", + "data": { + "isPrivate": true + } + } + """))) + }) + com.process.clash.adapter.web.common.ApiResponse updateMyPrivacy( + @Parameter(hidden = true) Actor actor, + @RequestBody(description = "비공개 설정", required = true, + content = @Content(schema = @Schema(implementation = UpdateMyPrivacyDto.Request.class), + examples = @ExampleObject(value = """ + { + "isPrivate": true + } + """))) + UpdateMyPrivacyDto.Request request + ); } From 9aae91d7268501d9d6c1a4cc27ce1382b95e1aa9 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:14:06 +0900 Subject: [PATCH 12/20] =?UTF-8?q?test:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetUserGitHubActivityServiceTest.java | 105 ++++++++++++++++++ .../service/GetUserItemsServiceTest.java | 99 +++++++++++++++++ .../service/GetUserProfileServiceTest.java | 104 +++++++++++++++++ .../service/UpdateMyPrivacyServiceTest.java | 93 ++++++++++++++++ 4 files changed, 401 insertions(+) create mode 100644 src/test/java/com/process/clash/application/profile/service/GetUserGitHubActivityServiceTest.java create mode 100644 src/test/java/com/process/clash/application/profile/service/GetUserItemsServiceTest.java create mode 100644 src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java create mode 100644 src/test/java/com/process/clash/application/user/user/service/UpdateMyPrivacyServiceTest.java diff --git a/src/test/java/com/process/clash/application/profile/service/GetUserGitHubActivityServiceTest.java b/src/test/java/com/process/clash/application/profile/service/GetUserGitHubActivityServiceTest.java new file mode 100644 index 000000000..4db850f4a --- /dev/null +++ b/src/test/java/com/process/clash/application/profile/service/GetUserGitHubActivityServiceTest.java @@ -0,0 +1,105 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.github.data.GitHubDailyContributionDto; +import com.process.clash.application.github.port.out.GitHubDailyStatsQueryPort; +import com.process.clash.application.profile.data.GetMyGitHubActivityData; +import com.process.clash.application.profile.data.GetUserGitHubActivityData; +import com.process.clash.application.profile.exception.exception.forbidden.ProfilePrivatedException; +import com.process.clash.application.profile.policy.ProfilePolicy; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.common.enums.PeriodCategory; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.user.enums.Role; +import com.process.clash.domain.user.user.enums.UserStatus; +import com.process.clash.domain.user.userrankhistory.enums.ExpTier; +import com.process.clash.domain.user.userrankhistory.enums.RankTier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GetUserGitHubActivityServiceTest { + + @Mock + private UserRepositoryPort userRepositoryPort; + + @Mock + private GitHubDailyStatsQueryPort gitHubDailyStatsQueryPort; + + @Mock + private ProfilePolicy profilePolicy; + + private GetUserGitHubActivityService getUserGitHubActivityService; + + @BeforeEach + void setUp() { + getUserGitHubActivityService = new GetUserGitHubActivityService( + userRepositoryPort, gitHubDailyStatsQueryPort, profilePolicy + ); + } + + @Test + @DisplayName("비공개 유저 GitHub 활동 조회 시 ProfilePrivatedException을 던진다") + void execute_throwsProfilePrivatedException_whenUserIsPrivate() { + Long targetUserId = 2L; + User privateUser = createUser(targetUserId, true); + GetUserGitHubActivityData.Command command = new GetUserGitHubActivityData.Command(targetUserId, PeriodCategory.WEEK); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(privateUser)); + + assertThatThrownBy(() -> getUserGitHubActivityService.execute(command)) + .isInstanceOf(ProfilePrivatedException.class); + } + + @Test + @DisplayName("공개 유저 GitHub 활동 조회 시 빈 기여도를 반환한다") + void execute_returnsEmptyContributions_whenNoActivity() { + Long targetUserId = 2L; + User publicUser = createUser(targetUserId, false); + GetUserGitHubActivityData.Command command = new GetUserGitHubActivityData.Command(targetUserId, PeriodCategory.WEEK); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(publicUser)); + when(gitHubDailyStatsQueryPort.findDailyContributionsByUserId(any(), any(), any(), any())) + .thenReturn(List.of()); + + GetMyGitHubActivityData.Result result = getUserGitHubActivityService.execute(command); + + assertThat(result.totalContributions()).isZero(); + assertThat(result.contributions()).isEmpty(); + } + + @Test + @DisplayName("존재하지 않는 유저 조회 시 UserNotFoundException을 던진다") + void execute_throwsUserNotFoundException_whenUserNotFound() { + Long targetUserId = 999L; + GetUserGitHubActivityData.Command command = new GetUserGitHubActivityData.Command(targetUserId, PeriodCategory.WEEK); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> getUserGitHubActivityService.execute(command)) + .isInstanceOf(UserNotFoundException.class); + } + + private User createUser(Long id, boolean isPrivate) { + return new User( + id, Instant.now(), Instant.now(), + "username", "user@example.com", "name", "encoded-password", + Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, + null, isPrivate, RankTier.NONE, ExpTier.UNRANKED + ); + } +} diff --git a/src/test/java/com/process/clash/application/profile/service/GetUserItemsServiceTest.java b/src/test/java/com/process/clash/application/profile/service/GetUserItemsServiceTest.java new file mode 100644 index 000000000..5754ff69a --- /dev/null +++ b/src/test/java/com/process/clash/application/profile/service/GetUserItemsServiceTest.java @@ -0,0 +1,99 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.profile.data.GetMyItemsData; +import com.process.clash.application.profile.data.GetUserItemsData; +import com.process.clash.application.profile.exception.exception.forbidden.ProfilePrivatedException; +import com.process.clash.application.shop.product.port.out.ProductRepositoryPort; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.application.user.useritem.port.out.UserItemRepositoryPort; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.common.enums.UserItemCategory; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.user.enums.Role; +import com.process.clash.domain.user.user.enums.UserStatus; +import com.process.clash.domain.user.userrankhistory.enums.ExpTier; +import com.process.clash.domain.user.userrankhistory.enums.RankTier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GetUserItemsServiceTest { + + @Mock + private UserRepositoryPort userRepositoryPort; + + @Mock + private UserItemRepositoryPort userItemRepositoryPort; + + @Mock + private ProductRepositoryPort productRepositoryPort; + + private GetUserItemsService getUserItemsService; + + @BeforeEach + void setUp() { + getUserItemsService = new GetUserItemsService(userRepositoryPort, userItemRepositoryPort, productRepositoryPort); + } + + @Test + @DisplayName("비공개 유저 아이템 조회 시 ProfilePrivatedException을 던진다") + void execute_throwsProfilePrivatedException_whenUserIsPrivate() { + Long targetUserId = 2L; + User privateUser = createUser(targetUserId, true); + GetUserItemsData.Command command = new GetUserItemsData.Command(targetUserId, UserItemCategory.ALL); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(privateUser)); + + assertThatThrownBy(() -> getUserItemsService.execute(command)) + .isInstanceOf(ProfilePrivatedException.class); + } + + @Test + @DisplayName("공개 유저 아이템 조회 시 빈 목록을 반환한다") + void execute_returnsEmptyList_whenUserHasNoItems() { + Long targetUserId = 2L; + User publicUser = createUser(targetUserId, false); + GetUserItemsData.Command command = new GetUserItemsData.Command(targetUserId, UserItemCategory.ALL); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(publicUser)); + when(userItemRepositoryPort.findProductIdsByUserId(targetUserId)).thenReturn(List.of()); + + GetMyItemsData.Result result = getUserItemsService.execute(command); + + assertThat(result.items()).isEmpty(); + } + + @Test + @DisplayName("존재하지 않는 유저 조회 시 UserNotFoundException을 던진다") + void execute_throwsUserNotFoundException_whenUserNotFound() { + Long targetUserId = 999L; + GetUserItemsData.Command command = new GetUserItemsData.Command(targetUserId, UserItemCategory.ALL); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> getUserItemsService.execute(command)) + .isInstanceOf(UserNotFoundException.class); + } + + private User createUser(Long id, boolean isPrivate) { + return new User( + id, Instant.now(), Instant.now(), + "username", "user@example.com", "name", "encoded-password", + Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, + null, isPrivate, RankTier.NONE, ExpTier.UNRANKED + ); + } +} diff --git a/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java b/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java new file mode 100644 index 000000000..2292dd5da --- /dev/null +++ b/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java @@ -0,0 +1,104 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.profile.data.EquippedItemsData; +import com.process.clash.application.profile.data.GetUserProfileData; +import com.process.clash.application.realtime.data.UserActivityStatus; +import com.process.clash.application.realtime.port.out.UserPresencePort; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.user.enums.Role; +import com.process.clash.domain.user.user.enums.UserStatus; +import com.process.clash.domain.user.userrankhistory.enums.ExpTier; +import com.process.clash.domain.user.userrankhistory.enums.RankTier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GetUserProfileServiceTest { + + @Mock + private UserRepositoryPort userRepositoryPort; + + @Mock + private UserPresencePort userPresencePort; + + @Mock + private EquippedItemsAssembler equippedItemsAssembler; + + private GetUserProfileService getUserProfileService; + + @BeforeEach + void setUp() { + getUserProfileService = new GetUserProfileService( + userRepositoryPort, userPresencePort, equippedItemsAssembler + ); + } + + @Test + @DisplayName("다른 유저 프로필 조회 시 activityStatus와 equippedItems를 반환한다") + void execute_returnsProfileWithActivityStatusAndEquippedItems() { + Long targetUserId = 2L; + User user = createUser(targetUserId, false); + GetUserProfileData.Command command = new GetUserProfileData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(user)); + when(userPresencePort.getStatus(user.id())).thenReturn(UserActivityStatus.ONLINE); + when(equippedItemsAssembler.loadByUserId(user.id())).thenReturn(EquippedItemsData.empty()); + + GetUserProfileData.Result result = getUserProfileService.execute(command); + + assertThat(result.id()).isEqualTo(targetUserId); + assertThat(result.activityStatus()).isEqualTo(UserActivityStatus.ONLINE); + assertThat(result.isPrivate()).isFalse(); + } + + @Test + @DisplayName("존재하지 않는 유저 조회 시 UserNotFoundException을 던진다") + void execute_throwsUserNotFoundException_whenUserNotFound() { + Long targetUserId = 999L; + GetUserProfileData.Command command = new GetUserProfileData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> getUserProfileService.execute(command)) + .isInstanceOf(UserNotFoundException.class); + } + + @Test + @DisplayName("비공개 유저도 기본 프로필은 반환한다") + void execute_returnsProfile_evenWhenPrivate() { + Long targetUserId = 3L; + User user = createUser(targetUserId, true); + GetUserProfileData.Command command = new GetUserProfileData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(user)); + when(userPresencePort.getStatus(user.id())).thenReturn(UserActivityStatus.OFFLINE); + when(equippedItemsAssembler.loadByUserId(user.id())).thenReturn(EquippedItemsData.empty()); + + GetUserProfileData.Result result = getUserProfileService.execute(command); + + assertThat(result.isPrivate()).isTrue(); + } + + private User createUser(Long id, boolean isPrivate) { + return new User( + id, Instant.now(), Instant.now(), + "username", "user@example.com", "name", "encoded-password", + Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, + null, isPrivate, RankTier.NONE, ExpTier.UNRANKED + ); + } +} diff --git a/src/test/java/com/process/clash/application/user/user/service/UpdateMyPrivacyServiceTest.java b/src/test/java/com/process/clash/application/user/user/service/UpdateMyPrivacyServiceTest.java new file mode 100644 index 000000000..a7ddc8b39 --- /dev/null +++ b/src/test/java/com/process/clash/application/user/user/service/UpdateMyPrivacyServiceTest.java @@ -0,0 +1,93 @@ +package com.process.clash.application.user.user.service; + +import com.process.clash.application.common.actor.Actor; +import com.process.clash.application.user.user.data.UpdateMyPrivacyData; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.user.enums.Role; +import com.process.clash.domain.user.user.enums.UserStatus; +import com.process.clash.domain.user.userrankhistory.enums.ExpTier; +import com.process.clash.domain.user.userrankhistory.enums.RankTier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UpdateMyPrivacyServiceTest { + + @Mock + private UserRepositoryPort userRepositoryPort; + + private UpdateMyPrivacyService updateMyPrivacyService; + + @BeforeEach + void setUp() { + updateMyPrivacyService = new UpdateMyPrivacyService(userRepositoryPort); + } + + @Test + @DisplayName("isPrivate=true로 설정하면 비공개로 변경된다") + void execute_setsPrivateToTrue() { + Actor actor = new Actor(1L); + User user = createUser(1L, false); + User updatedUser = createUser(1L, true); + UpdateMyPrivacyData.Command command = new UpdateMyPrivacyData.Command(actor, true); + + when(userRepositoryPort.findById(actor.id())).thenReturn(Optional.of(user)); + when(userRepositoryPort.save(any())).thenReturn(updatedUser); + + UpdateMyPrivacyData.Result result = updateMyPrivacyService.execute(command); + + assertThat(result.isPrivate()).isTrue(); + } + + @Test + @DisplayName("isPrivate=false로 설정하면 공개로 변경된다") + void execute_setsPrivateToFalse() { + Actor actor = new Actor(1L); + User user = createUser(1L, true); + User updatedUser = createUser(1L, false); + UpdateMyPrivacyData.Command command = new UpdateMyPrivacyData.Command(actor, false); + + when(userRepositoryPort.findById(actor.id())).thenReturn(Optional.of(user)); + when(userRepositoryPort.save(any())).thenReturn(updatedUser); + + UpdateMyPrivacyData.Result result = updateMyPrivacyService.execute(command); + + assertThat(result.isPrivate()).isFalse(); + } + + @Test + @DisplayName("존재하지 않는 유저면 UserNotFoundException을 던진다") + void execute_throwsUserNotFoundException_whenUserNotFound() { + Actor actor = new Actor(999L); + UpdateMyPrivacyData.Command command = new UpdateMyPrivacyData.Command(actor, true); + + when(userRepositoryPort.findById(actor.id())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> updateMyPrivacyService.execute(command)) + .isInstanceOf(UserNotFoundException.class); + } + + private User createUser(Long id, boolean isPrivate) { + return new User( + id, Instant.now(), Instant.now(), + "username", "user@example.com", "name", "encoded-password", + Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, + null, isPrivate, RankTier.NONE, ExpTier.UNRANKED + ); + } +} From 4d0c55ae99584f74f0c3fa5f8e1ec130dba0401b Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 15:14:29 +0900 Subject: [PATCH 13/20] =?UTF-8?q?test:=20User=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20isPrivate=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B8=B0=EC=A1=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserSectionProgressJpaRepositoryTest.java | 4 +- .../ReSignUpAfterWithdrawIntegrationTest.java | 6 +- .../softdelete/SoftDeleteIntegrationTest.java | 2 +- .../UserNoticeJpaRepositoryTest.java | 2 +- .../service/FindAllBattleInfoServiceTest.java | 2 +- .../FindApplyBattleListServiceTest.java | 2 +- .../service/CompareWithRivalsServiceTest.java | 2 +- .../service/FindAllRivalsServiceTest.java | 2 +- .../service/GetMyRivalActingServiceTest.java | 524 +++++++++--------- .../group/service/CreateGroupServiceTest.java | 2 +- .../service/GetGroupActivityServiceTest.java | 1 + .../service/GetMyProfileServiceTest.java | 1 + .../service/RankingTierUpdateServiceTest.java | 2 +- .../service/CreateSubjectV2ServiceTest.java | 1 + .../GetActivityStatisticsV2ServiceTest.java | 1 + .../service/GetTodayRecordV2ServiceTest.java | 1 + .../v2/service/StartRecordV2ServiceTest.java | 1 + .../SubmitQuestionV2AnswerServiceTest.java | 1 + .../service/SeasonFinishServiceTest.java | 2 +- .../service/GithubExpGrantServiceTest.java | 2 +- .../service/StudyTimeExpGrantServiceTest.java | 2 +- .../service/ResetPasswordServiceTest.java | 2 +- .../UpdateMyProfileImageServiceTest.java | 1 + .../user/service/WithdrawnUserQueryTest.java | 1 + .../service/LinkGitHubOAuthServiceTest.java | 1 + .../domain/user/user/entity/UserTest.java | 2 + 26 files changed, 292 insertions(+), 278 deletions(-) diff --git a/src/test/java/com/process/clash/adapter/persistence/roadmap/sectionprogress/UserSectionProgressJpaRepositoryTest.java b/src/test/java/com/process/clash/adapter/persistence/roadmap/sectionprogress/UserSectionProgressJpaRepositoryTest.java index e7fb2d336..2a6363ec5 100644 --- a/src/test/java/com/process/clash/adapter/persistence/roadmap/sectionprogress/UserSectionProgressJpaRepositoryTest.java +++ b/src/test/java/com/process/clash/adapter/persistence/roadmap/sectionprogress/UserSectionProgressJpaRepositoryTest.java @@ -94,12 +94,12 @@ void beforeEach() { // 4. User 저장 User noSectionProgressUser = new User(null, now, now, "userA", "userA@gmail.com", "유저A", "password", - Role.USER, "", 0, 0, Major.NONE, UserStatus.PENDING, null, RankTier.NONE, ExpTier.UNRANKED); + Role.USER, "", 0, 0, Major.NONE, UserStatus.PENDING, null, false, RankTier.NONE, ExpTier.UNRANKED); noSectionProgressUserJpaEntity = userJpaMapper.toJpaEntity(noSectionProgressUser); em.persist(noSectionProgressUserJpaEntity); User sectionProgressUser = new User(null, now, now, "userB", "userB@gmail.com", "유저B", "password", - Role.USER, "", 0, 0, Major.NONE, UserStatus.PENDING, null, RankTier.NONE, ExpTier.UNRANKED); + Role.USER, "", 0, 0, Major.NONE, UserStatus.PENDING, null, false, RankTier.NONE, ExpTier.UNRANKED); sectionProgressUserJpaEntity = userJpaMapper.toJpaEntity(sectionProgressUser); em.persist(sectionProgressUserJpaEntity); diff --git a/src/test/java/com/process/clash/adapter/persistence/softdelete/ReSignUpAfterWithdrawIntegrationTest.java b/src/test/java/com/process/clash/adapter/persistence/softdelete/ReSignUpAfterWithdrawIntegrationTest.java index 3b151be72..165f40338 100644 --- a/src/test/java/com/process/clash/adapter/persistence/softdelete/ReSignUpAfterWithdrawIntegrationTest.java +++ b/src/test/java/com/process/clash/adapter/persistence/softdelete/ReSignUpAfterWithdrawIntegrationTest.java @@ -72,7 +72,7 @@ void afterWithdraw_canReSignUpWithSameUsernameAndEmail() { UserJpaEntity newEntity = userJpaMapper.toJpaEntity(new User( null, Instant.now(), Instant.now(), "testuser", "test@example.com", "재가입유저", - "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, ExpTier.UNRANKED + "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.UNRANKED )); UserJpaEntity saved = userJpaRepository.save(newEntity); em.flush(); @@ -165,7 +165,7 @@ void afterReSignUp_findByUsername_returnsNewUser() { UserJpaEntity newEntity = userJpaMapper.toJpaEntity(new User( null, Instant.now(), Instant.now(), "testuser", "test@example.com", "재가입유저", - "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, ExpTier.UNRANKED + "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.UNRANKED )); UserJpaEntity saved = userJpaRepository.save(newEntity); em.flush(); @@ -187,7 +187,7 @@ private UserJpaEntity persistUser(String username, String email) { User user = new User( null, Instant.now(), Instant.now(), username, email, "테스트유저", - "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, ExpTier.UNRANKED + "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.UNRANKED ); UserJpaEntity entity = userJpaMapper.toJpaEntity(user); em.persist(entity); diff --git a/src/test/java/com/process/clash/adapter/persistence/softdelete/SoftDeleteIntegrationTest.java b/src/test/java/com/process/clash/adapter/persistence/softdelete/SoftDeleteIntegrationTest.java index 2a112ca0b..5434d3c4c 100644 --- a/src/test/java/com/process/clash/adapter/persistence/softdelete/SoftDeleteIntegrationTest.java +++ b/src/test/java/com/process/clash/adapter/persistence/softdelete/SoftDeleteIntegrationTest.java @@ -98,7 +98,7 @@ private UserJpaEntity persistUser(String label) { User user = new User( null, Instant.now(), Instant.now(), "user_" + suffix, suffix + "@test.com", "유저" + label, - "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, ExpTier.UNRANKED + "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.UNRANKED ); UserJpaEntity entity = userJpaMapper.toJpaEntity(user); em.persist(entity); diff --git a/src/test/java/com/process/clash/adapter/persistence/user/usernotice/UserNoticeJpaRepositoryTest.java b/src/test/java/com/process/clash/adapter/persistence/user/usernotice/UserNoticeJpaRepositoryTest.java index 16512f8c2..f01b11a68 100644 --- a/src/test/java/com/process/clash/adapter/persistence/user/usernotice/UserNoticeJpaRepositoryTest.java +++ b/src/test/java/com/process/clash/adapter/persistence/user/usernotice/UserNoticeJpaRepositoryTest.java @@ -111,7 +111,7 @@ private UserJpaEntity persistUser(String name, String email) { User user = new User( null, null, null, name + counter, email, name, - "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, ExpTier.UNRANKED + "pw", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.UNRANKED ); UserJpaEntity entity = userJpaMapper.toJpaEntity(user); em.persist(entity); diff --git a/src/test/java/com/process/clash/application/compete/rival/battle/service/FindAllBattleInfoServiceTest.java b/src/test/java/com/process/clash/application/compete/rival/battle/service/FindAllBattleInfoServiceTest.java index 98e666b92..1eda717c8 100644 --- a/src/test/java/com/process/clash/application/compete/rival/battle/service/FindAllBattleInfoServiceTest.java +++ b/src/test/java/com/process/clash/application/compete/rival/battle/service/FindAllBattleInfoServiceTest.java @@ -227,7 +227,7 @@ private void stubCommonDependenciesWithEnemy(RankTier rankTier, ExpTier expTier, User enemyUser = new User(ENEMY_USER_ID, Instant.now(), Instant.now(), "enemy", "enemy@example.com", "Enemy", "", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, - rankTier, expTier); + false, rankTier, expTier); when(battleRepositoryPort.findByUserIdWithOutRejected(CURRENT_USER_ID)).thenReturn(battles); when(rivalRepositoryPort.findByIdIn(Set.of(RIVAL_ID))).thenReturn(List.of(rival)); diff --git a/src/test/java/com/process/clash/application/compete/rival/battle/service/FindApplyBattleListServiceTest.java b/src/test/java/com/process/clash/application/compete/rival/battle/service/FindApplyBattleListServiceTest.java index 3c9464f37..022d32539 100644 --- a/src/test/java/com/process/clash/application/compete/rival/battle/service/FindApplyBattleListServiceTest.java +++ b/src/test/java/com/process/clash/application/compete/rival/battle/service/FindApplyBattleListServiceTest.java @@ -196,7 +196,7 @@ private User createUser(Long id, String name, String username, String profileIma username, username + "@test.com", name, "password", Role.USER, profileImage, 0, 0, Major.NONE, UserStatus.ACTIVE, - null, RankTier.NONE, ExpTier.UNRANKED + null, false, RankTier.NONE, ExpTier.UNRANKED ); } } diff --git a/src/test/java/com/process/clash/application/compete/rival/rival/service/CompareWithRivalsServiceTest.java b/src/test/java/com/process/clash/application/compete/rival/rival/service/CompareWithRivalsServiceTest.java index a59ed5799..f3e294441 100644 --- a/src/test/java/com/process/clash/application/compete/rival/rival/service/CompareWithRivalsServiceTest.java +++ b/src/test/java/com/process/clash/application/compete/rival/rival/service/CompareWithRivalsServiceTest.java @@ -106,7 +106,7 @@ private User createUser(Long id) { id, Instant.now(), Instant.now(), "user" + id, "user" + id + "@test.com", "User" + id, "password", Role.USER, "", 0, 0, - Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, ExpTier.UNRANKED + Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.UNRANKED ); } diff --git a/src/test/java/com/process/clash/application/compete/rival/rival/service/FindAllRivalsServiceTest.java b/src/test/java/com/process/clash/application/compete/rival/rival/service/FindAllRivalsServiceTest.java index c9d0fc201..800699abb 100644 --- a/src/test/java/com/process/clash/application/compete/rival/rival/service/FindAllRivalsServiceTest.java +++ b/src/test/java/com/process/clash/application/compete/rival/rival/service/FindAllRivalsServiceTest.java @@ -156,7 +156,7 @@ private User createUser(Long id, String name, RankTier rankTier, ExpTier expTier name, name + "@test.com", name, "password", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, - null, rankTier, expTier + null, false, rankTier, expTier ); } } \ No newline at end of file diff --git a/src/test/java/com/process/clash/application/compete/rival/rival/service/GetMyRivalActingServiceTest.java b/src/test/java/com/process/clash/application/compete/rival/rival/service/GetMyRivalActingServiceTest.java index 28d2b280f..703f929c2 100644 --- a/src/test/java/com/process/clash/application/compete/rival/rival/service/GetMyRivalActingServiceTest.java +++ b/src/test/java/com/process/clash/application/compete/rival/rival/service/GetMyRivalActingServiceTest.java @@ -1,261 +1,263 @@ -//package com.process.clash.application.compete.rival.rival.service; -// -//import com.process.clash.application.common.actor.Actor; -//import com.process.clash.application.compete.rival.rival.data.GetMyRivalActingData; -//import com.process.clash.application.compete.rival.rival.port.out.RivalRepositoryPort; -//import com.process.clash.application.profile.data.EquippedItemsData; -//import com.process.clash.application.profile.service.EquippedItemsAssembler; -//import com.process.clash.application.realtime.data.UserActivityStatus; -//import com.process.clash.application.realtime.port.out.UserPresencePort; -//import com.process.clash.application.record.v2.port.out.RecordSessionV2RepositoryPort; -//import com.process.clash.application.user.user.port.out.UserRepositoryPort; -//import com.process.clash.domain.common.enums.Major; -//import com.process.clash.domain.record.enums.MonitoredApp; -//import com.process.clash.domain.record.v2.entity.RecordSessionV2; -//import com.process.clash.domain.rival.rival.entity.Rival; -//import com.process.clash.domain.rival.rival.enums.RivalLinkingStatus; -//import com.process.clash.domain.user.user.entity.User; -//import com.process.clash.domain.user.user.enums.Role; -//import com.process.clash.domain.user.user.enums.UserStatus; -//import com.process.clash.domain.user.userrankhistory.enums.ExpTier; -//import com.process.clash.domain.user.userrankhistory.enums.RankTier; -//import com.process.clash.infrastructure.config.record.RecordProperties; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.DisplayName; -//import org.junit.jupiter.api.Test; -//import org.junit.jupiter.api.extension.ExtendWith; -//import org.mockito.Mock; -//import org.mockito.junit.jupiter.MockitoExtension; -// -//import java.time.Instant; -//import java.time.ZoneId; -//import java.util.List; -//import java.util.Map; -// -//import static org.assertj.core.api.Assertions.assertThat; -//import static org.mockito.ArgumentMatchers.any; -//import static org.mockito.ArgumentMatchers.anyList; -//import static org.mockito.Mockito.when; -// -//@ExtendWith(MockitoExtension.class) -//class GetMyRivalActingServiceTest { -// -// @Mock -// private RivalRepositoryPort rivalRepositoryPort; -// -// @Mock -// private RecordSessionV2RepositoryPort recordSessionRepositoryPort; -// -// @Mock -// private UserRepositoryPort userRepositoryPort; -// -// @Mock -// private UserPresencePort userPresencePort; -// -// private GetMyRivalActingService getMyRivalActingService; -// @Mock -// private EquippedItemsAssembler equippedItemsAssembler; -// -// @BeforeEach -// void setUp() { -// getMyRivalActingService = new GetMyRivalActingService( -// rivalRepositoryPort, -// recordSessionRepositoryPort, -// userRepositoryPort, -// userPresencePort, -// -// new RecordProperties("UTC", 0), -// ZoneId.of("UTC"), -// -// equippedItemsAssembler -// ); -// } -// -// @Test -// @DisplayName("내 라이벌 조회 시 각 라이벌의 실시간 활동 상태를 반환한다") -// void execute_returnsRivalsWithActivityStatus() { -// Actor actor = new Actor(1L); -// GetMyRivalActingData.Command command = new GetMyRivalActingData.Command(actor); -// -// Rival rivalA = new Rival( -// 100L, -// Instant.now().minusSeconds(86_400), -// Instant.now(), -// RivalLinkingStatus.ACCEPTED, -// 1L, -// 2L -// ); -// Rival rivalB = new Rival( -// 101L, -// Instant.now().minusSeconds(86_400), -// Instant.now(), -// RivalLinkingStatus.ACCEPTED, -// 3L, -// 1L -// ); -// -// User rivalUserA = createUser(2L, "rivalA", "Rival A"); -// User rivalUserB = createUser(3L, "rivalB", "Rival B"); -// -// when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rivalA, rivalB)); -// when(userRepositoryPort.findAllByIds(anyList())) -// .thenReturn(List.of(rivalUserA, rivalUserB, createUser(1L, "me", "Me"))); -// when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds( -// anyList(), -// any(), -// any() -// )).thenReturn(Map.of(2L, 1800L, 3L, 600L)); -// when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())) -// .thenReturn(List.of( -// RecordSessionV2.createDevelop( -// 2L, -// MonitoredApp.VSCODE, -// Instant.now().minusSeconds(300) -// ), -// RecordSessionV2.createTask( -// 3L, -// null, -// null, -// null, -// "Task B", -// Instant.now().minusSeconds(600) -// ) -// )); -// when(userPresencePort.getStatuses(anyList())) -// .thenReturn(Map.of(2L, UserActivityStatus.ONLINE, 3L, UserActivityStatus.AWAY, 1L, UserActivityStatus.ONLINE)); -// when(equippedItemsAssembler.loadByUserIds(anyList())).thenReturn(Map.of()); -// -// GetMyRivalActingData.Result result = getMyRivalActingService.execute(command); -// -// assertThat(result.myRivals()).hasSize(2); -// assertThat(result.myRivals().get(0).rivalId()).isEqualTo(100L); -// assertThat(result.myRivals().get(1).rivalId()).isEqualTo(101L); -// assertThat(result.myRivals().get(0).status()).isEqualTo(UserActivityStatus.ONLINE); -// assertThat(result.myRivals().get(1).status()).isEqualTo(UserActivityStatus.AWAY); -// assertThat(result.myRivals().get(0).usingApp()).isEqualTo("VSCODE"); -// assertThat(result.myRivals().get(1).usingApp()).isNull(); -// assertThat(result.myRivals().get(0).isStudying()).isTrue(); -// assertThat(result.myRivals().get(1).isStudying()).isTrue(); -// assertThat(result.myRivals().get(0).tier()).isEqualTo("UNRANKED"); -// } -// -// @Test -// @DisplayName("rankTier가 NONE이면 expTier를 tier로 반환한다") -// void execute_returnsTierAsExpTier_whenRankTierIsNone() { -// Actor actor = new Actor(1L); -// Rival rival = new Rival(100L, Instant.now().minusSeconds(86_400), Instant.now(), -// RivalLinkingStatus.ACCEPTED, 1L, 2L); -// -// User rivalUser = createUser(2L, "rivalA", "Rival A", RankTier.NONE, ExpTier.GOLD); -// -// when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rival)); -// when(userRepositoryPort.findAllByIds(anyList())) -// .thenReturn(List.of(rivalUser, createUser(1L, "me", "Me"))); -// when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds(anyList(), any(), any())) -// .thenReturn(Map.of()); -// when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())).thenReturn(List.of()); -// when(userPresencePort.getStatuses(anyList())).thenReturn(Map.of(2L, UserActivityStatus.OFFLINE)); -// when(equippedItemsAssembler.loadByUserIds(anyList())).thenReturn(Map.of()); -// -// GetMyRivalActingData.Result result = getMyRivalActingService.execute( -// new GetMyRivalActingData.Command(actor)); -// -// assertThat(result.myRivals().get(0).tier()).isEqualTo("GOLD"); -// } -// -// @Test -// @DisplayName("rankTier가 NONE이 아니면 rankTier를 tier로 반환한다") -// void execute_returnsTierAsRankTier_whenRankTierIsNotNone() { -// Actor actor = new Actor(1L); -// Rival rival = new Rival(100L, Instant.now().minusSeconds(86_400), Instant.now(), -// RivalLinkingStatus.ACCEPTED, 1L, 2L); -// -// User rivalUser = createUser(2L, "rivalA", "Rival A", RankTier.MASTER, ExpTier.DIAMOND); -// -// when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rival)); -// when(userRepositoryPort.findAllByIds(anyList())) -// .thenReturn(List.of(rivalUser, createUser(1L, "me", "Me"))); -// when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds(anyList(), any(), any())) -// .thenReturn(Map.of()); -// when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())).thenReturn(List.of()); -// when(userPresencePort.getStatuses(anyList())).thenReturn(Map.of(2L, UserActivityStatus.OFFLINE)); -// when(equippedItemsAssembler.loadByUserIds(anyList())).thenReturn(Map.of()); -// -// GetMyRivalActingData.Result result = getMyRivalActingService.execute( -// new GetMyRivalActingData.Command(actor)); -// -// assertThat(result.myRivals().get(0).tier()).isEqualTo("MASTER"); -// } -// -// @Test -// @DisplayName("라이벌이 없으면 빈 리스트를 반환한다") -// void execute_returnsEmptyList_whenNoRivals() { -// Actor actor = new Actor(1L); -// when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of()); -// -// GetMyRivalActingData.Result result = getMyRivalActingService.execute( -// new GetMyRivalActingData.Command(actor)); -// -// assertThat(result.myRivals()).isEmpty(); -// } -// -// @Test -// @DisplayName("라이벌의 equippedItems가 응답에 올바르게 포함된다") -// void execute_returnsEquippedItems() { -// Actor actor = new Actor(1L); -// Rival rival = new Rival(100L, Instant.now().minusSeconds(86_400), Instant.now(), -// RivalLinkingStatus.ACCEPTED, 1L, 2L); -// -// User rivalUser = createUser(2L, "rivalA", "Rival A"); -// -// EquippedItemsData equippedItemsData = new EquippedItemsData( -// new EquippedItemsData.EquippedItemData(1L, "배지이름", "badge.png"), -// new EquippedItemsData.EquippedItemData(2L, "닉네임판", "nameplate.png"), -// null -// ); -// -// when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rival)); -// when(userRepositoryPort.findAllByIds(anyList())) -// .thenReturn(List.of(rivalUser, createUser(1L, "me", "Me"))); -// when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds(anyList(), any(), any())) -// .thenReturn(Map.of()); -// when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())).thenReturn(List.of()); -// when(userPresencePort.getStatuses(anyList())).thenReturn(Map.of(2L, UserActivityStatus.OFFLINE)); -// when(equippedItemsAssembler.loadByUserIds(anyList())) -// .thenReturn(Map.of(2L, equippedItemsData)); -// -// GetMyRivalActingData.Result result = getMyRivalActingService.execute( -// new GetMyRivalActingData.Command(actor)); -// -// EquippedItemsData actual = result.myRivals().get(0).equippedItems(); -// assertThat(actual.insignia().id()).isEqualTo(1L); -// assertThat(actual.insignia().name()).isEqualTo("배지이름"); -// assertThat(actual.nameplate().id()).isEqualTo(2L); -// assertThat(actual.banner()).isNull(); -// } -// -// private User createUser(Long id, String username, String name) { -// return createUser(id, username, name, RankTier.NONE, ExpTier.UNRANKED); -// } -// -// private User createUser(Long id, String username, String name, RankTier rankTier, ExpTier expTier) { -// return new User( -// id, -// Instant.now().minusSeconds(86_400), -// Instant.now(), -// username, -// username + "@example.com", -// name, -// "encoded-password", -// Role.USER, -// "", -// 0, -// 0, -// Major.NONE, -// UserStatus.ACTIVE, -// null, -// rankTier, -// expTier -// ); -// } -//} +package com.process.clash.application.compete.rival.rival.service; + +import com.process.clash.application.common.actor.Actor; +import com.process.clash.application.compete.rival.rival.data.GetMyRivalActingData; +import com.process.clash.application.compete.rival.rival.port.out.RivalRepositoryPort; +import com.process.clash.application.profile.data.EquippedItemsData; +import com.process.clash.application.profile.service.EquippedItemsAssembler; +import com.process.clash.application.realtime.data.UserActivityStatus; +import com.process.clash.application.realtime.port.out.UserPresencePort; +import com.process.clash.application.record.v2.port.out.RecordSessionV2RepositoryPort; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.record.enums.MonitoredApp; +import com.process.clash.domain.record.v2.entity.RecordSessionV2; +import com.process.clash.domain.rival.rival.entity.Rival; +import com.process.clash.domain.rival.rival.enums.RivalLinkingStatus; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.user.enums.Role; +import com.process.clash.domain.user.user.enums.UserStatus; +import com.process.clash.domain.user.userrankhistory.enums.ExpTier; +import com.process.clash.domain.user.userrankhistory.enums.RankTier; +import com.process.clash.infrastructure.config.record.RecordProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.time.ZoneId; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GetMyRivalActingServiceTest { + + @Mock + private RivalRepositoryPort rivalRepositoryPort; + + @Mock + private RecordSessionV2RepositoryPort recordSessionRepositoryPort; + + @Mock + private UserRepositoryPort userRepositoryPort; + + @Mock + private UserPresencePort userPresencePort; + + private GetMyRivalActingService getMyRivalActingService; + @Mock + private EquippedItemsAssembler equippedItemsAssembler; + + @BeforeEach + void setUp() { + getMyRivalActingService = new GetMyRivalActingService( + rivalRepositoryPort, + recordSessionRepositoryPort, + userRepositoryPort, + userPresencePort, + + new RecordProperties("UTC", 0), + ZoneId.of("UTC"), + + equippedItemsAssembler + ); + } + + @Test + @DisplayName("내 라이벌 조회 시 각 라이벌의 실시간 활동 상태를 반환한다") + void execute_returnsRivalsWithActivityStatus() { + Actor actor = new Actor(1L); + GetMyRivalActingData.Command command = new GetMyRivalActingData.Command(actor); + + Rival rivalA = new Rival( + 100L, + Instant.now().minusSeconds(86_400), + Instant.now(), + RivalLinkingStatus.ACCEPTED, + 1L, + 2L + ); + Rival rivalB = new Rival( + 101L, + Instant.now().minusSeconds(86_400), + Instant.now(), + RivalLinkingStatus.ACCEPTED, + 3L, + 1L + ); + + User rivalUserA = createUser(2L, "rivalA", "Rival A"); + User rivalUserB = createUser(3L, "rivalB", "Rival B"); + + when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rivalA, rivalB)); + when(userRepositoryPort.findAllByIds(anyList())) + .thenReturn(List.of(rivalUserA, rivalUserB, createUser(1L, "me", "Me"))); + when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds( + anyList(), + any(), + any() + )).thenReturn(Map.of(2L, 1800L, 3L, 600L)); + when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())) + .thenReturn(List.of( + RecordSessionV2.createDevelop( + 2L, + MonitoredApp.VSCODE, + Instant.now().minusSeconds(300) + ), + RecordSessionV2.createTask( + 3L, + null, + null, + null, + "Task B", + Instant.now().minusSeconds(600) + ) + )); + when(userPresencePort.getStatuses(anyList())) + .thenReturn(Map.of(2L, UserActivityStatus.ONLINE, 3L, UserActivityStatus.AWAY, 1L, UserActivityStatus.ONLINE)); + when(equippedItemsAssembler.loadByUserIds(anyList())).thenReturn(Map.of()); + + GetMyRivalActingData.Result result = getMyRivalActingService.execute(command); + + assertThat(result.myRivals()).hasSize(2); + assertThat(result.myRivals().get(0).rivalId()).isEqualTo(100L); + assertThat(result.myRivals().get(1).rivalId()).isEqualTo(101L); + assertThat(result.myRivals().get(0).status()).isEqualTo(UserActivityStatus.ONLINE); + assertThat(result.myRivals().get(1).status()).isEqualTo(UserActivityStatus.AWAY); + assertThat(result.myRivals().get(0).usingApp()).isEqualTo("VSCODE"); + assertThat(result.myRivals().get(1).usingApp()).isNull(); + assertThat(result.myRivals().get(0).isStudying()).isTrue(); + assertThat(result.myRivals().get(1).isStudying()).isTrue(); + assertThat(result.myRivals().get(0).tier()).isEqualTo("UNRANKED"); + } + + @Test + @DisplayName("rankTier가 NONE이면 expTier를 tier로 반환한다") + void execute_returnsTierAsExpTier_whenRankTierIsNone() { + Actor actor = new Actor(1L); + Rival rival = new Rival(100L, Instant.now().minusSeconds(86_400), Instant.now(), + RivalLinkingStatus.ACCEPTED, 1L, 2L); + + User rivalUser = createUser(2L, "rivalA", "Rival A", RankTier.NONE, ExpTier.GOLD); + + when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rival)); + when(userRepositoryPort.findAllByIds(anyList())) + .thenReturn(List.of(rivalUser, createUser(1L, "me", "Me"))); + when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds(anyList(), any(), any())) + .thenReturn(Map.of()); + when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())).thenReturn(List.of()); + when(userPresencePort.getStatuses(anyList())).thenReturn(Map.of(2L, UserActivityStatus.OFFLINE)); + when(equippedItemsAssembler.loadByUserIds(anyList())).thenReturn(Map.of()); + + GetMyRivalActingData.Result result = getMyRivalActingService.execute( + new GetMyRivalActingData.Command(actor)); + + assertThat(result.myRivals().get(0).tier()).isEqualTo("GOLD"); + } + + @Test + @DisplayName("rankTier가 NONE이 아니면 rankTier를 tier로 반환한다") + void execute_returnsTierAsRankTier_whenRankTierIsNotNone() { + Actor actor = new Actor(1L); + Rival rival = new Rival(100L, Instant.now().minusSeconds(86_400), Instant.now(), + RivalLinkingStatus.ACCEPTED, 1L, 2L); + + User rivalUser = createUser(2L, "rivalA", "Rival A", RankTier.MASTER, ExpTier.DIAMOND); + + when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rival)); + when(userRepositoryPort.findAllByIds(anyList())) + .thenReturn(List.of(rivalUser, createUser(1L, "me", "Me"))); + when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds(anyList(), any(), any())) + .thenReturn(Map.of()); + when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())).thenReturn(List.of()); + when(userPresencePort.getStatuses(anyList())).thenReturn(Map.of(2L, UserActivityStatus.OFFLINE)); + when(equippedItemsAssembler.loadByUserIds(anyList())).thenReturn(Map.of()); + + GetMyRivalActingData.Result result = getMyRivalActingService.execute( + new GetMyRivalActingData.Command(actor)); + + assertThat(result.myRivals().get(0).tier()).isEqualTo("MASTER"); + } + + @Test + @DisplayName("라이벌이 없으면 빈 리스트를 반환한다") + void execute_returnsEmptyList_whenNoRivals() { + Actor actor = new Actor(1L); + when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of()); + + GetMyRivalActingData.Result result = getMyRivalActingService.execute( + new GetMyRivalActingData.Command(actor)); + + assertThat(result.myRivals()).isEmpty(); + } + + @Test + @DisplayName("라이벌의 equippedItems가 응답에 올바르게 포함된다") + void execute_returnsEquippedItems() { + Actor actor = new Actor(1L); + Rival rival = new Rival(100L, Instant.now().minusSeconds(86_400), Instant.now(), + RivalLinkingStatus.ACCEPTED, 1L, 2L); + + User rivalUser = createUser(2L, "rivalA", "Rival A"); + + EquippedItemsData equippedItemsData = new EquippedItemsData( + new EquippedItemsData.EquippedItemData(1L, "배지이름", "badge.png", null), + new EquippedItemsData.EquippedItemData(2L, "닉네임판", "nameplate.png", null), + null, + null + ); + + when(rivalRepositoryPort.findAllByUserId(actor.id())).thenReturn(List.of(rival)); + when(userRepositoryPort.findAllByIds(anyList())) + .thenReturn(List.of(rivalUser, createUser(1L, "me", "Me"))); + when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds(anyList(), any(), any())) + .thenReturn(Map.of()); + when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())).thenReturn(List.of()); + when(userPresencePort.getStatuses(anyList())).thenReturn(Map.of(2L, UserActivityStatus.OFFLINE)); + when(equippedItemsAssembler.loadByUserIds(anyList())) + .thenReturn(Map.of(2L, equippedItemsData)); + + GetMyRivalActingData.Result result = getMyRivalActingService.execute( + new GetMyRivalActingData.Command(actor)); + + EquippedItemsData actual = result.myRivals().get(0).equippedItems(); + assertThat(actual.insignia().id()).isEqualTo(1L); + assertThat(actual.insignia().name()).isEqualTo("배지이름"); + assertThat(actual.nameplate().id()).isEqualTo(2L); + assertThat(actual.banner()).isNull(); + } + + private User createUser(Long id, String username, String name) { + return createUser(id, username, name, RankTier.NONE, ExpTier.UNRANKED); + } + + private User createUser(Long id, String username, String name, RankTier rankTier, ExpTier expTier) { + return new User( + id, + Instant.now().minusSeconds(86_400), + Instant.now(), + username, + username + "@example.com", + name, + "encoded-password", + Role.USER, + "", + 0, + 0, + Major.NONE, + UserStatus.ACTIVE, + null, + false, + rankTier, + expTier + ); + } +} diff --git a/src/test/java/com/process/clash/application/group/service/CreateGroupServiceTest.java b/src/test/java/com/process/clash/application/group/service/CreateGroupServiceTest.java index 74fe8789b..f0322449a 100644 --- a/src/test/java/com/process/clash/application/group/service/CreateGroupServiceTest.java +++ b/src/test/java/com/process/clash/application/group/service/CreateGroupServiceTest.java @@ -182,7 +182,7 @@ private User createUser(Long id) { id, Instant.now(), Instant.now(), "testuser", "test@example.com", "테스트유저", "password", Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, - RankTier.NONE, ExpTier.UNRANKED + false, RankTier.NONE, ExpTier.UNRANKED ); } } \ No newline at end of file diff --git a/src/test/java/com/process/clash/application/group/service/GetGroupActivityServiceTest.java b/src/test/java/com/process/clash/application/group/service/GetGroupActivityServiceTest.java index de79d3af6..226243808 100644 --- a/src/test/java/com/process/clash/application/group/service/GetGroupActivityServiceTest.java +++ b/src/test/java/com/process/clash/application/group/service/GetGroupActivityServiceTest.java @@ -127,6 +127,7 @@ private User createUser(Long id, String name) { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/profile/service/GetMyProfileServiceTest.java b/src/test/java/com/process/clash/application/profile/service/GetMyProfileServiceTest.java index 98e5e49d6..95418f0fe 100644 --- a/src/test/java/com/process/clash/application/profile/service/GetMyProfileServiceTest.java +++ b/src/test/java/com/process/clash/application/profile/service/GetMyProfileServiceTest.java @@ -133,6 +133,7 @@ private User createUser(Long id, int totalExp, RankTier currentRankTier) { Major.NONE, UserStatus.ACTIVE, null, + false, currentRankTier, ExpTier.fromExp(totalExp) ); diff --git a/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java b/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java index 63dddd4af..20392eaeb 100644 --- a/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java +++ b/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java @@ -277,7 +277,7 @@ private User createUser(Long id, int totalExp, RankTier currentRankTier) { id, Instant.now(), Instant.now(), "user" + id, "user" + id + "@test.com", "User" + id, "pw", Role.USER, "", totalExp, 0, - Major.NONE, UserStatus.ACTIVE, null, currentRankTier, + Major.NONE, UserStatus.ACTIVE, null, false, currentRankTier, ExpTier.fromExp(totalExp) ); } diff --git a/src/test/java/com/process/clash/application/record/v2/service/CreateSubjectV2ServiceTest.java b/src/test/java/com/process/clash/application/record/v2/service/CreateSubjectV2ServiceTest.java index 71fe2ae51..6911d5675 100644 --- a/src/test/java/com/process/clash/application/record/v2/service/CreateSubjectV2ServiceTest.java +++ b/src/test/java/com/process/clash/application/record/v2/service/CreateSubjectV2ServiceTest.java @@ -88,6 +88,7 @@ private User createUser(Long id) { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/record/v2/service/GetActivityStatisticsV2ServiceTest.java b/src/test/java/com/process/clash/application/record/v2/service/GetActivityStatisticsV2ServiceTest.java index 36cbec2e3..78152c13a 100644 --- a/src/test/java/com/process/clash/application/record/v2/service/GetActivityStatisticsV2ServiceTest.java +++ b/src/test/java/com/process/clash/application/record/v2/service/GetActivityStatisticsV2ServiceTest.java @@ -152,6 +152,7 @@ private User createUser(Long id) { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/record/v2/service/GetTodayRecordV2ServiceTest.java b/src/test/java/com/process/clash/application/record/v2/service/GetTodayRecordV2ServiceTest.java index a36750986..0161f6b31 100644 --- a/src/test/java/com/process/clash/application/record/v2/service/GetTodayRecordV2ServiceTest.java +++ b/src/test/java/com/process/clash/application/record/v2/service/GetTodayRecordV2ServiceTest.java @@ -238,6 +238,7 @@ private User createUser(Long id) { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/record/v2/service/StartRecordV2ServiceTest.java b/src/test/java/com/process/clash/application/record/v2/service/StartRecordV2ServiceTest.java index fd2f3e798..68cc861cf 100644 --- a/src/test/java/com/process/clash/application/record/v2/service/StartRecordV2ServiceTest.java +++ b/src/test/java/com/process/clash/application/record/v2/service/StartRecordV2ServiceTest.java @@ -531,6 +531,7 @@ private User createUser(Long id) { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/roadmap/v2/question/service/SubmitQuestionV2AnswerServiceTest.java b/src/test/java/com/process/clash/application/roadmap/v2/question/service/SubmitQuestionV2AnswerServiceTest.java index b8aa7f924..d230935c4 100644 --- a/src/test/java/com/process/clash/application/roadmap/v2/question/service/SubmitQuestionV2AnswerServiceTest.java +++ b/src/test/java/com/process/clash/application/roadmap/v2/question/service/SubmitQuestionV2AnswerServiceTest.java @@ -165,6 +165,7 @@ private User createUser(Long userId, int totalCookie) { Major.SERVER, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/shop/season/service/SeasonFinishServiceTest.java b/src/test/java/com/process/clash/application/shop/season/service/SeasonFinishServiceTest.java index 4d8934811..8520bd4e5 100644 --- a/src/test/java/com/process/clash/application/shop/season/service/SeasonFinishServiceTest.java +++ b/src/test/java/com/process/clash/application/shop/season/service/SeasonFinishServiceTest.java @@ -272,7 +272,7 @@ private User createUser(Long id, int totalExp, int totalCookie) { id, Instant.now(), Instant.now(), "user" + id, "user" + id + "@test.com", "User" + id, "pw", Role.USER, "", totalExp, totalCookie, - Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, + Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.fromExp(totalExp) ); } diff --git a/src/test/java/com/process/clash/application/user/exp/service/GithubExpGrantServiceTest.java b/src/test/java/com/process/clash/application/user/exp/service/GithubExpGrantServiceTest.java index b46af0364..b51fab414 100644 --- a/src/test/java/com/process/clash/application/user/exp/service/GithubExpGrantServiceTest.java +++ b/src/test/java/com/process/clash/application/user/exp/service/GithubExpGrantServiceTest.java @@ -281,7 +281,7 @@ private GitHubDailyStats createStats(Long userId, int commits, int prs, int revi private User createUser(Long id, int totalExp) { return new User(id, Instant.now(), Instant.now(), "user" + id, "user@example.com", - "name", "pw", Role.USER, "", totalExp, 0, Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, + "name", "pw", Role.USER, "", totalExp, 0, Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.fromExp(totalExp)); } } diff --git a/src/test/java/com/process/clash/application/user/exp/service/StudyTimeExpGrantServiceTest.java b/src/test/java/com/process/clash/application/user/exp/service/StudyTimeExpGrantServiceTest.java index 61eecaa71..721b0c196 100644 --- a/src/test/java/com/process/clash/application/user/exp/service/StudyTimeExpGrantServiceTest.java +++ b/src/test/java/com/process/clash/application/user/exp/service/StudyTimeExpGrantServiceTest.java @@ -277,7 +277,7 @@ void grant_usesTodayAsStudyDateWhenStartedAfterBoundary() { private User createUser(Long id, int totalExp) { return new User(id, Instant.now(), Instant.now(), "user" + id, "user@example.com", - "name", "pw", Role.USER, "", totalExp, 0, Major.NONE, UserStatus.ACTIVE, null, RankTier.NONE, + "name", "pw", Role.USER, "", totalExp, 0, Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, ExpTier.fromExp(totalExp)); } } diff --git a/src/test/java/com/process/clash/application/user/user/service/ResetPasswordServiceTest.java b/src/test/java/com/process/clash/application/user/user/service/ResetPasswordServiceTest.java index 6d3ea2b99..fc9714d34 100644 --- a/src/test/java/com/process/clash/application/user/user/service/ResetPasswordServiceTest.java +++ b/src/test/java/com/process/clash/application/user/user/service/ResetPasswordServiceTest.java @@ -111,7 +111,7 @@ private User createUser(Long id, String password) { id, Instant.now(), Instant.now(), "testuser", "test@example.com", "테스트유저", password, Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, null, - RankTier.NONE, ExpTier.UNRANKED + false, RankTier.NONE, ExpTier.UNRANKED ); } } diff --git a/src/test/java/com/process/clash/application/user/user/service/UpdateMyProfileImageServiceTest.java b/src/test/java/com/process/clash/application/user/user/service/UpdateMyProfileImageServiceTest.java index a1aadf5c6..96dcf7af0 100644 --- a/src/test/java/com/process/clash/application/user/user/service/UpdateMyProfileImageServiceTest.java +++ b/src/test/java/com/process/clash/application/user/user/service/UpdateMyProfileImageServiceTest.java @@ -95,6 +95,7 @@ private User createUser(Long id) { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/user/user/service/WithdrawnUserQueryTest.java b/src/test/java/com/process/clash/application/user/user/service/WithdrawnUserQueryTest.java index 4a5dc5053..524ed9f48 100644 --- a/src/test/java/com/process/clash/application/user/user/service/WithdrawnUserQueryTest.java +++ b/src/test/java/com/process/clash/application/user/user/service/WithdrawnUserQueryTest.java @@ -131,6 +131,7 @@ private User withdrawnUser(Long id) { Major.NONE, UserStatus.ACTIVE, Instant.now(), // deletedAt 설정 → 탈퇴 상태 + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/application/user/usergithub/service/LinkGitHubOAuthServiceTest.java b/src/test/java/com/process/clash/application/user/usergithub/service/LinkGitHubOAuthServiceTest.java index 910aef4e5..bbc9064fb 100644 --- a/src/test/java/com/process/clash/application/user/usergithub/service/LinkGitHubOAuthServiceTest.java +++ b/src/test/java/com/process/clash/application/user/usergithub/service/LinkGitHubOAuthServiceTest.java @@ -147,6 +147,7 @@ private User createUser(Long id) { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); diff --git a/src/test/java/com/process/clash/domain/user/user/entity/UserTest.java b/src/test/java/com/process/clash/domain/user/user/entity/UserTest.java index 6832c3fcf..1f6ee971e 100644 --- a/src/test/java/com/process/clash/domain/user/user/entity/UserTest.java +++ b/src/test/java/com/process/clash/domain/user/user/entity/UserTest.java @@ -46,6 +46,7 @@ private User activeUser() { Major.NONE, UserStatus.ACTIVE, null, + false, RankTier.NONE, ExpTier.UNRANKED ); @@ -67,6 +68,7 @@ private User deletedUser() { Major.NONE, UserStatus.ACTIVE, Instant.now(), + false, RankTier.NONE, ExpTier.UNRANKED ); From d7f3b67b1efd633fee311dee58a83d5412e2b110 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 23:35:52 +0900 Subject: [PATCH 14/20] =?UTF-8?q?fix:=20insigma=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=ED=98=B8=ED=99=98=20=EC=9C=A0=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20deprecated=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clash/adapter/web/user/dto/EquippedItemsDto.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/process/clash/adapter/web/user/dto/EquippedItemsDto.java b/src/main/java/com/process/clash/adapter/web/user/dto/EquippedItemsDto.java index 1bf9f3ec7..9c2c9a184 100644 --- a/src/main/java/com/process/clash/adapter/web/user/dto/EquippedItemsDto.java +++ b/src/main/java/com/process/clash/adapter/web/user/dto/EquippedItemsDto.java @@ -1,5 +1,6 @@ package com.process.clash.adapter.web.user.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import com.process.clash.application.profile.data.EquippedItemsData; public class EquippedItemsDto { @@ -21,6 +22,13 @@ public static Response from(EquippedItemsData data) { Item.from(data.bgm()) ); } + + /** @deprecated insignia 필드로 대체됨. 클라이언트 업데이트 후 제거 예정 */ + @Deprecated + @JsonProperty("insigma") + public Item insigma() { + return insignia; + } } public record Item( From 00ab1f6ab036bc9ab1ad0a280d6fa3de7943112a Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 23:36:11 +0900 Subject: [PATCH 15/20] =?UTF-8?q?fix:=20=ED=83=88=ED=87=B4=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20UserNotFoundExcept?= =?UTF-8?q?ion=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/profile/service/GetUserProfileService.java | 4 ++++ .../application/profile/service/GetUserRivalsService.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java b/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java index 2013a22af..4af1bdf3b 100644 --- a/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java +++ b/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java @@ -25,6 +25,10 @@ public GetUserProfileData.Result execute(GetUserProfileData.Command command) { User user = userRepositoryPort.findById(command.targetUserId()) .orElseThrow(UserNotFoundException::new); + if (user.isDeleted()) { + throw new UserNotFoundException(); + } + UserActivityStatus activityStatus = userPresencePort.getStatus(user.id()); return GetUserProfileData.Result.from( user, diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java b/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java index a5c2592eb..5cd163a1f 100644 --- a/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java +++ b/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java @@ -46,6 +46,10 @@ public GetMyRivalActingData.Result execute(GetUserRivalsData.Command command) { User targetUser = userRepositoryPort.findById(command.targetUserId()) .orElseThrow(UserNotFoundException::new); + if (targetUser.isDeleted()) { + throw new UserNotFoundException(); + } + if (targetUser.isPrivate()) { throw new ProfilePrivatedException(); } From fb012b539d98d6b537d2fb96d3050a5637ba97b5 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sun, 12 Apr 2026 23:36:23 +0900 Subject: [PATCH 16/20] =?UTF-8?q?test:=20=ED=83=88=ED=87=B4=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EB=B0=8F=20=EC=9C=A0=EC=A0=80=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=8C=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/GetUserProfileServiceTest.java | 18 ++ .../service/GetUserRivalsServiceTest.java | 202 ++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 src/test/java/com/process/clash/application/profile/service/GetUserRivalsServiceTest.java diff --git a/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java b/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java index 2292dd5da..7b120b7c9 100644 --- a/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java +++ b/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java @@ -77,6 +77,24 @@ void execute_throwsUserNotFoundException_whenUserNotFound() { .isInstanceOf(UserNotFoundException.class); } + @Test + @DisplayName("탈퇴한 유저 조회 시 UserNotFoundException을 던진다") + void execute_throwsUserNotFoundException_whenUserDeleted() { + Long targetUserId = 2L; + User deletedUser = new User( + targetUserId, Instant.now(), Instant.now(), + "username", "user@example.com", "name", "encoded-password", + Role.USER, "", 0, 0, Major.NONE, UserStatus.ACTIVE, + Instant.now(), false, RankTier.NONE, ExpTier.UNRANKED + ); + GetUserProfileData.Command command = new GetUserProfileData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(deletedUser)); + + assertThatThrownBy(() -> getUserProfileService.execute(command)) + .isInstanceOf(UserNotFoundException.class); + } + @Test @DisplayName("비공개 유저도 기본 프로필은 반환한다") void execute_returnsProfile_evenWhenPrivate() { diff --git a/src/test/java/com/process/clash/application/profile/service/GetUserRivalsServiceTest.java b/src/test/java/com/process/clash/application/profile/service/GetUserRivalsServiceTest.java new file mode 100644 index 000000000..142ba7cbf --- /dev/null +++ b/src/test/java/com/process/clash/application/profile/service/GetUserRivalsServiceTest.java @@ -0,0 +1,202 @@ +package com.process.clash.application.profile.service; + +import com.process.clash.application.compete.rival.rival.data.GetMyRivalActingData; +import com.process.clash.application.compete.rival.rival.port.out.RivalRepositoryPort; +import com.process.clash.application.profile.data.GetUserRivalsData; +import com.process.clash.application.profile.exception.exception.forbidden.ProfilePrivatedException; +import com.process.clash.application.realtime.data.UserActivityStatus; +import com.process.clash.application.realtime.port.out.UserPresencePort; +import com.process.clash.application.record.v2.port.out.RecordSessionV2RepositoryPort; +import com.process.clash.application.user.user.exception.exception.notfound.UserNotFoundException; +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.rival.rival.entity.Rival; +import com.process.clash.domain.rival.rival.enums.RivalLinkingStatus; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.user.enums.Role; +import com.process.clash.domain.user.user.enums.UserStatus; +import com.process.clash.domain.user.userrankhistory.enums.ExpTier; +import com.process.clash.domain.user.userrankhistory.enums.RankTier; +import com.process.clash.infrastructure.config.record.RecordProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.time.ZoneId; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GetUserRivalsServiceTest { + + @Mock + private UserRepositoryPort userRepositoryPort; + + @Mock + private RivalRepositoryPort rivalRepositoryPort; + + @Mock + private RecordSessionV2RepositoryPort recordSessionRepositoryPort; + + @Mock + private UserPresencePort userPresencePort; + + @Mock + private EquippedItemsAssembler equippedItemsAssembler; + + private GetUserRivalsService getUserRivalsService; + + @BeforeEach + void setUp() { + getUserRivalsService = new GetUserRivalsService( + userRepositoryPort, + rivalRepositoryPort, + recordSessionRepositoryPort, + userPresencePort, + new RecordProperties("UTC", 0), + ZoneId.of("UTC"), + equippedItemsAssembler + ); + } + + @Test + @DisplayName("존재하지 않는 유저 조회 시 UserNotFoundException을 던진다") + void execute_throwsUserNotFoundException_whenUserNotFound() { + Long targetUserId = 999L; + GetUserRivalsData.Command command = new GetUserRivalsData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> getUserRivalsService.execute(command)) + .isInstanceOf(UserNotFoundException.class); + } + + @Test + @DisplayName("탈퇴한 유저 조회 시 UserNotFoundException을 던진다") + void execute_throwsUserNotFoundException_whenUserDeleted() { + Long targetUserId = 2L; + User deletedUser = createDeletedUser(targetUserId); + GetUserRivalsData.Command command = new GetUserRivalsData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(deletedUser)); + + assertThatThrownBy(() -> getUserRivalsService.execute(command)) + .isInstanceOf(UserNotFoundException.class); + } + + @Test + @DisplayName("비공개 유저 조회 시 ProfilePrivatedException을 던진다") + void execute_throwsProfilePrivatedException_whenUserIsPrivate() { + Long targetUserId = 2L; + User privateUser = createUser(targetUserId, true); + GetUserRivalsData.Command command = new GetUserRivalsData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(privateUser)); + + assertThatThrownBy(() -> getUserRivalsService.execute(command)) + .isInstanceOf(ProfilePrivatedException.class); + } + + @Test + @DisplayName("라이벌이 없으면 빈 리스트를 반환한다") + void execute_returnsEmptyList_whenNoRivals() { + Long targetUserId = 2L; + User targetUser = createUser(targetUserId, false); + GetUserRivalsData.Command command = new GetUserRivalsData.Command(targetUserId); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + when(rivalRepositoryPort.findAllByUserId(targetUserId)).thenReturn(List.of()); + + GetMyRivalActingData.Result result = getUserRivalsService.execute(command); + + assertThat(result.myRivals()).isEmpty(); + } + + @Test + @DisplayName("라이벌 목록을 정상적으로 반환한다") + void execute_returnsRivalList() { + Long targetUserId = 2L; + User targetUser = createUser(targetUserId, false); + GetUserRivalsData.Command command = new GetUserRivalsData.Command(targetUserId); + + Rival rival = new Rival( + 100L, + Instant.now().minusSeconds(86_400), + Instant.now(), + RivalLinkingStatus.ACCEPTED, + targetUserId, + 3L + ); + + User opponentUser = createUser(3L, false); + + when(userRepositoryPort.findById(targetUserId)).thenReturn(Optional.of(targetUser)); + when(rivalRepositoryPort.findAllByUserId(targetUserId)).thenReturn(List.of(rival)); + when(userRepositoryPort.findAllByIds(anyList())).thenReturn(List.of(opponentUser)); + when(recordSessionRepositoryPort.getTotalStudyTimeInSecondsByUserIds(anyList(), any(), any())) + .thenReturn(Map.of()); + when(recordSessionRepositoryPort.findAllActiveSessionsByUserIds(anyList())).thenReturn(List.of()); + when(userPresencePort.getStatuses(anyList())).thenReturn(Map.of(3L, UserActivityStatus.OFFLINE)); + when(equippedItemsAssembler.loadByUserIds(anyList())).thenReturn(Map.of()); + + GetMyRivalActingData.Result result = getUserRivalsService.execute(command); + + assertThat(result.myRivals()).hasSize(1); + assertThat(result.myRivals().get(0).rivalId()).isEqualTo(100L); + } + + private User createUser(Long id, boolean isPrivate) { + return new User( + id, + Instant.now().minusSeconds(86_400), + Instant.now(), + "username-" + id, + "user" + id + "@example.com", + "name-" + id, + "encoded-password", + Role.USER, + "", + 0, + 0, + Major.NONE, + UserStatus.ACTIVE, + null, + isPrivate, + RankTier.NONE, + ExpTier.UNRANKED + ); + } + + private User createDeletedUser(Long id) { + return new User( + id, + Instant.now().minusSeconds(86_400), + Instant.now(), + "username-" + id, + "user" + id + "@example.com", + "name-" + id, + "encoded-password", + Role.USER, + "", + 0, + 0, + Major.NONE, + UserStatus.ACTIVE, + Instant.now(), + false, + RankTier.NONE, + ExpTier.UNRANKED + ); + } +} From e4cfadd6e4ae04f5a51d774e1aca7db8fd4f696b Mon Sep 17 00:00:00 2001 From: redjungi Date: Mon, 13 Apr 2026 01:00:32 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20GetUserProfileService=20=EB=B9=88?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=20(GetOtherUserProfileService=EB=A1=9C=20=EB=B3=80=EA=B2=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...serProfileService.java => GetOtherUserProfileService.java} | 2 +- .../profile/service/GetUserProfileServiceTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/com/process/clash/application/profile/service/{GetUserProfileService.java => GetOtherUserProfileService.java} (95%) diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java b/src/main/java/com/process/clash/application/profile/service/GetOtherUserProfileService.java similarity index 95% rename from src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java rename to src/main/java/com/process/clash/application/profile/service/GetOtherUserProfileService.java index 4af1bdf3b..c422a77b6 100644 --- a/src/main/java/com/process/clash/application/profile/service/GetUserProfileService.java +++ b/src/main/java/com/process/clash/application/profile/service/GetOtherUserProfileService.java @@ -14,7 +14,7 @@ @Service @Transactional(readOnly = true) @RequiredArgsConstructor -public class GetUserProfileService implements GetUserProfileUsecase { +public class GetOtherUserProfileService implements GetUserProfileUsecase { private final UserRepositoryPort userRepositoryPort; private final UserPresencePort userPresencePort; diff --git a/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java b/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java index 7b120b7c9..2182b4497 100644 --- a/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java +++ b/src/test/java/com/process/clash/application/profile/service/GetUserProfileServiceTest.java @@ -38,11 +38,11 @@ class GetUserProfileServiceTest { @Mock private EquippedItemsAssembler equippedItemsAssembler; - private GetUserProfileService getUserProfileService; + private GetOtherUserProfileService getUserProfileService; @BeforeEach void setUp() { - getUserProfileService = new GetUserProfileService( + getUserProfileService = new GetOtherUserProfileService( userRepositoryPort, userPresencePort, equippedItemsAssembler ); } From 4ac412bf0a407c73329ed54f3e0a1a69a6531413 Mon Sep 17 00:00:00 2001 From: redjungi Date: Mon, 13 Apr 2026 01:26:45 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=B2=84=EC=A0=84=20V32=20?= =?UTF-8?q?=E2=86=92=20V33=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...d_is_private_to_users.sql => V33__add_is_private_to_users.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V32__add_is_private_to_users.sql => V33__add_is_private_to_users.sql} (100%) diff --git a/src/main/resources/db/migration/V32__add_is_private_to_users.sql b/src/main/resources/db/migration/V33__add_is_private_to_users.sql similarity index 100% rename from src/main/resources/db/migration/V32__add_is_private_to_users.sql rename to src/main/resources/db/migration/V33__add_is_private_to_users.sql From d6d2be08b6d2ceb0f7b403388b7adf403aab5b6e Mon Sep 17 00:00:00 2001 From: redjungi Date: Mon, 13 Apr 2026 02:54:54 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=B2=84=EC=A0=84=20V33=20->=20V?= =?UTF-8?q?34=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...d_is_private_to_users.sql => V34__add_is_private_to_users.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V33__add_is_private_to_users.sql => V34__add_is_private_to_users.sql} (100%) diff --git a/src/main/resources/db/migration/V33__add_is_private_to_users.sql b/src/main/resources/db/migration/V34__add_is_private_to_users.sql similarity index 100% rename from src/main/resources/db/migration/V33__add_is_private_to_users.sql rename to src/main/resources/db/migration/V34__add_is_private_to_users.sql From c6f2de4cc968add24f84fcbc0726c31434556e87 Mon Sep 17 00:00:00 2001 From: redjungi Date: Mon, 13 Apr 2026 02:56:16 +0900 Subject: [PATCH 20/20] =?UTF-8?q?fix:=20=ED=83=88=ED=87=B4=ED=95=9C=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B2=8C=20=EC=83=81=EB=8C=80=EB=B0=A9=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=A0=84=EC=B2=B4=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=8B=A4=ED=8C=A8=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile/service/GetUserRivalsService.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java b/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java index 5cd163a1f..98b38c76c 100644 --- a/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java +++ b/src/main/java/com/process/clash/application/profile/service/GetUserRivalsService.java @@ -26,6 +26,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; @Service @@ -101,16 +102,13 @@ public GetMyRivalActingData.Result execute(GetUserRivalsData.Command command) { List myRivals = rivals.stream() .map(rival -> { - Long opponentId; - if (rival.firstUserId().equals(command.targetUserId())) { - opponentId = rival.secondUserId(); - } else { - opponentId = rival.firstUserId(); - } + Long opponentId = rival.firstUserId().equals(command.targetUserId()) + ? rival.secondUserId() + : rival.firstUserId(); User opponent = opponentMap.get(opponentId); if (opponent == null) { - throw new UserNotFoundException(); + return null; } Long activeTime = studyTimeMap.getOrDefault(opponentId, 0L); @@ -124,6 +122,7 @@ public GetMyRivalActingData.Result execute(GetUserRivalsData.Command command) { rival.id(), opponent, activeTime, usingApp, isStudying, activityStatus, equippedItems ); }) + .filter(Objects::nonNull) .toList(); return GetMyRivalActingData.Result.from(myRivals);