diff --git a/src/main/java/org/sopt/makers/internal/member/constants/AskMemberId.java b/src/main/java/org/sopt/makers/internal/member/constants/AskMemberId.java new file mode 100644 index 00000000..8cdaced2 --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/member/constants/AskMemberId.java @@ -0,0 +1,28 @@ +package org.sopt.makers.internal.member.constants; + +import java.util.List; +import java.util.Map; +import lombok.Getter; +import org.sopt.makers.internal.member.domain.enums.Part; + +public class AskMemberId { + @Getter + private static final Map> askMembersByPart = Map.of( + Part.SERVER, List.of(929L,209L), + Part.IOS, List.of(192L), + Part.ANDROID, List.of(223L), + Part.WEB, List.of(945L), + Part.DESIGN, List.of(930L), + Part.PLAN, List.of(229L) + ); + + public static List getAskMembersByPart(Part part) { + return askMembersByPart.getOrDefault(part, List.of()); + } + + public static List getAllAskMembers() { + return askMembersByPart.values().stream() + .flatMap(List::stream) + .toList(); + } +} diff --git a/src/main/java/org/sopt/makers/internal/member/controller/MemberController.java b/src/main/java/org/sopt/makers/internal/member/controller/MemberController.java index 90ef2673..c3f9834d 100644 --- a/src/main/java/org/sopt/makers/internal/member/controller/MemberController.java +++ b/src/main/java/org/sopt/makers/internal/member/controller/MemberController.java @@ -32,6 +32,7 @@ import org.sopt.makers.internal.member.dto.response.MemberProfileSpecificResponse; import org.sopt.makers.internal.member.dto.response.MemberPropertiesResponse; import org.sopt.makers.internal.member.dto.response.MemberResponse; +import org.sopt.makers.internal.member.dto.response.AskMemberResponse; import org.sopt.makers.internal.member.dto.response.WorkPreferenceRecommendationResponse; import org.sopt.makers.internal.member.dto.response.WorkPreferenceResponse; import org.sopt.makers.internal.member.dto.response.TlMemberResponse; @@ -102,6 +103,20 @@ public ResponseEntity> getTlMembers( return ResponseEntity.status(HttpStatus.OK).body(responses); } + @Operation(summary = "질문 대상 멤버 조회 API", description = """ + 질문을 받을 수 있는 대상 멤버들을 파트별로 조회합니다. + part 파라미터가 없으면 모든 파트의 멤버를 반환합니다. + part 파라미터 옵션: 서버, SERVER, iOS, 안드로이드, ANDROID, 웹, WEB, 디자인, DESIGN, 기획, PLAN + 각 파트별로 하드코딩된 멤버를 반환합니다. + """) + @GetMapping("/ask/list") + public ResponseEntity getAskMembers( + @RequestParam(required = false, name = "part") String part + ) { + AskMemberResponse response = memberService.getAskMembers(part); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + // 프론트 연결 되면 삭제 예정 @Deprecated diff --git a/src/main/java/org/sopt/makers/internal/member/domain/WorkPreference.java b/src/main/java/org/sopt/makers/internal/member/domain/WorkPreference.java index c1ed1e9b..33d02bff 100644 --- a/src/main/java/org/sopt/makers/internal/member/domain/WorkPreference.java +++ b/src/main/java/org/sopt/makers/internal/member/domain/WorkPreference.java @@ -15,7 +15,7 @@ import java.io.Serializable; @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder public class WorkPreference implements Serializable { diff --git a/src/main/java/org/sopt/makers/internal/member/dto/response/AskMemberResponse.java b/src/main/java/org/sopt/makers/internal/member/dto/response/AskMemberResponse.java new file mode 100644 index 00000000..bb0cde5b --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/member/dto/response/AskMemberResponse.java @@ -0,0 +1,49 @@ +package org.sopt.makers.internal.member.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema(description = "질문 대상 멤버 응답 DTO") +public record +AskMemberResponse( + @Schema(description = "질문 대상 멤버 목록") + List members +) { + @Schema(description = "질문 대상 멤버 정보") + public record QuestionTargetMember( + @Schema(description = "멤버 ID", required = true) + Long id, + + @Schema(description = "멤버 이름", required = true) + String name, + + @Schema(description = "프로필 이미지 URL") + String profileImage, + + @Schema(description = "소개") + String introduction, + + @Schema(description = "최근 활동 정보", required = true) + MemberSoptActivityResponse latestActivity, + + @Schema(description = "현재 커리어") + MemberCareerResponse currentCareer, + + @Schema(description = "직전 커리어") + MemberCareerResponse previousCareer, + + @Schema(description = "답변 보장 여부", example = "true", required = true) + Boolean isAnswerGuaranteed + ) {} + + public record MemberSoptActivityResponse( + Integer generation, + String part, + String team + ) {} + + public record MemberCareerResponse( + String companyName, + String title + ) {} +} diff --git a/src/main/java/org/sopt/makers/internal/member/service/MemberService.java b/src/main/java/org/sopt/makers/internal/member/service/MemberService.java index d5aa07f4..021ebbc7 100644 --- a/src/main/java/org/sopt/makers/internal/member/service/MemberService.java +++ b/src/main/java/org/sopt/makers/internal/member/service/MemberService.java @@ -29,6 +29,7 @@ import org.sopt.makers.internal.external.slack.SlackClient; import org.sopt.makers.internal.external.slack.SlackMessageUtil; import org.sopt.makers.internal.member.constants.AppJamObMemberId; +import org.sopt.makers.internal.member.constants.AskMemberId; import org.sopt.makers.internal.member.constants.MakersMemberId; import org.sopt.makers.internal.member.domain.Member; import org.sopt.makers.internal.member.domain.MemberBlock; @@ -40,6 +41,7 @@ import org.sopt.makers.internal.member.domain.WorkPreference; import org.sopt.makers.internal.member.domain.enums.ActivityTeam; import org.sopt.makers.internal.member.domain.enums.OrderByCondition; +import org.sopt.makers.internal.member.domain.enums.Part; import org.sopt.makers.internal.member.domain.enums.ServiceType; import org.sopt.makers.internal.member.dto.ActivityVo; import org.sopt.makers.internal.member.dto.MemberProfileProjectDao; @@ -58,6 +60,7 @@ import org.sopt.makers.internal.member.dto.response.MemberPropertiesResponse; import org.sopt.makers.internal.member.dto.response.MemberResponse; import org.sopt.makers.internal.member.dto.response.MemberSoptActivityResponse; +import org.sopt.makers.internal.member.dto.response.AskMemberResponse; import org.sopt.makers.internal.member.dto.response.WorkPreferenceRecommendationResponse; import org.sopt.makers.internal.member.dto.response.WorkPreferenceResponse; import org.sopt.makers.internal.member.dto.response.TlMemberResponse; @@ -1048,4 +1051,128 @@ private TlMemberResponse buildTlMemberResponse( ); } + @Transactional(readOnly = true) + public AskMemberResponse getAskMembers(String partName) { + List targetMembers = new ArrayList<>(); + + Part part = convertToPart(partName); + + // 특정 파트가 지정된 경우 해당 파트만, 없으면 모든 파트 + List memberIds; + if (part != null) { + memberIds = AskMemberId.getAskMembersByPart(part); + } else { + memberIds = AskMemberId.getAllAskMembers(); + } + + for (Long memberId : memberIds) { + try { + Member member = memberRepository.findById(memberId).orElse(null); + if (member == null || !member.getHasProfile()) { + continue; + } + + InternalUserDetails userDetails = platformService.getInternalUser(memberId); + + // 최근 활동 정보 가져오기 + SoptActivity latestActivity = userDetails.soptActivities().stream() + .max((a1, a2) -> Integer.compare(a1.generation(), a2.generation())) + .orElse(null); + + if (latestActivity == null) { + continue; + } + + // 커리어 정보 처리 + AskMemberResponse.MemberCareerResponse currentCareer = null; + AskMemberResponse.MemberCareerResponse previousCareer = null; + + List careers = member.getCareers(); + if (careers != null && !careers.isEmpty()) { + // 현재 재직중인 커리어 찾기 + Optional currentCareerOpt = careers.stream() + .filter(career -> career.getIsCurrent() != null && career.getIsCurrent()) + .findFirst(); + + if (currentCareerOpt.isPresent()) { + MemberCareer current = currentCareerOpt.get(); + currentCareer = new AskMemberResponse.MemberCareerResponse( + current.getCompanyName(), + current.getTitle() + ); + } + + // 직전 커리어 찾기 (isCurrent가 false인 것 중 가장 최근) + List pastCareers = careers.stream() + .filter(career -> career.getIsCurrent() == null || !career.getIsCurrent()) + .filter(career -> career.getEndDate() != null) + .toList(); + + if (!pastCareers.isEmpty()) { + // endDate로 정렬하여 가장 최근 것 선택 + MemberCareer mostRecentPast = pastCareers.stream() + .max((c1, c2) -> { + try { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + val end1 = YearMonth.parse(c1.getEndDate(), formatter); + val end2 = YearMonth.parse(c2.getEndDate(), formatter); + return end1.compareTo(end2); + } catch (Exception e) { + return 0; + } + }) + .orElse(null); + + previousCareer = new AskMemberResponse.MemberCareerResponse( + mostRecentPast.getCompanyName(), + mostRecentPast.getTitle() + ); + } + } + + AskMemberResponse.MemberSoptActivityResponse activityResponse = + new AskMemberResponse.MemberSoptActivityResponse( + latestActivity.generation(), + latestActivity.part(), + latestActivity.team() + ); + + AskMemberResponse.QuestionTargetMember targetMember = + new AskMemberResponse.QuestionTargetMember( + memberId, + userDetails.name(), + userDetails.profileImage(), + member.getIntroduction(), + activityResponse, + currentCareer, + previousCareer, + true // 답변보장 항상 true + ); + + targetMembers.add(targetMember); + } catch (Exception e) { + log.error("Failed to process member ID: " + memberId, e); + continue; + } + } + + return new AskMemberResponse(targetMembers); + } + + private Part convertToPart(String partName) { + if (partName == null || partName.isBlank()) { + return null; + } + + return switch (partName.toUpperCase()) { + case "서버", "SERVER" -> Part.SERVER; + case "IOS" -> Part.IOS; + case "안드로이드", "ANDROID" -> Part.ANDROID; + case "웹", "WEB" -> Part.WEB; + case "디자인", "DESIGN" -> Part.DESIGN; + case "기획", "PLAN" -> Part.PLAN; + default -> null; + }; + } + }