Skip to content

Commit 23e805f

Browse files
authored
Merge pull request #331 from prgrms-web-devcourse-final-project/develop
배포
2 parents deb4833 + c74b530 commit 23e805f

File tree

6 files changed

+248
-16
lines changed

6 files changed

+248
-16
lines changed

backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepositoryImpl.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
import org.springframework.stereotype.Repository;
1313
import com.querydsl.core.Tuple;
1414

15+
import java.util.ArrayList;
16+
import java.util.HashMap;
1517
import java.util.List;
18+
import java.util.Map;
1619

1720
import com.ai.lawyer.domain.poll.dto.PollAgeStaticsDto.AgeGroupCountDto;
1821
import com.ai.lawyer.domain.poll.dto.PollGenderStaticsDto.GenderCountDto;
@@ -95,20 +98,25 @@ public List<PollStaticsDto> countStaticsByPollOptionIds(List<Long> pollOptionIds
9598
.where(pollOptions.getPollItemsId().in(pollOptionIds))
9699
.groupBy(pollOptions.getPollItemsId(), member.getGender(), member.getAge())
97100
.fetch();
98-
return tuples.stream()
99-
.map(t -> {
100-
Member.Gender genderEnum = t.get(1, Member.Gender.class);
101-
String gender = genderEnum != null ? genderEnum.name() : "기타";
102-
Integer age = t.get(2, Integer.class);
103-
String ageGroup = getAgeGroup(age);
104-
Long voteCount = t.get(3, Long.class);
105-
return PollStaticsDto.builder()
106-
.gender(gender)
107-
.ageGroup(ageGroup)
108-
.voteCount(voteCount)
109-
.build();
110-
})
111-
.toList();
101+
102+
// gender와 ageGroup별로 voteCount 합산
103+
Map<String, Integer> staticsMap = new HashMap<>();
104+
for (Tuple t : tuples) {
105+
Member.Gender genderEnum = t.get(1, Member.Gender.class);
106+
String gender = genderEnum != null ? genderEnum.name() : "기타";
107+
Integer age = t.get(2, Integer.class);
108+
String ageGroup = getAgeGroup(age);
109+
Long voteCount = t.get(3, Long.class);
110+
String key = gender + "_" + ageGroup;
111+
staticsMap.put(key, staticsMap.getOrDefault(key, 0) + voteCount.intValue());
112+
}
113+
114+
List<PollStaticsDto> result = new ArrayList<>();
115+
for (Map.Entry<String, Integer> entry : staticsMap.entrySet()) {
116+
String[] key = entry.getKey().split("_");
117+
result.add(new PollStaticsDto(key[0], key[1], entry.getValue().longValue()));
118+
}
119+
return result;
112120
}
113121

114122
private String getAgeGroup(Integer age) {

backend/src/main/java/com/ai/lawyer/global/jwt/JwtAuthenticationFilter.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ private void setAuthentication(String token) {
162162
Long memberId = tokenProvider.getMemberIdFromToken(token);
163163
String loginId = tokenProvider.getLoginIdFromToken(token);
164164
String role = tokenProvider.getRoleFromToken(token);
165+
String loginType = tokenProvider.getLoginTypeFromToken(token);
165166

166167
if (memberId == null) {
167168
log.warn(LOG_MEMBER_ID_EXTRACTION_FAILED);
@@ -174,7 +175,7 @@ private void setAuthentication(String token) {
174175

175176
// memberId를 principal로 하는 인증 객체 생성
176177
// getName()은 memberId를 반환 (PollController 호환)
177-
// getDetails()는 loginId를 반환 (MemberController 호환)
178+
// getDetails()는 loginId와 loginType을 포함한 맵을 반환
178179
UsernamePasswordAuthenticationToken authentication =
179180
new UsernamePasswordAuthenticationToken(memberId, null, authorities) {
180181
@Override
@@ -184,11 +185,15 @@ public String getName() {
184185

185186
@Override
186187
public Object getDetails() {
187-
return loginId;
188+
return java.util.Map.of(
189+
"loginId", loginId != null ? loginId : "",
190+
"loginType", loginType != null ? loginType : "LOCAL"
191+
);
188192
}
189193
};
190194

191195
SecurityContextHolder.getContext().setAuthentication(authentication);
196+
log.debug("JWT 인증 설정 완료: memberId={}, loginId={}, loginType={}", memberId, loginId, loginType);
192197
} catch (Exception e) {
193198
log.warn(LOG_SET_AUTH_FAILED, e.getMessage());
194199
}

backend/src/main/java/com/ai/lawyer/global/jwt/TokenProvider.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class TokenProvider {
3737
private static final String CLAIM_LOGIN_ID = "loginId";
3838
private static final String CLAIM_MEMBER_ID = "memberId";
3939
private static final String CLAIM_ROLE = "role";
40+
private static final String CLAIM_LOGIN_TYPE = "loginType";
4041

4142
// 로그 메시지 상수
4243
private static final String LOG_ACCESS_TOKEN_SAVED = "=== Access token Hash 저장 성공: key={}, expiry={} ===";
@@ -60,12 +61,16 @@ public String generateAccessToken(com.ai.lawyer.domain.member.entity.MemberAdapt
6061
Date now = new Date();
6162
Date expiry = new Date(now.getTime() + jwtProperties.getAccessToken().getExpirationSeconds() * MILLIS_PER_SECOND);
6263

64+
// 로그인 타입 결정 (OAuth2Member인지 Member인지 확인)
65+
String loginType = (member instanceof com.ai.lawyer.domain.member.entity.OAuth2Member) ? "OAUTH2" : "LOCAL";
66+
6367
String accessToken = Jwts.builder()
6468
.setIssuedAt(now)
6569
.setExpiration(expiry)
6670
.claim(CLAIM_LOGIN_ID, member.getLoginId())
6771
.claim(CLAIM_MEMBER_ID, member.getMemberId())
6872
.claim(CLAIM_ROLE, member.getRole().name())
73+
.claim(CLAIM_LOGIN_TYPE, loginType)
6974
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
7075
.compact();
7176

@@ -176,6 +181,10 @@ public String getLoginIdFromToken(String token) {
176181
return getClaimFromToken(token, CLAIM_LOGIN_ID, String.class, LOG_LOGIN_ID_EXTRACTION_FAILED);
177182
}
178183

184+
public String getLoginTypeFromToken(String token) {
185+
return getClaimFromToken(token, CLAIM_LOGIN_TYPE, String.class, "토큰에서 로그인 타입 추출 실패: {}");
186+
}
187+
179188
/**
180189
* 토큰에서 특정 Claim을 추출하는 공통 메서드
181190
*/

backend/src/main/java/com/ai/lawyer/global/util/AuthUtil.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,46 @@ public static Member getMemberOrThrow(Long memberId) {
104104
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다");
105105
}
106106

107+
/**
108+
* memberId와 loginType으로 회원을 조회합니다.
109+
* loginType이 "LOCAL"이면 Member 테이블에서, "OAUTH2"이면 OAuth2Member 테이블에서 조회합니다.
110+
* @param memberId 회원 ID
111+
* @param loginType 로그인 타입 ("LOCAL" 또는 "OAUTH2")
112+
* @return Member 객체
113+
* @throws ResponseStatusException 회원을 찾을 수 없는 경우
114+
*/
115+
public static Member getMemberOrThrow(Long memberId, String loginType) {
116+
if ("OAUTH2".equals(loginType)) {
117+
// OAuth2 회원 조회
118+
if (oauth2MemberRepository != null) {
119+
java.util.Optional<com.ai.lawyer.domain.member.entity.OAuth2Member> oauth2Member =
120+
oauth2MemberRepository.findById(memberId);
121+
if (oauth2Member.isPresent()) {
122+
// OAuth2Member를 Member로 변환
123+
com.ai.lawyer.domain.member.entity.OAuth2Member oauth = oauth2Member.get();
124+
return Member.builder()
125+
.memberId(oauth.getMemberId())
126+
.loginId(oauth.getLoginId())
127+
.name(oauth.getName())
128+
.age(oauth.getAge())
129+
.gender(oauth.getGender())
130+
.role(oauth.getRole())
131+
.password("") // OAuth2는 비밀번호 없음
132+
.build();
133+
}
134+
}
135+
} else {
136+
// LOCAL 회원 조회 (기본값)
137+
java.util.Optional<Member> member = memberRepository.findById(memberId);
138+
if (member.isPresent()) {
139+
return member.get();
140+
}
141+
}
142+
143+
// 찾지 못한 경우 예외 발생
144+
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "회원 정보를 찾을 수 없습니다");
145+
}
146+
107147
public static Long getAuthenticatedMemberId() {
108148
try {
109149
Long memberId = getCurrentMemberId();

backend/src/test/java/com/ai/lawyer/global/jwt/TokenProviderTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ void generateAccessToken_Success() {
118118
assertThat(claims.get("loginId", String.class)).as("loginId claim 일치").isEqualTo("[email protected]");
119119
assertThat(claims.get("memberId", Long.class)).as("memberId claim 일치").isEqualTo(1L);
120120
assertThat(claims.get("role", String.class)).as("role claim 일치").isEqualTo("USER");
121+
assertThat(claims.get("loginType", String.class)).as("loginType claim 일치").isEqualTo("LOCAL");
121122
assertThat(claims.getIssuedAt()).as("발급 시간 존재").isNotNull();
122123
assertThat(claims.getExpiration()).as("만료 시간 존재").isNotNull();
123124
assertThat(claims.getExpiration()).as("만료 시간이 발급 시간 이후").isAfter(claims.getIssuedAt());
@@ -571,6 +572,80 @@ void deleteAllTokens_Success() {
571572
log.info("=== 모든 토큰 삭제 테스트 완료 ===");
572573
}
573574

575+
@Test
576+
@DisplayName("토큰에서 loginType 추출 성공 - LOCAL")
577+
void getLoginTypeFromToken_Success_Local() {
578+
// given
579+
log.info("=== 토큰에서 loginType 추출 테스트 시작 (LOCAL) ===");
580+
willDoNothing().given(hashOperations).put(anyString(), anyString(), anyString());
581+
given(redisTemplate.expire(anyString(), any(Duration.class))).willReturn(true);
582+
583+
String token = tokenProvider.generateAccessToken(member);
584+
log.info("토큰 생성 완료");
585+
586+
// when
587+
log.info("loginType 추출 호출 중...");
588+
String loginType = tokenProvider.getLoginTypeFromToken(token);
589+
log.info("loginType 추출 완료: {}", loginType);
590+
591+
// then
592+
assertThat(loginType).as("loginType이 null이 아님").isNotNull();
593+
assertThat(loginType).as("loginType 일치").isEqualTo("LOCAL");
594+
log.info("=== 토큰에서 loginType 추출 테스트 완료 (LOCAL) ===");
595+
}
596+
597+
@Test
598+
@DisplayName("토큰에서 loginType 추출 성공 - OAUTH2")
599+
void getLoginTypeFromToken_Success_OAuth2() {
600+
// given
601+
log.info("=== 토큰에서 loginType 추출 테스트 시작 (OAUTH2) ===");
602+
willDoNothing().given(hashOperations).put(anyString(), anyString(), anyString());
603+
given(redisTemplate.expire(anyString(), any(Duration.class))).willReturn(true);
604+
605+
com.ai.lawyer.domain.member.entity.OAuth2Member oauth2Member =
606+
com.ai.lawyer.domain.member.entity.OAuth2Member.builder()
607+
.memberId(2L)
608+
.loginId("[email protected]")
609+
610+
.name("OAuth User")
611+
.age(30)
612+
.gender(Member.Gender.MALE)
613+
.provider(com.ai.lawyer.domain.member.entity.OAuth2Member.Provider.KAKAO)
614+
.providerId("kakao123")
615+
.role(Member.Role.USER)
616+
.build();
617+
618+
String token = tokenProvider.generateAccessToken(oauth2Member);
619+
log.info("OAuth2 토큰 생성 완료");
620+
621+
// when
622+
log.info("loginType 추출 호출 중...");
623+
String loginType = tokenProvider.getLoginTypeFromToken(token);
624+
log.info("loginType 추출 완료: {}", loginType);
625+
626+
// then
627+
assertThat(loginType).as("loginType이 null이 아님").isNotNull();
628+
assertThat(loginType).as("loginType 일치").isEqualTo("OAUTH2");
629+
log.info("=== 토큰에서 loginType 추출 테스트 완료 (OAUTH2) ===");
630+
}
631+
632+
@Test
633+
@DisplayName("토큰에서 loginType 추출 실패 - 유효하지 않은 토큰")
634+
void getLoginTypeFromToken_Fail_InvalidToken() {
635+
// given
636+
log.info("=== 토큰에서 loginType 추출 실패 테스트 시작 ===");
637+
String invalidToken = "invalid.token.format";
638+
639+
// when
640+
log.info("loginType 추출 호출 중...");
641+
String loginType = tokenProvider.getLoginTypeFromToken(invalidToken);
642+
log.info("loginType 추출 결과: {}", loginType);
643+
644+
// then
645+
assertThat(loginType).as("유효하지 않은 토큰에서는 null 반환").isNull();
646+
log.info("=== 토큰에서 loginType 추출 실패 테스트 완료 ===");
647+
}
648+
574649
@Test
575650
@DisplayName("여러 사용자의 토큰 생성 및 검증 - 멀티 유저 시나리오")
576651
void multipleUsers_TokenGeneration() {

backend/src/test/java/com/ai/lawyer/global/util/AuthUtilTest.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,99 @@ void getMemberOrThrow_NullOAuth2Repository() {
170170
assertThat(result).isNotNull();
171171
assertThat(result.getLoginId()).isEqualTo("[email protected]");
172172
}
173+
174+
@Test
175+
@DisplayName("loginType으로 로컬 회원 조회 성공")
176+
void getMemberOrThrow_WithLoginType_Local_Success() {
177+
// given
178+
Long memberId = 1L;
179+
String loginType = "LOCAL";
180+
given(memberRepository.findById(memberId)).willReturn(Optional.of(localMember));
181+
182+
// when
183+
Member result = AuthUtil.getMemberOrThrow(memberId, loginType);
184+
185+
// then
186+
assertThat(result).isNotNull();
187+
assertThat(result.getMemberId()).isEqualTo(1L);
188+
assertThat(result.getLoginId()).isEqualTo("[email protected]");
189+
assertThat(result.getName()).isEqualTo("로컬사용자");
190+
191+
verify(memberRepository).findById(memberId);
192+
// OAuth2 repository는 호출되지 않음
193+
org.mockito.Mockito.verifyNoInteractions(oauth2MemberRepository);
194+
}
195+
196+
@Test
197+
@DisplayName("loginType으로 OAuth2 회원 조회 성공")
198+
void getMemberOrThrow_WithLoginType_OAuth2_Success() {
199+
// given
200+
Long memberId = 2L;
201+
String loginType = "OAUTH2";
202+
given(oauth2MemberRepository.findById(memberId)).willReturn(Optional.of(oauth2Member));
203+
204+
// when
205+
Member result = AuthUtil.getMemberOrThrow(memberId, loginType);
206+
207+
// then
208+
assertThat(result).isNotNull();
209+
assertThat(result.getMemberId()).isEqualTo(2L);
210+
assertThat(result.getLoginId()).isEqualTo("[email protected]");
211+
assertThat(result.getName()).isEqualTo("소셜사용자");
212+
assertThat(result.getAge()).isEqualTo(25);
213+
assertThat(result.getGender()).isEqualTo(Member.Gender.FEMALE);
214+
215+
verify(oauth2MemberRepository).findById(memberId);
216+
// Member repository는 호출되지 않음
217+
org.mockito.Mockito.verifyNoInteractions(memberRepository);
218+
}
219+
220+
@Test
221+
@DisplayName("loginType이 LOCAL이지만 회원을 찾을 수 없을 때 예외 발생")
222+
void getMemberOrThrow_WithLoginType_Local_NotFound() {
223+
// given
224+
Long memberId = 999L;
225+
String loginType = "LOCAL";
226+
given(memberRepository.findById(memberId)).willReturn(Optional.empty());
227+
228+
// when & then
229+
assertThatThrownBy(() -> AuthUtil.getMemberOrThrow(memberId, loginType))
230+
.isInstanceOf(ResponseStatusException.class)
231+
.hasMessageContaining("회원 정보를 찾을 수 없습니다");
232+
233+
verify(memberRepository).findById(memberId);
234+
}
235+
236+
@Test
237+
@DisplayName("loginType이 OAUTH2이지만 회원을 찾을 수 없을 때 예외 발생")
238+
void getMemberOrThrow_WithLoginType_OAuth2_NotFound() {
239+
// given
240+
Long memberId = 999L;
241+
String loginType = "OAUTH2";
242+
given(oauth2MemberRepository.findById(memberId)).willReturn(Optional.empty());
243+
244+
// when & then
245+
assertThatThrownBy(() -> AuthUtil.getMemberOrThrow(memberId, loginType))
246+
.isInstanceOf(ResponseStatusException.class)
247+
.hasMessageContaining("회원 정보를 찾을 수 없습니다");
248+
249+
verify(oauth2MemberRepository).findById(memberId);
250+
}
251+
252+
@Test
253+
@DisplayName("loginType이 null일 때는 기본값 LOCAL로 처리")
254+
void getMemberOrThrow_WithLoginType_Null_DefaultsToLocal() {
255+
// given
256+
Long memberId = 1L;
257+
String loginType = null;
258+
given(memberRepository.findById(memberId)).willReturn(Optional.of(localMember));
259+
260+
// when
261+
Member result = AuthUtil.getMemberOrThrow(memberId, loginType);
262+
263+
// then
264+
assertThat(result).isNotNull();
265+
assertThat(result.getLoginId()).isEqualTo("[email protected]");
266+
verify(memberRepository).findById(memberId);
267+
}
173268
}

0 commit comments

Comments
 (0)