Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
package org.sopt.app.application.platform;

import feign.HeaderMap;
import feign.Param;
import feign.Headers;
import feign.QueryMap;
import feign.RequestLine;
import org.sopt.app.application.platform.dto.PlatformUserInfoResponse;

import org.sopt.app.application.platform.dto.PlatformUserIdsRequest;
import org.sopt.app.application.platform.dto.PlatformUserInfoWrapper;
import org.springframework.cloud.openfeign.EnableFeignClients;

import java.util.Collection;
import java.util.List;
import java.util.Map;

@EnableFeignClients
public interface PlatformClient {

// 유저정보 GET (CSV 쿼리)
@RequestLine("GET /api/v1/users")
PlatformUserInfoWrapper getPlatformUserInfo(@HeaderMap final Map<String, String> headers,
@QueryMap Map<String, Collection<String>> queryMap);
@QueryMap Map<String, String> queryMap);

// 유저정보 조회 POST (JSON 바디)
@RequestLine("POST /api/v1/users")
@Headers("Content-Type: application/json")
PlatformUserInfoWrapper postPlatformUserInfo(@HeaderMap Map<String, String> headers,
PlatformUserIdsRequest body);
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
package org.sopt.app.application.platform;

import static org.sopt.app.application.playground.PlaygroundHeaderCreator.*;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.ExpiredJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;

import org.sopt.app.application.auth.dto.PlaygroundAuthTokenInfo;
import org.sopt.app.application.platform.dto.PlatformUserIdsRequest;
import org.sopt.app.application.platform.dto.PlatformUserInfoResponse;
import org.sopt.app.application.platform.dto.PlatformUserInfoWrapper;
import org.sopt.app.application.playground.dto.PlaygroundProfileInfo;
import org.sopt.app.common.exception.BadRequestException;
import org.sopt.app.common.exception.UnauthorizedException;
import org.sopt.app.common.response.ErrorCode;
import org.sopt.app.domain.enums.UserStatus;
import org.sopt.app.presentation.auth.AppAuthRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
Expand All @@ -42,18 +32,65 @@ public class PlatformService {
@Value("${sopt.current.generation}")
private Long currentGeneration;

// URL 길이 한도
private static final int URL_QUERY_LENGTH_THRESHOLD = 1200;

public PlatformUserInfoResponse getPlatformUserInfoResponse(Long userId) {
final Map<String, String> headers = createAuthorizationHeader();
final Map<String, Collection<String>> params = createQueryParams(Collections.singletonList(userId));
final Map<String, String> params = createQueryParams(Collections.singletonList(userId));
PlatformUserInfoWrapper platformUserInfoWrapper = platformClient.getPlatformUserInfo(headers, params);
return platformUserInfoWrapper.data().getFirst();
List<PlatformUserInfoResponse> data= platformUserInfoWrapper.data();
if (data == null || data.isEmpty()) {
throw new BadRequestException(ErrorCode.PLATFORM_USER_NOT_EXISTS);
}
return data.getFirst();
}

public List<PlatformUserInfoResponse> getPlatformUserInfosResponse(List<Long> userIds) {
final Map<String, String> headers = createAuthorizationHeader();
final Map<String, Collection<String>> params = createQueryParams(userIds);

// 중복 제거
userIds = userIds.stream().distinct().toList();
final Map<String, String> params = createQueryParams(userIds);

PlatformUserInfoWrapper platformUserInfoWrapper = platformClient.getPlatformUserInfo(headers, params);
return platformUserInfoWrapper.data();

List<PlatformUserInfoResponse> data= platformUserInfoWrapper.data();
if (data == null || data.isEmpty()) {
throw new BadRequestException(ErrorCode.PLATFORM_USER_NOT_EXISTS);
}
return data;
}

// 길이에 따라 GET/POST 자동 선택
public List<PlatformUserInfoResponse> getPlatformUserInfosResponseSmart(List<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
throw new BadRequestException(ErrorCode.INVALID_PARAMETER);
}
final Map<String, String> headers = createAuthorizationHeader();

// 중복 제거
userIds = userIds.stream().distinct().toList();

// CSV 만들고 길이 체크
final String csv = toCsv(userIds);
final boolean usePost = csv.length() > URL_QUERY_LENGTH_THRESHOLD;

PlatformUserInfoWrapper wrapper;
if (usePost) {
// POST with JSON body
wrapper = platformClient.postPlatformUserInfo(headers, new PlatformUserIdsRequest(userIds));
} else {
// GET with CSV query
final Map<String, String> params = Map.of("userIds", csv);
wrapper = platformClient.getPlatformUserInfo(headers, params);
}

List<PlatformUserInfoResponse> data = wrapper.data();
if (data == null || data.isEmpty()) {
throw new BadRequestException(ErrorCode.PLATFORM_USER_NOT_EXISTS);
}
return data;
}

public UserStatus getStatus(Long userId) {
Expand All @@ -66,17 +103,21 @@ private UserStatus getStatus(List<Long> generationList) {

private Map<String, String> createAuthorizationHeader() {
Map<String, String> headers = new HashMap<>();
headers.put("x-api-key", apiKey);
headers.put("x-service-name", serviceName);
headers.put("X-Api-Key", apiKey);
headers.put("X-Service-Name", serviceName);
return headers;
}

private Map<String, Collection<String>> createQueryParams(List<Long> userId) {
Map<String, Collection<String>> queryParams = new HashMap<>();
for (Long id : userId) {
queryParams.put("userIds", Collections.singletonList(id.toString()));
private Map<String, String> createQueryParams(List<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
throw new BadRequestException(ErrorCode.INVALID_PARAMETER);
}
return queryParams;
final String csv = toCsv(userIds);
return Collections.singletonMap("userIds", csv);
}

private String toCsv(List<Long> userIds) {
return userIds.stream().map(String::valueOf).collect(Collectors.joining(","));
}

public List<Long> getMemberGenerationList(Long userId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.sopt.app.application.platform.dto;

import java.util.List;

public record PlatformUserIdsRequest(List<Long> userIds) {}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
Expand All @@ -24,7 +25,6 @@ public List<PokeHistoryInfo> getAllOfPokeBetween(Long userId, Long friendId) {

return pokeHistoryList.stream()
.map(PokeHistoryInfo::from)
.sorted(Comparator.comparing(PokeHistoryInfo::getCreatedAt).reversed())
.toList();
}

Expand Down Expand Up @@ -54,6 +54,9 @@ public List<PokeHistory> getAllLatestPokeHistoryFromTo(Long pokerId, Long pokedI
}

public Page<PokeHistory> getAllLatestPokeHistoryIn(List<Long> targetHistoryIds, Pageable pageable) {
if (targetHistoryIds == null || targetHistoryIds.isEmpty()) {
return Page.empty(pageable);
}
return pokeHistoryRepository.findAllByIdIsInOrderByCreatedAtDesc(targetHistoryIds, pageable);
}

Expand All @@ -79,6 +82,7 @@ public List<PokeHistoryInfo> getAllPokeHistoryByUsers(Long userId, Long friendUs
return pokeHistories.stream().map(PokeHistoryInfo::from).toList();
}

@Transactional
@EventListener(UserWithdrawEvent.class)
public void handleUserWithdrawEvent(final UserWithdrawEvent event) {
pokeHistoryRepository.deleteAllByPokerIdInQuery(event.getUserId());
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/sopt/app/common/response/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public enum ErrorCode {
PLAYGROUND_PROFILE_NOT_EXISTS("플레이그라운드 프로필을 등록하지 않은 유저입니다.", HttpStatus.NOT_FOUND),
INVALID_PLAYGROUND_CARDINAL_INFO("플레이그라운드 활동 정보가 유효하지 않습니다.", HttpStatus.BAD_REQUEST),

// PLATFORM
PLATFORM_USER_NOT_EXISTS("플랫폼 유저 정보를 가져올 수 없습니다.", HttpStatus.NOT_FOUND),

// OPERATION
OPERATION_PROFILE_NOT_EXISTS("운영 서비스에 존재하지 않는 회원입니다.", HttpStatus.NOT_FOUND),

Expand Down Expand Up @@ -76,6 +79,7 @@ public enum ErrorCode {
POKE_HISTORY_NOT_FOUND("해당 찌르기 내역은 존재하지 않습니다.", HttpStatus.NOT_FOUND),
POKE_MESSAGE_TYPE_NOT_FOUND("해당 찌르기 메시지 타입은 존재하지 않습니다.", HttpStatus.NOT_FOUND),
POKE_MESSAGE_MUST_NOT_BE_NULL("찌르기 메시지 타입은 필수 값입니다.", HttpStatus.BAD_REQUEST),
SELF_POKE_NOT_ALLOWED("본인을 찌를 수 없습니다.", HttpStatus.BAD_REQUEST),
DUPLICATE_POKE("이미 찌르기를 보낸 친구입니다.", HttpStatus.CONFLICT),

// FRIEND
Expand Down
77 changes: 61 additions & 16 deletions src/main/java/org/sopt/app/common/response/SlackLoggerAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.sopt.app.domain.entity.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

@Aspect
Expand All @@ -22,26 +23,70 @@ public class SlackLoggerAspect {
private final HttpServletRequest request;
private final SlackService slackService;

private Long getUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
val user = (User) authentication.getPrincipal();
return user.getId();
// 가능한 모든 케이스에서 userId를 안전하게 추출
private Long resolveUserIdSafely() {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) return null;

Object principal = authentication.getPrincipal();

// 1) 애플리케이션에서 전역적으로 쓰는 Long
if (principal instanceof Long l) return l;

// 2) 도메인 User 엔티티
if (principal instanceof User u) return u.getId();

// 3) Spring Security UserDetails (username이 숫자 id일 수 있음)
if (principal instanceof UserDetails ud) {
return parseLongOrNull(ud.getUsername());
}

// 4) String (예: "anonymousUser" 또는 "123")
if (principal instanceof String s) {
Long parsed = parseLongOrNull(s);
if (parsed != null) return parsed; // 숫자면 사용
return null; // anonymous 등은 null 처리
}

// 5) JWT/기타 토큰: authentication.getName()이 종종 subject/username
return parseLongOrNull(authentication.getName());
} catch (Exception ex) {
log.debug("SlackLoggerAspect: failed to resolve user id: {}", ex.getMessage());
return null; // userId를 못 구해도 로깅은 계속 진행
}
}

private Long parseLongOrNull(String v) {
if (v == null) return null;
try { return Long.parseLong(v); }
catch (NumberFormatException e) { return null; }
}

@Before(value = "@annotation(org.sopt.app.common.response.SlackLogger)")
public void sendLogForError(final JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args.length != 1) {
log.warn("Slack Logger Failed : Invalid Used");
return;
}
try {
Object[] args = joinPoint.getArgs();

// 이 애스펙트는 보통 ControllerAdvice의 핸들러(예외 1개 파라미터)를 대상으로 사용된다고 가정
if (args.length == 1 && args[0] instanceof Exception e) {
String requestUrl = request.getRequestURI();
String requestMethod = request.getMethod();

ExceptionWrapper exceptionWrapper = extractExceptionWrapper(e);
Long userId = resolveUserIdSafely();

if (args[0] instanceof Exception e) {
String requestUrl = request.getRequestURI();
String requestMethod = request.getMethod();
ExceptionWrapper exceptionWrapper = extractExceptionWrapper(e);
slackService.sendSlackMessage(
SlackMessageGenerator.generate(exceptionWrapper,getUserId(),requestMethod,requestUrl));
slackService.sendSlackMessage(
SlackMessageGenerator.generate(exceptionWrapper, userId, requestMethod, requestUrl)
);
} else {
// 잘못 붙은 경우에도 전체 플로우를 깨지 않도록 경고만 남김
log.warn("Slack Logger skipped: invalid usage (expects single Exception arg). method={}, args={}",
joinPoint.getSignature(), args.length);
}
} catch (Exception ex) {
// 로거 자체가 장애 유발하지 않도록 방어
log.error("Slack Logger failed: {}", ex.getMessage(), ex);
}
}
}
}
Loading