diff --git a/src/main/java/org/sopt/app/application/platform/PlatformService.java b/src/main/java/org/sopt/app/application/platform/PlatformService.java index b3bf0cf9..5552df96 100644 --- a/src/main/java/org/sopt/app/application/platform/PlatformService.java +++ b/src/main/java/org/sopt/app/application/platform/PlatformService.java @@ -1,8 +1,13 @@ package org.sopt.app.application.platform; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import org.sopt.app.application.platform.dto.PlatformUserIdsRequest; import org.sopt.app.application.platform.dto.PlatformUserInfoResponse; import org.sopt.app.application.platform.dto.PlatformUserInfoWrapper; @@ -13,9 +18,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.util.*; -import java.util.stream.Collectors; - @Slf4j @Service @RequiredArgsConstructor @@ -39,7 +41,7 @@ public PlatformUserInfoResponse getPlatformUserInfoResponse(Long userId) { final Map headers = createAuthorizationHeader(); final Map params = createQueryParams(Collections.singletonList(userId)); PlatformUserInfoWrapper platformUserInfoWrapper = platformClient.getPlatformUserInfo(headers, params); - List data= platformUserInfoWrapper.data(); + List data = platformUserInfoWrapper.data(); if (data == null || data.isEmpty()) { throw new BadRequestException(ErrorCode.PLATFORM_USER_NOT_EXISTS); } @@ -49,7 +51,7 @@ public PlatformUserInfoResponse getPlatformUserInfoResponse(Long userId) { public List getPlatformUserInfosResponse(List userIds) { final Map headers = createAuthorizationHeader(); - if(userIds == null || userIds.isEmpty()){ + if (userIds == null || userIds.isEmpty()) { return Collections.emptyList(); } @@ -59,7 +61,7 @@ public List getPlatformUserInfosResponse(List us PlatformUserInfoWrapper platformUserInfoWrapper = platformClient.getPlatformUserInfo(headers, params); - List data= platformUserInfoWrapper.data(); + List data = platformUserInfoWrapper.data(); if (data == null || data.isEmpty()) { throw new BadRequestException(ErrorCode.PLATFORM_USER_NOT_EXISTS); } @@ -98,11 +100,13 @@ public List getPlatformUserInfosResponseSmart(List generationList) { - return generationList.contains(currentGeneration) ? UserStatus.ACTIVE : UserStatus.INACTIVE; + public UserStatus getStatus(PlatformUserInfoResponse profile) { + return Long.valueOf(profile.getLastSoptGeneration()).equals(currentGeneration) + ? UserStatus.ACTIVE + : UserStatus.INACTIVE; } private Map createAuthorizationHeader() { @@ -126,12 +130,12 @@ private String toCsv(List userIds) { public List getMemberGenerationList(Long userId) { return getPlatformUserInfoResponse(userId) - .soptActivities().stream() - .map(PlatformUserInfoResponse.SoptActivities::generation) - .map(Integer::longValue) - .distinct() - .sorted(Comparator.reverseOrder()) - .toList(); + .soptActivities().stream() + .map(PlatformUserInfoResponse.SoptActivities::generation) + .map(Integer::longValue) + .distinct() + .sorted(Comparator.reverseOrder()) + .toList(); } public boolean isCurrentGeneration(Long generation) { diff --git a/src/main/java/org/sopt/app/application/platform/dto/PlatformUserInfoResponse.java b/src/main/java/org/sopt/app/application/platform/dto/PlatformUserInfoResponse.java index cd85de6a..041fae9c 100644 --- a/src/main/java/org/sopt/app/application/platform/dto/PlatformUserInfoResponse.java +++ b/src/main/java/org/sopt/app/application/platform/dto/PlatformUserInfoResponse.java @@ -1,6 +1,7 @@ package org.sopt.app.application.platform.dto; +import java.util.Comparator; import java.util.List; public record PlatformUserInfoResponse( @@ -10,15 +11,19 @@ public record PlatformUserInfoResponse( String birthday, String phone, String email, - int lastGeneration, + int lastGeneration, // 솝트 기수 기준으로 내려주긴 함. List soptActivities ) { public record SoptActivities( int activityId, int generation, String part, - String team + String team, + Boolean isSopt ){ + public boolean isSoptActivity() { + return Boolean.TRUE.equals(this.isSopt); + } } /** @@ -28,7 +33,29 @@ public record SoptActivities( public SoptActivities getLatestActivity() { if (soptActivities == null) return null; return soptActivities.stream() - .max(java.util.Comparator.comparingInt(SoptActivities::generation)) + .max(Comparator.comparingInt(SoptActivities::generation)) .orElse(null); } + + /** + * isSopt=true인 활동 중 가장 최신 기수의 SOPT 정규 활동을 반환. + * 반환값이 null이면 현재 기수에 솝트 활동이 없음. + */ + public SoptActivities getLatestSoptActivity() { + if (soptActivities == null) return null; + return soptActivities.stream() + .filter(SoptActivities::isSoptActivity) + .max(Comparator.comparingInt(SoptActivities::generation)) + .orElse(null); + } + + public int getLastSoptGeneration(){ + if (soptActivities == null || soptActivities.isEmpty()) return 0; + + return soptActivities.stream() + .filter(SoptActivities::isSoptActivity) + .map(SoptActivities::generation) + .max(Integer::compareTo) + .orElse(0); + } } diff --git a/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java b/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java index c1475da0..ac7eeb0c 100755 --- a/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java +++ b/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java @@ -3,8 +3,9 @@ import static org.sopt.app.domain.entity.soptamp.SoptampUser.createNewSoptampUser; import static org.sopt.app.domain.enums.SoptPart.findSoptPartByPartName; -import java.util.*; - +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; import org.sopt.app.application.platform.dto.PlatformUserInfoResponse; import org.sopt.app.application.rank.CachedUserInfo; import org.sopt.app.application.rank.RankCacheService; @@ -20,8 +21,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; - @Service @RequiredArgsConstructor public class SoptampUserService { @@ -47,7 +46,8 @@ public SoptampUserInfo editProfileMessage(Long userId, String profileMessage) { SoptampUser soptampUser = soptampUserRepository.findByUserId(userId) .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND)); soptampUser.updateProfileMessage(profileMessage); - rankCacheService.updateCachedUserInfo(soptampUser.getUserId(), CachedUserInfo.of(SoptampUserInfo.of(soptampUser))); + rankCacheService.updateCachedUserInfo(soptampUser.getUserId(), + CachedUserInfo.of(SoptampUserInfo.of(soptampUser))); return SoptampUserInfo.of(soptampUser); } @@ -58,14 +58,19 @@ public SoptampUserInfo editProfileMessage(Long userId, String profileMessage) { public void upsertSoptampUser(PlatformUserInfoResponse profile, Long userId) { if (profile == null) return; - var latest = profile.getLatestActivity(); - if (latest == null) - return; if (appjamMode) { + var latest = profile.getLatestActivity(); + if (latest == null) { + return; + } upsertSoptampUserForAppjam(profile, userId, latest); } else { - upsertSoptampUserNormal(profile, userId, latest); + var latestSopt = profile.getLatestSoptActivity(); + if (latestSopt == null) { + return; + } + upsertSoptampUserNormal(profile, userId, latestSopt); } } @@ -73,37 +78,41 @@ public void upsertSoptampUser(PlatformUserInfoResponse profile, Long userId) { // 기본 시즌용 upsert (파트 + 이름 기반 닉네임) private void upsertSoptampUserNormal(PlatformUserInfoResponse profile, Long userId, - PlatformUserInfoResponse.SoptActivities latest) { + PlatformUserInfoResponse.SoptActivities latest) { Optional user = soptampUserRepository.findByUserId(userId); if (user.isEmpty()) { this.createSoptampUserNormal(profile, userId, latest); return; } SoptampUser registeredUser = user.get(); - if(this.isGenerationChanged(registeredUser, (long)profile.lastGeneration())) { + if (this.isGenerationChanged(registeredUser, (long) profile.lastGeneration())) { updateSoptampUserNormal(registeredUser, profile, latest); } } - private void updateSoptampUserNormal(SoptampUser registeredUser, PlatformUserInfoResponse profile, PlatformUserInfoResponse.SoptActivities latest){ + private void updateSoptampUserNormal(SoptampUser registeredUser, PlatformUserInfoResponse profile, + PlatformUserInfoResponse.SoptActivities latest) { Long userId = registeredUser.getUserId(); String part = latest.part() == null ? "미상" : latest.part(); String newNickname = generatePartBasedUniqueNickname(profile.name(), part, userId); registeredUser.initTotalPoints(); registeredUser.updateChangedGenerationInfo( - (long)profile.lastGeneration(), - findSoptPartByPartName(part), + (long) profile.lastGeneration(), + findSoptPartByPartName(part), newNickname ); rankCacheService.removeRank(userId); rankCacheService.createNewRank(userId); } - private void createSoptampUserNormal(PlatformUserInfoResponse profile, Long userId, PlatformUserInfoResponse.SoptActivities latest) { - String part = latest.part() == null ? "미상" : latest.part(); + private void createSoptampUserNormal(PlatformUserInfoResponse profile, Long userId, + PlatformUserInfoResponse.SoptActivities latestSopt + ) { + String part = latestSopt.part() == null ? "미상" : latestSopt.part(); String uniqueNickname = generatePartBasedUniqueNickname(profile.name(), part, null); - SoptampUser newSoptampUser = createNewSoptampUser(userId, uniqueNickname, (long)profile.lastGeneration(), findSoptPartByPartName(part)); + SoptampUser newSoptampUser = createNewSoptampUser(userId, uniqueNickname, (long) profile.lastGeneration(), + findSoptPartByPartName(part)); soptampUserRepository.save(newSoptampUser); rankCacheService.createNewRank(userId); } @@ -115,8 +124,8 @@ private boolean isGenerationChanged(SoptampUser registeredUser, Long profileGene // ==================== 앱잼 시즌용 upsert ==================== private void upsertSoptampUserForAppjam(PlatformUserInfoResponse profile, - Long userId, - PlatformUserInfoResponse.SoptActivities latest) { + Long userId, + PlatformUserInfoResponse.SoptActivities latest) { Optional userOpt = soptampUserRepository.findByUserId(userId); if (userOpt.isEmpty()) { @@ -137,37 +146,36 @@ private void upsertSoptampUserForAppjam(PlatformUserInfoResponse profile, String uniqueNickname = generateUniqueNicknameInternal(baseNickname, userId); String part = latest.part() == null ? "미상" : latest.part(); - + // 앱잼 시즌: 파트, Makers 무관 buildAppjamBaseNickname이 자연스럽게 처리 registeredUser.updateChangedGenerationInfo( - (long) profile.lastGeneration(), - findSoptPartByPartName(part), - uniqueNickname - ); + (long) profile.lastGeneration(), + findSoptPartByPartName(part), + uniqueNickname + ); // 앱잼 변환 시점에 한 번 포인트 초기화 registeredUser.initTotalPoints(); } private void createSoptampUserAppjam(PlatformUserInfoResponse profile, - Long userId, - PlatformUserInfoResponse.SoptActivities latest) { + Long userId, + PlatformUserInfoResponse.SoptActivities latest) { String baseNickname = buildAppjamBaseNickname(profile, userId); // 새 유저: 전체에서 중복 검사 String uniqueNickname = generateUniqueNicknameInternal( - baseNickname, - null - ); + baseNickname, + null + ); String part = latest.part() == null ? "미상" : latest.part(); SoptampUser newSoptampUser = createNewSoptampUser( - userId, - uniqueNickname, - (long) profile.lastGeneration(), - findSoptPartByPartName(part) - ); + userId, + uniqueNickname, + (long) profile.lastGeneration(), + findSoptPartByPartName(part)); newSoptampUser.initTotalPoints(); // 새 시즌이니 0점부터 soptampUserRepository.save(newSoptampUser); @@ -180,8 +188,10 @@ private boolean needsAppjamNicknameMigration(SoptampUser user) { return true; } - // SoptPart 기준으로 "서버", "기획" 같은 축약/프리픽스를 모두 검사 + // SoptPart 기준으로 "서버", "기획" 같은 축약/프리픽스를 모두 검사 (SOPT 파트만) for (SoptPart part : SoptPart.values()) { + if (!part.isSoptPart()) + continue; String prefix = part.getShortedPartName(); if (nickname.startsWith(prefix)) { // 서버김솝트, 디자인김솝트 등 → 기존 시즌(파트 기반) 닉네임이므로 앱잼 변환 필요 @@ -200,8 +210,8 @@ private boolean needsAppjamNicknameMigration(SoptampUser user) { */ private String buildAppjamBaseNickname(PlatformUserInfoResponse profile, Long userId) { return appjamUserRepository.findByUserId(userId) - .map(appjamUser -> appjamUser.getTeamName() + profile.name()) - .orElseGet(() -> profile.lastGeneration() + "기" + profile.name()); + .map(appjamUser -> appjamUser.getTeamName() + profile.name()) + .orElseGet(() -> profile.lastGeneration() + "기" + profile.name()); } // ==================== 닉네임 유니크 로직 공통부 ==================== @@ -218,7 +228,7 @@ private String generatePartBasedUniqueNickname(String name, String part, Long cu /** * baseNickname을 기준으로, 전역 유니크 닉네임 생성 - * - currentUserIdOrNull == null : 새 유저 생성 (그냥 existsByNickname) + * - currentUserIdOrNull == null : 새 유저 생성 (그냥 existsByNickname) * - currentUserIdOrNull != null : 내 row는 제외하고 중복 체크 */ private String generateUniqueNicknameInternal(String baseNickname, Long currentUserIdOrNull) { diff --git a/src/main/java/org/sopt/app/domain/enums/SoptPart.java b/src/main/java/org/sopt/app/domain/enums/SoptPart.java index b49026a1..e0d8f0c7 100644 --- a/src/main/java/org/sopt/app/domain/enums/SoptPart.java +++ b/src/main/java/org/sopt/app/domain/enums/SoptPart.java @@ -1,6 +1,8 @@ package org.sopt.app.domain.enums; import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; import lombok.AllArgsConstructor; import lombok.Getter; @@ -29,7 +31,19 @@ public enum SoptPart { // 파트장이 솝탬프 파트별 랭킹에 관여할 수 있으려면 각 파트의 shortedPartName이 접두사로 필요하다 NONE("미상", "선배"), + + /** + * Sopt Makers Chapter + */ + PM("PM", "PM"), + FRONTEND("프론트엔드", "FE"), + BACKEND("백엔드", "BE"), + MARKETER("마케터", "마케터"), + RESEARCHER("리서처", "리서처"), + ORGANIZER("오거나이저", "오거나이저"), + CX("CX", "CX"), ; + final String partName; final String shortedPartName; @@ -40,6 +54,18 @@ public static SoptPart findSoptPartByPartName(String partName) { .orElse(SoptPart.NONE); } + private static final Set SOPT_PARTS = EnumSet.of( + PLAN, PLAN_PART_LEADER, + DESIGN, DESIGN_PART_LEADER, + ANDROID, ANDROID_PART_LEADER, + IOS, IOS_PART_LEADER, + WEB, WEB_PART_LEADER, + SERVER, SERVER_PART_LEADER); + + public boolean isSoptPart() { + return SOPT_PARTS.contains(this); + } + public static Part toPart(SoptPart soptPart) { return switch (soptPart) { case PLAN, PLAN_PART_LEADER -> Part.PLAN; diff --git a/src/main/java/org/sopt/app/facade/HomeFacade.java b/src/main/java/org/sopt/app/facade/HomeFacade.java index ff16fcbe..672e5342 100755 --- a/src/main/java/org/sopt/app/facade/HomeFacade.java +++ b/src/main/java/org/sopt/app/facade/HomeFacade.java @@ -68,10 +68,9 @@ public List checkAppServiceEntryStatus(Long userI if(userId == null){ return this.getOnlyAppServiceInfo(); } - UserStatus status = platformService.getStatus(userId); - // TODO : 추후 유저 생성 api response 변경해 생성 api 쪽에서 soptamp user upsert 하도록 변경 PlatformUserInfoResponse platformUserInfo = platformService.getPlatformUserInfoResponse(userId); + UserStatus status = platformService.getStatus(platformUserInfo); List appServiceEntryStatusResponses = appServiceService.getAllAppService().stream() .filter(appServiceInfo -> isServiceVisibleToUser(appServiceInfo, status)) diff --git a/src/test/java/org/sopt/app/application/SoptampUserServiceTest.java b/src/test/java/org/sopt/app/application/SoptampUserServiceTest.java index 2fe12b36..f1bab963 100755 --- a/src/test/java/org/sopt/app/application/SoptampUserServiceTest.java +++ b/src/test/java/org/sopt/app/application/SoptampUserServiceTest.java @@ -61,23 +61,28 @@ class SoptampUserServiceTest { @InjectMocks SoptampUserService soptampUserService; - private PlatformUserInfoResponse buildProfile(String name, int lastGeneration, String part) { - PlatformUserInfoResponse.SoptActivities latest = - new PlatformUserInfoResponse.SoptActivities( - 1, // activityId - lastGeneration, // generation - part, // part - "아무팀" // team (여기선 안 씀) - ); - - return new PlatformUserInfoResponse( - 1, // userId - name, - null, null, null, null, - lastGeneration, - List.of(latest) - ); - } + private PlatformUserInfoResponse buildProfile(String name, int lastGeneration, String part) { + return buildProfile(name, lastGeneration, part, true); // SOPT 정규 활동 기본값 + } + + private PlatformUserInfoResponse buildProfile(String name, int lastGeneration, String part, boolean isSopt) { + PlatformUserInfoResponse.SoptActivities latest = + new PlatformUserInfoResponse.SoptActivities( + 1, // activityId + lastGeneration, // generation + part, // part + "아무팀", // team (여기선 안 씀) + isSopt + ); + + return new PlatformUserInfoResponse( + 1, // userId + name, + null, null, null, null, + lastGeneration, + List.of(latest) + ); + } @BeforeEach void setUp() { @@ -222,7 +227,54 @@ void setUp() { verify(rankCacheService).createNewRank(userId); } - /* ==================== APPJAM 모드 테스트 ==================== */ + @Test + @DisplayName("NORMAL 모드 - isSopt=false인 Makers 활동만 있으면 SoptampUser를 생성하지 않는다") + void 일반모드_Makers이면_솝탬프유저_생성안함() { + // given + long userId = 1L; + // "백엔드" 파트, isSopt=false → getLatestSoptActivity() = null + PlatformUserInfoResponse profile = buildProfile("김솝트", 37, "백엔드", false); + + // when + soptampUserService.upsertSoptampUser(profile, userId); + + // then + verifyNoInteractions(soptampUserRepository, appjamUserRepository, rankCacheService); + } + + @Test + @DisplayName("NORMAL 모드 - 메이커스(isSopt=false)이면서 동시에 SOPT 서버 파트(isSopt=true) 활동이 있으면 SOPT 활동 기준으로 생성된다") + void 일반모드_메이커스이면서_솝트파트있으면_솝트파트기준으로_생성() { + // given + long userId = 1L; + PlatformUserInfoResponse.SoptActivities presidentActivity = + new PlatformUserInfoResponse.SoptActivities(1, 37, "BE", null, false); + PlatformUserInfoResponse.SoptActivities serverActivity = + new PlatformUserInfoResponse.SoptActivities(2, 37, "서버", null, true); + + PlatformUserInfoResponse profile = new PlatformUserInfoResponse( + 1, "김솝트", null, null, null, null, 37, + List.of(presidentActivity, serverActivity) + ); + + when(soptampUserRepository.findByUserId(userId)).thenReturn(Optional.empty()); + when(soptampUserRepository.existsByNickname(anyString())).thenReturn(false); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SoptampUser.class); + + // when + soptampUserService.upsertSoptampUser(profile, userId); + + // then + verify(soptampUserRepository).save(captor.capture()); + SoptampUser saved = captor.getValue(); + assertThat(saved.getNickname()) + .startsWith(SoptPart.findSoptPartByPartName("서버").getShortedPartName()); + assertThat(saved.getNickname()).contains("김솝트"); + verify(rankCacheService).createNewRank(userId); + } + + /* ==================== APPJAM 모드 테스트 ==================== */ @Test @DisplayName("APPJAM 모드 - SoptampUser가 없고 AppjamUser가 있으면 팀명+이름으로 앱잼 유저 생성") @@ -289,13 +341,42 @@ void setUp() { assertThat(saved.getNickname()).contains("김솝트"); assertThat(saved.getTotalPoints()).isZero(); - // 앱잼 시즌: 개인 랭킹 캐시 사용 안 함 - verifyNoInteractions(rankCacheService); - } + // 앱잼 시즌: 개인 랭킹 캐시 사용 안 함 + verifyNoInteractions(rankCacheService); + } - @Test - @DisplayName("APPJAM 모드 - 기존 닉네임이 파트 기반이면 앱잼 닉네임으로 1회 마이그레이션 후 포인트 초기화") - void 앱잼모드_파트닉네임이면_앱잼닉네임으로변환_포인트초기화() { + @Test + @DisplayName("APPJAM 모드 - 임원진(isSopt=false)이면서 AppjamUser 없으면 기수+기+이름으로 생성된다") + void 앱잼모드_임원진이면서_AppjamUser없으면_기수닉네임으로생성() { + // given + ReflectionTestUtils.setField(soptampUserService, "appjamMode", true); + + long userId = 1L; + // 앱잼 시즌에는 getLatestActivity()를 쓰므로 isSopt 무관 — 임원진도 포함 + PlatformUserInfoResponse profile = buildProfile("김솝트", 37, "회장", false); + + when(soptampUserRepository.findByUserId(userId)).thenReturn(Optional.empty()); + when(appjamUserRepository.findByUserId(userId)).thenReturn(Optional.empty()); + when(soptampUserRepository.existsByNickname(anyString())).thenReturn(false); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SoptampUser.class); + + // when + soptampUserService.upsertSoptampUser(profile, userId); + + // then: 임원진도 앱잼 시즌에는 "37기김솝트" 형식으로 구경용 유저 생성 + verify(soptampUserRepository).save(captor.capture()); + SoptampUser saved = captor.getValue(); + assertThat(saved.getNickname()).startsWith("37기"); + assertThat(saved.getNickname()).contains("김솝트"); + assertThat(saved.getTotalPoints()).isZero(); + + verifyNoInteractions(rankCacheService); + } + + @Test + @DisplayName("APPJAM 모드 - 기존 닉네임이 파트 기반이면 앱잼 닉네임으로 1회 마이그레이션 후 포인트 초기화") + void 앱잼모드_파트닉네임이면_앱잼닉네임으로변환_포인트초기화() { // given ReflectionTestUtils.setField(soptampUserService, "appjamMode", true); diff --git a/src/test/java/org/sopt/app/common/fixtures/SoptampUserFixture.java b/src/test/java/org/sopt/app/common/fixtures/SoptampUserFixture.java index 2cab5c68..420accf2 100755 --- a/src/test/java/org/sopt/app/common/fixtures/SoptampUserFixture.java +++ b/src/test/java/org/sopt/app/common/fixtures/SoptampUserFixture.java @@ -127,7 +127,16 @@ public static SoptActivities getSoptActivities( int generation, String part ) { - return new SoptActivities(ID_GENERATOR.getAndIncrement(), generation, part, null); + // SOPT 정규 활동(isSopt=true) 기본 헬퍼 + return new SoptActivities(ID_GENERATOR.getAndIncrement(), generation, part, null, true); + } + + public static SoptActivities getSoptActivities( + int generation, + String part, + boolean isSopt + ) { + return new SoptActivities(ID_GENERATOR.getAndIncrement(), generation, part, null, isSopt); } public static PlatformUserInfoResponse getPlatformUserInfoResponse(