From cceadc68f318a86681b1a0f4c73b9132525cc062 Mon Sep 17 00:00:00 2001 From: JiwonHwang Date: Fri, 4 Apr 2025 14:40:45 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feature:=20UserService.getUserProfile=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20id=20=ED=95=B4=EC=8B=B1=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/ktb/modie/service/UserService.java | 297 +++++++++--------- 1 file changed, 150 insertions(+), 147 deletions(-) diff --git a/src/main/java/org/ktb/modie/service/UserService.java b/src/main/java/org/ktb/modie/service/UserService.java index 79ad0f6..d870c30 100644 --- a/src/main/java/org/ktb/modie/service/UserService.java +++ b/src/main/java/org/ktb/modie/service/UserService.java @@ -5,6 +5,7 @@ import org.ktb.modie.core.exception.BusinessException; import org.ktb.modie.core.exception.CustomErrorCode; +import org.ktb.modie.core.util.HashIdUtil; import org.ktb.modie.domain.User; import org.ktb.modie.presentation.v1.dto.UpdateAccountRequest; import org.ktb.modie.presentation.v1.dto.UserResponse; @@ -25,152 +26,154 @@ @Service @RequiredArgsConstructor public class UserService { - private static final String KAKAO_API_URL = "https://kapi.kakao.com/v2/user/me"; - private static final String KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token"; - - @Value("${kakao.client-id}") - private String kakaoClientId; - - @Value("${kakao.redirect-uri}") - private String kakaoRedirectUri; - - @Autowired - private UserRepository userRepository; - - @Autowired - private RestTemplate restTemplate; // RestTemplate을 빈으로 관리하도록 수정 - - // 카카오 로그인 후 Access Token을 가져오는 메서드 - public User getKakaoUserInfo(String code) { - // Step 1: Access Token을 얻기 위한 요청 - String tokenUrl = KAKAO_TOKEN_URL + "?grant_type=authorization_code" - + "&client_id=" + kakaoClientId - + "&redirect_uri=" + kakaoRedirectUri - + "&code=" + code; - - try { - System.out.println("Access Token 요청 URL: " + tokenUrl); // 토큰 요청 URL 로그 - // Access Token 요청 - ResponseEntity response = restTemplate.exchange(tokenUrl, HttpMethod.POST, - new HttpEntity<>(new HttpHeaders()), Map.class); - - System.out.println("Access Token 응답: " + response.getBody()); // 응답 내용 로그 - - String accessToken = (String)response.getBody().get("access_token"); - String refreshToken = (String)response.getBody().get("refresh_token"); - - // Step 2: Access Token이 없으면 Refresh Token으로 새 Access Token 발급 - if (accessToken == null && refreshToken != null) { - System.out.println("Access Token이 없어서 Refresh Token으로 새로 발급 시도"); - accessToken = refreshAccessToken(refreshToken); - } - - System.out.println("Access Token: " + accessToken); // Access Token 확인 - - // Step 3: 카카오 API로부터 사용자 정보 조회 - String userInfoUrl = KAKAO_API_URL + "?access_token=" + accessToken; - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + accessToken); - HttpEntity entity = new HttpEntity<>(headers); - - System.out.println("사용자 정보 조회 URL: " + userInfoUrl); // 사용자 정보 조회 URL 로그 - - ResponseEntity userInfoResponse = restTemplate.exchange(userInfoUrl, HttpMethod.GET, - entity, Map.class); - - System.out.println("사용자 정보 응답: " + userInfoResponse.getBody()); // 사용자 정보 응답 로그 - - // 카카오 사용자 정보 파싱 - Object kakaoIdObject = userInfoResponse.getBody().get("id"); - Long kakaoId = null; - - if (kakaoIdObject instanceof Long) { - kakaoId = (Long)kakaoIdObject; - } else if (kakaoIdObject instanceof Integer) { - kakaoId = ((Integer)kakaoIdObject).longValue(); - } - - if (kakaoId == null) { - System.out.println("카카오 ID가 없습니다!"); // 카카오 ID 없는 경우 - } - - String userId = String.valueOf(kakaoId); // Long을 String으로 변환 - - String userName = (String)((Map)userInfoResponse.getBody().get("properties")).get("nickname"); - String profileImageUrl = (String)((Map)userInfoResponse.getBody().get("properties")).get("profile_image"); - - System.out.println("사용자 이름: " + userName); // 사용자 이름 로그 - System.out.println("프로필 이미지 URL: " + profileImageUrl); // 프로필 이미지 URL 로그 - - // Step 4: DB에서 사용자 조회, 없다면 새로 추가 - User user = userRepository.findByUserId(userId); - if (user == null) { - System.out.println("새로운 사용자 등록: " + userName); // 새로운 사용자 등록 로그 - user = User.builder() - .userId(userId) - .userName(userName) - .profileImageUrl(profileImageUrl) - .bankName("") - .accountNumber("") - .createdAt(LocalDateTime.now()) - .build(); - userRepository.save(user); - } - - return user; - } catch (HttpClientErrorException e) { - e.printStackTrace(); - // code가 만료되었을 경우 401 - if (e.getStatusCode().value() == 401) { - throw new BusinessException(CustomErrorCode.INVALID_PERMISSION_CODE); - } - // 카카오 API 호출 오류 처리 - throw new BusinessException(CustomErrorCode.INVALID_REQUEST); - } - } - - // Step 5: Refresh Token을 이용해 새 Access Token을 발급 받는 메서드 - private String refreshAccessToken(String refreshToken) { - String refreshTokenUrl = KAKAO_TOKEN_URL + "?grant_type=refresh_token" - + "&client_id=" + kakaoClientId - + "&refresh_token=" + refreshToken; - - try { - // Refresh Token을 이용해 새로운 Access Token 발급 - ResponseEntity response = restTemplate.exchange(refreshTokenUrl, HttpMethod.POST, - new HttpEntity<>(new HttpHeaders()), Map.class); - return (String)response.getBody().get("access_token"); - } catch (HttpClientErrorException e) { - // 토큰 갱신 실패 시 예외 처리 - throw new BusinessException(CustomErrorCode.INVALID_TOKEN); - } - } - - public UserResponse getUserProfile(String userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException( - CustomErrorCode.USER_NOT_FOUND - )); - - // TODO: 인증되지 않은 사용자(토큰 유효성 검사) 예외처리 - - return UserResponse.builder() - .userId(user.getUserId()) - .userName(user.getUserName()) - .profileImageUrl(user.getProfileImageUrl()) - .bankName(user.getBankName()) - .accountNumber(user.getAccountNumber()) - .build(); - } - - @Transactional - public void updateAccount(String userId, UpdateAccountRequest request) { - // 사용자 조회 - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); - - // 계좌 정보 업데이트 - user.updateAccountInfo(request.bankName(), request.accountNumber()); - } + private static final String KAKAO_API_URL = "https://kapi.kakao.com/v2/user/me"; + private static final String KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token"; + private final HashIdUtil hashIdUtil; + + @Value("${kakao.client-id}") + private String kakaoClientId; + + @Value("${kakao.redirect-uri}") + private String kakaoRedirectUri; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RestTemplate restTemplate; // RestTemplate을 빈으로 관리하도록 수정 + + // 카카오 로그인 후 Access Token을 가져오는 메서드 + public User getKakaoUserInfo(String code) { + // Step 1: Access Token을 얻기 위한 요청 + String tokenUrl = KAKAO_TOKEN_URL + "?grant_type=authorization_code" + + "&client_id=" + kakaoClientId + + "&redirect_uri=" + kakaoRedirectUri + + "&code=" + code; + + try { + System.out.println("Access Token 요청 URL: " + tokenUrl); // 토큰 요청 URL 로그 + // Access Token 요청 + ResponseEntity response = restTemplate.exchange(tokenUrl, HttpMethod.POST, + new HttpEntity<>(new HttpHeaders()), Map.class); + + System.out.println("Access Token 응답: " + response.getBody()); // 응답 내용 로그 + + String accessToken = (String)response.getBody().get("access_token"); + String refreshToken = (String)response.getBody().get("refresh_token"); + + // Step 2: Access Token이 없으면 Refresh Token으로 새 Access Token 발급 + if (accessToken == null && refreshToken != null) { + System.out.println("Access Token이 없어서 Refresh Token으로 새로 발급 시도"); + accessToken = refreshAccessToken(refreshToken); + } + + System.out.println("Access Token: " + accessToken); // Access Token 확인 + + // Step 3: 카카오 API로부터 사용자 정보 조회 + String userInfoUrl = KAKAO_API_URL + "?access_token=" + accessToken; + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + System.out.println("사용자 정보 조회 URL: " + userInfoUrl); // 사용자 정보 조회 URL 로그 + + ResponseEntity userInfoResponse = restTemplate.exchange(userInfoUrl, HttpMethod.GET, + entity, Map.class); + + System.out.println("사용자 정보 응답: " + userInfoResponse.getBody()); // 사용자 정보 응답 로그 + + // 카카오 사용자 정보 파싱 + Object kakaoIdObject = userInfoResponse.getBody().get("id"); + Long kakaoId = null; + + if (kakaoIdObject instanceof Long) { + kakaoId = (Long)kakaoIdObject; + } else if (kakaoIdObject instanceof Integer) { + kakaoId = ((Integer)kakaoIdObject).longValue(); + } + + if (kakaoId == null) { + System.out.println("카카오 ID가 없습니다!"); // 카카오 ID 없는 경우 + } + + String userId = String.valueOf(kakaoId); // Long을 String으로 변환 + + String userName = (String)((Map)userInfoResponse.getBody().get("properties")).get("nickname"); + String profileImageUrl = (String)((Map)userInfoResponse.getBody().get("properties")).get("profile_image"); + + System.out.println("사용자 이름: " + userName); // 사용자 이름 로그 + System.out.println("프로필 이미지 URL: " + profileImageUrl); // 프로필 이미지 URL 로그 + + // Step 4: DB에서 사용자 조회, 없다면 새로 추가 + User user = userRepository.findByUserId(userId); + if (user == null) { + System.out.println("새로운 사용자 등록: " + userName); // 새로운 사용자 등록 로그 + user = User.builder() + .userId(userId) + .userName(userName) + .profileImageUrl(profileImageUrl) + .bankName("") + .accountNumber("") + .createdAt(LocalDateTime.now()) + .build(); + userRepository.save(user); + } + + return user; + } catch (HttpClientErrorException e) { + e.printStackTrace(); + // code가 만료되었을 경우 401 + if (e.getStatusCode().value() == 401) { + throw new BusinessException(CustomErrorCode.INVALID_PERMISSION_CODE); + } + // 카카오 API 호출 오류 처리 + throw new BusinessException(CustomErrorCode.INVALID_REQUEST); + } + } + + // Step 5: Refresh Token을 이용해 새 Access Token을 발급 받는 메서드 + private String refreshAccessToken(String refreshToken) { + String refreshTokenUrl = KAKAO_TOKEN_URL + "?grant_type=refresh_token" + + "&client_id=" + kakaoClientId + + "&refresh_token=" + refreshToken; + + try { + // Refresh Token을 이용해 새로운 Access Token 발급 + ResponseEntity response = restTemplate.exchange(refreshTokenUrl, HttpMethod.POST, + new HttpEntity<>(new HttpHeaders()), Map.class); + return (String)response.getBody().get("access_token"); + } catch (HttpClientErrorException e) { + // 토큰 갱신 실패 시 예외 처리 + throw new BusinessException(CustomErrorCode.INVALID_TOKEN); + } + } + + public UserResponse getUserProfile(String userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException( + CustomErrorCode.USER_NOT_FOUND + )); + + // TODO: 인증되지 않은 사용자(토큰 유효성 검사) 예외처리 + String userHashId = hashIdUtil.encode(Long.parseLong(user.getUserId())); + + return UserResponse.builder() + .userId(userHashId) + .userName(user.getUserName()) + .profileImageUrl(user.getProfileImageUrl()) + .bankName(user.getBankName()) + .accountNumber(user.getAccountNumber()) + .build(); + } + + @Transactional + public void updateAccount(String userId, UpdateAccountRequest request) { + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); + + // 계좌 정보 업데이트 + user.updateAccountInfo(request.bankName(), request.accountNumber()); + } } From a5cb56ff139a416b0a322ce27e7b28eded5177cf Mon Sep 17 00:00:00 2001 From: JiwonHwang Date: Fri, 4 Apr 2025 18:29:43 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/controller/ChatHistoryController.java | 147 +++++++++--------- 1 file changed, 74 insertions(+), 73 deletions(-) diff --git a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java index 4d28f4a..4b61ece 100644 --- a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java +++ b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java @@ -1,6 +1,8 @@ package org.ktb.modie.presentation.v1.controller; -import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.stream.Collectors; + import org.ktb.modie.core.exception.BusinessException; import org.ktb.modie.core.exception.CustomErrorCode; import org.ktb.modie.core.response.SuccessResponse; @@ -22,80 +24,79 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.List; -import java.util.stream.Collectors; +import jakarta.servlet.http.HttpServletRequest; @RestController public class ChatHistoryController { - private final ChatRepository chatRepository; - private final UserRepository userRepository; - private final MeetRepository meetRepository; // MeetRepository 추가 - private final HashIdUtil hashIdUtil; - - // 생성자 수정: MeetRepository도 주입받도록 수정 - public ChatHistoryController(ChatRepository chatRepository, UserRepository userRepository, - MeetRepository meetRepository, HashIdUtil hashIdUtil) { - this.chatRepository = chatRepository; - this.userRepository = userRepository; - this.meetRepository = meetRepository; - this.hashIdUtil = hashIdUtil; - } - - @GetMapping("/api/v1/chat/{meetId}") - public ResponseEntity>> getChatHistory( - @PathVariable("meetId") String meetHashId, - @RequestParam(value = "lastChatId", required = false) Long lastChatId, - @RequestAttribute("userId") String loggedInUserId, - HttpServletRequest request) { - - Long meetId = hashIdUtil.decode(meetHashId); - System.out.println("getChatHistory start !! "); - // 유저 정보 보완 (userId로 DB에서 조회) - meetRepository.findById(meetId) - .orElseThrow(() -> new BusinessException( - CustomErrorCode.MEETING_NOT_FOUND, - "알 수 없는 모임입니다." - )); - - List chatList; - Pageable pageable = PageRequest.of(0, 25); - - if (lastChatId == null || lastChatId == 0) { - chatList = chatRepository.findTop25ByMeetIdOrderByCreatedAtDesc(meetId, pageable); - } else { - chatList = chatRepository.findByMeetIdAndMessageIdLessThanOrderByCreatedAtDesc(meetId, lastChatId, - pageable); - } - - List chatDtoList = chatList.stream() - .map(chat -> { - User user = chat.getUser(); - Meet meet = chat.getMeet(); - - // 방장 여부 확인 - boolean isOwner = meet.getOwner().getUserId().equals(chat.getUser().getUserId()); - - // 본인 여부 확인 - boolean isMe = chat.getUser().getUserId().equals(loggedInUserId); - - // 변경된 DTO 필드명에 맞춰 생성자 호출 - return new ChatDto( - // chat.getMessageId().longValue(), // chatId - chat.getMessageId(), // chatId - (chat.getUser().getUserId()), // userId - chat.getMessageContent(), // content (이전의 message) - user.getUserName(), // nickname (이전의 sender) - chat.getCreatedAt().toString().split("\\.")[0], // dateTime - meetHashId, // meetId - isOwner, // isOwner - isMe // isMe - ); - }) - .collect(Collectors.toList()); - - System.out.println("chatDtoList size : " + chatDtoList.size()); - - return SuccessResponse.of(chatDtoList).asHttp(HttpStatus.OK); - } + private final ChatRepository chatRepository; + private final UserRepository userRepository; + private final MeetRepository meetRepository; // MeetRepository 추가 + private final HashIdUtil hashIdUtil; + + // 생성자 수정: MeetRepository도 주입받도록 수정 + public ChatHistoryController(ChatRepository chatRepository, UserRepository userRepository, + MeetRepository meetRepository, HashIdUtil hashIdUtil) { + this.chatRepository = chatRepository; + this.userRepository = userRepository; + this.meetRepository = meetRepository; + this.hashIdUtil = hashIdUtil; + } + + @GetMapping("/api/v1/chat/{meetId}") + public ResponseEntity>> getChatHistory( + @PathVariable("meetId") String meetHashId, + @RequestParam(value = "lastChatId", required = false) Long lastChatId, + @RequestAttribute("userId") String loggedInUserId, + HttpServletRequest request) { + + Long meetId = hashIdUtil.decode(meetHashId); + System.out.println("getChatHistory start !! "); + // 유저 정보 보완 (userId로 DB에서 조회) + meetRepository.findById(meetId) + .orElseThrow(() -> new BusinessException( + CustomErrorCode.MEETING_NOT_FOUND, + "알 수 없는 모임입니다." + )); + + List chatList; + Pageable pageable = PageRequest.of(0, 25); + + if (lastChatId == null || lastChatId == 0) { + chatList = chatRepository.findTop25ByMeetIdOrderByCreatedAtDesc(meetId, pageable); + } else { + chatList = chatRepository.findByMeetIdAndMessageIdLessThanOrderByCreatedAtDesc(meetId, lastChatId, + pageable); + } + + List chatDtoList = chatList.stream() + .map(chat -> { + User user = chat.getUser(); + Meet meet = chat.getMeet(); + + // 방장 여부 확인 + boolean isOwner = meet.getOwner().getUserId().equals(chat.getUser().getUserId()); + + // 본인 여부 확인 + boolean isMe = chat.getUser().getUserId().equals(loggedInUserId); + + // 변경된 DTO 필드명에 맞춰 생성자 호출 + String userHashId = hashIdUtil.encode(Long.parseLong(user.getUserId())); + return new ChatDto( + chat.getMessageId(), // chatId + userHashId, // userId + chat.getMessageContent(), // content (이전의 message) + user.getUserName(), // nickname (이전의 sender) + chat.getCreatedAt().toString().split("\\.")[0], // dateTime + meetHashId, // meetId + isOwner, // isOwner + isMe // isMe + ); + }) + .collect(Collectors.toList()); + + System.out.println("chatDtoList size : " + chatDtoList.size()); + + return SuccessResponse.of(chatDtoList).asHttp(HttpStatus.OK); + } } From c1fd91f3e94901103b37636b8a8364dfb5d9098e Mon Sep 17 00:00:00 2001 From: JiwonHwang Date: Fri, 4 Apr 2025 18:30:33 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feature:=20JwtService=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20user=5Fid=20=ED=95=B4=EC=8B=B1=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/ktb/modie/service/JwtService.java | 152 ++++++++++-------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/ktb/modie/service/JwtService.java b/src/main/java/org/ktb/modie/service/JwtService.java index 17d2752..3c653c9 100644 --- a/src/main/java/org/ktb/modie/service/JwtService.java +++ b/src/main/java/org/ktb/modie/service/JwtService.java @@ -4,6 +4,9 @@ import javax.crypto.SecretKey; +import org.ktb.modie.core.exception.BusinessException; +import org.ktb.modie.core.exception.CustomErrorCode; +import org.ktb.modie.core.util.HashIdUtil; import org.ktb.modie.domain.User; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -15,76 +18,93 @@ import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Service @Slf4j +@RequiredArgsConstructor public class JwtService { - private final long expirationTime = 1000L * 60 * 60 * 24 * 30; // 1개월 - - @Value("${jwt.secret}") - private String secretKey; - - private SecretKey getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } - - // JWT 토큰 생성 - public String createToken(User user) { - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + expirationTime); // 만료 시간 설정 - - return Jwts.builder() - .setSubject(user.getUserId()) - .setIssuedAt(now) // 발급 시간 설정 - .expiration(expiryDate) // 만료 시간 설정 - .signWith(getSigningKey()) // SecretKey 적용 - .compact(); // 토큰 생성 - } - - // JWT 토큰에서 사용자 정보 추출 - public String extractUserId(String token) { - Claims claims = Jwts.parser() - .verifyWith(getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - return claims.getSubject(); // 사용자 ID 반환 - } - - // JWT 토큰 유효성 검사 - public boolean isTokenExpired(String token) { - Date expiration = Jwts.parser() - .verifyWith(getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload() - .getExpiration(); - return expiration.before(new Date()); - } - - // JWT 토큰의 유효성 검사 - public boolean isTokenValid(String token) { - try { - Jwts.parser() - .verifyWith(getSigningKey()) - .build() - .parseSignedClaims(token); - return !isTokenExpired(token); - } catch (ExpiredJwtException e) { - log.warn("Token expired: {}", e.getMessage()); - return false; - } catch (MalformedJwtException e) { - log.warn("Invalid JWT token format: {}", e.getMessage()); - return false; - } catch (SignatureException e) { - log.error("Invalid JWT signature: {}", e.getMessage()); - return false; - } catch (Exception e) { - log.error("Unexpected token validation error: {}", e.getMessage()); - return false; - } - } + private final long expirationTime = 1000L * 60 * 60 * 24 * 30; // 1개월 + private final HashIdUtil hashIdUtil; + + @Value("${jwt.secret}") + private String secretKey; + + private SecretKey getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + // JWT 토큰 생성 + public String createToken(User user) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expirationTime); // 만료 시간 설정 + + // userId Hash encode + String userId = hashIdUtil.encode(Long.parseLong(user.getUserId())); + + return Jwts.builder() + .setSubject(userId) + .setIssuedAt(now) // 발급 시간 설정 + .expiration(expiryDate) // 만료 시간 설정 + .signWith(getSigningKey()) // SecretKey 적용 + .compact(); // 토큰 생성 + } + + // JWT 토큰에서 사용자 정보 추출 + public String extractUserId(String token) { + Claims claims = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + + String jwtUserId = claims.getSubject(); + if (jwtUserId == null || jwtUserId.isEmpty()) { + throw new BusinessException(CustomErrorCode.INVALID_TOKEN); + } + + try { + return String.valueOf(hashIdUtil.decode(jwtUserId)); + } catch (Exception e) { + log.error("Failed to decode JWT userId: {}", jwtUserId, e); + throw new BusinessException(CustomErrorCode.INVALID_TOKEN); + } + } + + // JWT 토큰 유효성 검사 + public boolean isTokenExpired(String token) { + Date expiration = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .getExpiration(); + return expiration.before(new Date()); + } + + // JWT 토큰의 유효성 검사 + public boolean isTokenValid(String token) { + try { + Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token); + return !isTokenExpired(token); + } catch (ExpiredJwtException e) { + log.warn("Token expired: {}", e.getMessage()); + return false; + } catch (MalformedJwtException e) { + log.warn("Invalid JWT token format: {}", e.getMessage()); + return false; + } catch (SignatureException e) { + log.error("Invalid JWT signature: {}", e.getMessage()); + return false; + } catch (Exception e) { + log.error("Unexpected token validation error: {}", e.getMessage()); + return false; + } + } } From 19faaafab724b76eff0fbeaddde7ae88459a42a9 Mon Sep 17 00:00:00 2001 From: JiwonHwang Date: Fri, 4 Apr 2025 18:31:02 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feature:=20MeetService.updatePaymentStatus?= =?UTF-8?q?=20user=5Fid=20=ED=95=B4=EC=8B=B1=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/ktb/modie/service/MeetService.java | 789 +++++++++--------- 1 file changed, 400 insertions(+), 389 deletions(-) diff --git a/src/main/java/org/ktb/modie/service/MeetService.java b/src/main/java/org/ktb/modie/service/MeetService.java index c3e78bc..90f0957 100644 --- a/src/main/java/org/ktb/modie/service/MeetService.java +++ b/src/main/java/org/ktb/modie/service/MeetService.java @@ -1,6 +1,10 @@ package org.ktb.modie.service; -import lombok.RequiredArgsConstructor; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + import org.ktb.modie.core.exception.BusinessException; import org.ktb.modie.core.exception.CustomErrorCode; import org.ktb.modie.core.util.HashIdUtil; @@ -26,398 +30,405 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; +import lombok.RequiredArgsConstructor; // meetService @Service @RequiredArgsConstructor public class MeetService { - private final UserMeetRepository userMeetRepository; - private final UserRepository userRepository; - private final MeetRepository meetRepository; - private final HashIdUtil hashIdUtil; - - @Transactional - public CreateMeetResponse createMeet(String userId, CreateMeetRequest request) { - User owner = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); - - // meetAt이 1년 이상 이후일 경우 예외 - if (request.meetAt().isAfter(LocalDateTime.now().plusYears(1))) { - throw new BusinessException(CustomErrorCode.INVALID_DATE_TOO_FAR); - } - - Meet meet = Meet.builder() - .meetIntro(request.meetIntro()) - .meetType(request.meetType()) - .address(request.address()) - .addressDescription(request.addressDescription()) - .meetAt(request.meetAt()) - .totalCost(request.totalCost()) - .memberLimit(request.memberLimit()) - .owner(owner) - .build(); - - Meet savedMeet = meetRepository.save(meet); - String meetHashId = hashIdUtil.encode(savedMeet.getMeetId()); - - return new CreateMeetResponse(meetHashId); - } - - @Transactional - public void createUserMeet(String userId, String meetHashId) { - Long meetId = hashIdUtil.decode(meetHashId); - - // Token 받아오면 userId로 변환하는 과정 필요 - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); - - // 모임 조회 - Meet meet = meetRepository.findById(meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); - - // 삭제나 완료된 모임 - if (meet.getDeletedAt() != null || meet.getCompletedAt() != null) { - throw new BusinessException(CustomErrorCode.DENIED_JOIN_ALREADY_ENDED); - } - - // 방장은 참여 불가 - if (meet.getOwner().getUserId().equals(userId)) { - throw new BusinessException(CustomErrorCode.OWNER_CANNOT_JOIN_MEET); - } - - // 중복 참여 방지 및 탈퇴한 사용자 처리 - Optional existUserMeet = userMeetRepository - .findUserMeetByUser_UserIdAndMeet_MeetId(userId, meetId); - - if (existUserMeet.isPresent()) { - UserMeet userMeet = existUserMeet.get(); - - if (userMeet.getDeletedAt() == null) { - // 이미 참여 중 - throw new BusinessException(CustomErrorCode.ALREADY_JOINED_MEET); - } else { - // 탈퇴한 이력 있음 - userMeet.setDeletedAt(null); - userMeet.setPayed(false); - return; // 복구 후 종료 - } - } - - // 정원 초과 여부 체크 - int currentMemberCount = userMeetRepository.countByMeetAndDeletedAtIsNull(meet) + 1; - if (currentMemberCount >= meet.getMemberLimit()) { - throw new BusinessException(CustomErrorCode.MEETING_CAPACITY_FULL); - } - // 참여 정보 저장 - UserMeet userMeet = UserMeet.builder() - .user(user) - .meet(meet) - .isPayed(false) - .build(); - - userMeetRepository.save(userMeet); - } - - public MeetDto getMeet(String userId, String meetHashId) { - // NOTE: 비정상적인 meetID가 넘어온 경우 - Long meetId = hashIdUtil.decode(meetHashId); - - if (meetId <= 0) { - throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); - } - // NOTE: 정상적인 meetID 이지만 Data가 없는 경우 - Meet meet = meetRepository.findById(meetId) - .orElseThrow(() -> new BusinessException( - CustomErrorCode.MEETING_NOT_FOUND - )); - // NOTE: 역할(owner, member, guest) - String meetRule = getMeetRole(userId, meet); - - // NOTE: 참여중인 멤버 - List members = userMeetRepository.findUserDtosByMeetId(meetId); - - return MeetDto.builder() -// .meetId(meet.getMeetId()) - .meetId(meetHashId) - .ownerName(meet.getOwner().getUserName()) - .meetIntro(meet.getMeetIntro()) - .meetType(meet.getMeetType()) - .address(meet.getAddress()) - .addressDescription(meet.getAddressDescription()) - .meetAt(meet.getMeetAt()) - .totalCost(meet.getTotalCost()) - .memberLimit(meet.getMemberLimit()) - .createdAt(meet.getCreatedAt()) - .updatedAt(meet.getUpdatedAt()) - .deletedAt(meet.getDeletedAt()) - .completedAt(meet.getCompletedAt()) - .meetRule(meetRule) - .members(members) - .build(); - } - - public MeetListResponse getMeetList(String userId, String meetType, Boolean isCompleted, int page) { - if (meetType != null && meetType.length() > 10) { - throw new BusinessException(CustomErrorCode.INVALID_INPUT_PAGE); // meetType이 10자를 초과하면 예외 발생 - } - - // 페이지 번호 검증 - if (page < 1) { - throw new BusinessException(CustomErrorCode.INVALID_INPUT_PAGE); - } - - // 페이징 설정 (기본 페이지 크기 = 10) - Pageable pageable = PageRequest.of(page - 1, 10, Sort.by(Sort.Direction.DESC, "meetAt")); - - // 필터링된 모임 리스트 조회 - Page meetPage = meetRepository.findFilteredMeets(userId, meetType, isCompleted, pageable); - - // 총 페이지 수를 벗어난 경우 - if (page > (meetPage.getTotalElements() / 10) + 1) { - throw new BusinessException(CustomErrorCode.INVALID_INPUT_PAGE); - } - - // MeetSummaryDto로 변환 - List meetSummaryList = meetPage.getContent().stream() - .map(meet -> new MeetSummaryDto( - hashIdUtil.encode(meet.getMeetId()), - meet.getMeetIntro(), - meet.getMeetType(), - meet.getMeetAt(), - meet.getAddress(), - meet.getAddressDescription(), - meet.getTotalCost() > 0, // 비용 여부 (0보다 크면 true) - userMeetRepository.countByMeetAndDeletedAtIsNull(meet) + 1, // 현재 참여 인원 수 - meet.getMemberLimit(), // 최대 인원 수 - meet.getOwner().getUserName() // 모임장 이름 - )) - .toList(); - - return new MeetListResponse( - page, // 페이지 번호 - 10, // 페이지 크기 (고정값) - meetPage.getTotalElements(), // 전체 요소 수 - meetSummaryList // 변환된 모임 리스트 - ); - - } - - @Transactional - public void deleteUserMeet(String userId, String meetHashId) { - Long meetId = hashIdUtil.decode(meetHashId); - // 모임 조회 - Meet meet = meetRepository.findById(meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); - - // 사용자 조회 - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); - - // 현재 시간이 모임 시작 시간을 지나면 나갈 수 없음 - if (LocalDateTime.now().isAfter(meet.getMeetAt())) { - throw new BusinessException(CustomErrorCode.MEETING_ALREADY_STARTED); - } - - // 종료된 모임 확인 - 종료된 모임은 나갈 수 없음 - if (meet.getCompletedAt() != null) { - throw new BusinessException(CustomErrorCode.MEETING_ALREADY_ENDED); - } - - // 사용자가 해당 모임에 참여 중인지 확인 - UserMeet userMeet = userMeetRepository.findUserMeetByUser_UserIdAndMeet_MeetIdAndDeletedAtIsNull(userId, meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.PERMISSION_DENIED_NOT_MEMBER)); - - if (userMeet.getDeletedAt() != null) { - throw new BusinessException(CustomErrorCode.ALREADY_EXITED_MEET); - } - - // 모임에서 나가기 처리 - userMeet.setDeletedAt(LocalDateTime.now()); - } - - @Transactional - public UpdateMeetResponse updateMeet(String userId, String meetHashId, UpdateMeetRequest request) { - Long meetId = hashIdUtil.decode(meetHashId); - - // NOTE: 비정상적인 meetID가 넘어온 경우 - if (meetId <= 0) { - throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); - } - - // NOTE: 정상적인 meetID 이지만 Data가 없는 경우 - Meet meet = meetRepository.findById(meetId) - .orElseThrow(() -> new BusinessException( - CustomErrorCode.MEETING_NOT_FOUND - )); - - // NOTE: 요청한 유저의 id이 ownerId와 같은지 확인 -> 토큰 구현 후 구현예정 - if (!meet.getOwner().getUserId().equals(userId)) { - throw new BusinessException(CustomErrorCode.UNAUTHORIZED_USER_NOT_OWNER); - } - // ✅ 현재 참여 인원 수 확인 - int currentMemberCount = userMeetRepository.countByMeet_MeetIdAndDeletedAtIsNull(meetId); - - // ❌ 최소 인원 제한 위반 - if (request.memberLimit() < 2) { - throw new BusinessException(CustomErrorCode.MEMBER_LIMIT_TOO_LOW); - } - - // ❌ 현재 인원보다 낮게 설정한 경우 - if (request.memberLimit() < currentMemberCount) { - throw new BusinessException(CustomErrorCode.MEMBER_LIMIT_LESS_THAN_CURRENT); - } - meet.setMeetIntro(request.meetIntro()); - meet.setMeetType(request.meetType()); - meet.setAddress(request.address()); - meet.setAddressDescription(request.addressDescription()); - meet.setMeetAt(request.meetAt()); - meet.setTotalCost(request.totalCost()); - meet.setMemberLimit(request.memberLimit()); - meet.setUpdatedAt(LocalDateTime.now()); - - meetRepository.save(meet); - - return new UpdateMeetResponse(meet.getMeetId()); - } - - @Transactional - public void completeMeet(String userId, String meetHashId) { - Long meetId = hashIdUtil.decode(meetHashId); - - // 모임 조회 - Meet meet = meetRepository.findById(meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); - - // 모임 생성자인지 확인 - if (!meet.getOwner().getUserId().equals(userId)) { - throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_COMPLETED_NOT_OWNER); - } - - // NOTE: 비용이 없는 경우 바로 종료 - if (meet.getTotalCost() == 0) { - meet.setCompletedAt(LocalDateTime.now()); - return; - } - // NOTE: 정산 완료 여부 확인(비용이 있는 경우) - Long unpaidUsers = userMeetRepository.countUnpaidActiveUsersByMeetId(meetId); - if (unpaidUsers > 0) { - throw new BusinessException(CustomErrorCode.OPERATION_DENIED_SETTLEMENT_INCOMPLETE); - } - - // 모임 종료 처리 - meet.setCompletedAt(LocalDateTime.now()); - } - - @Transactional - public void updatePaymentStatus(String userId, String meetHashId, UpdatePaymentRequest request) { - Long meetId = hashIdUtil.decode(meetHashId); - - // 모임 조회 - Meet meet = meetRepository.findById(meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); - - // 종료된 모임이면 예외처리 - if (meet.getCompletedAt() != null && meet.getCompletedAt().isBefore(LocalDateTime.now())) { - throw new BusinessException(CustomErrorCode.ALREADY_COMPLETED_MEET); - } - - // 타겟 사용자 조회 - User user = userRepository.findById(request.userId()) - .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); - - // 방장 사용자 조회 - User owner = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); - - // 요청한 사람이 방장인지 확인 - if (!meet.getOwner().getUserId().equals(userId)) { - throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_SETTLEMENT_NOT_OWNER); - } - - // 해당 유저가 해당 모임에 참여 중인지 확인 - UserMeet userMeet = userMeetRepository.findUserMeetByUser_UserIdAndMeet_MeetIdAndDeletedAtIsNull( - request.userId(), meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.SETTLEMENT_PERMISSION_DENIED_NOT_MEMBER)); - - // 정산 상태 변경 (true <-> false 토글) - userMeet.setPayed(!userMeet.isPayed()); - } - - @Transactional - public void deleteMeet(String meetHashId, String userId) { - Long meetId = hashIdUtil.decode(meetHashId); - - // 모임 존재여부 - Meet meet = meetRepository.findActiveByMeedId(meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); - - // 모임 생성자 여부 - if (!meet.getOwner().getUserId().equals(userId)) { // 12345 -> currentUser(controller) - throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_NOT_OWNER); - } - // 시작된 모임 삭제 불가 - if (meet.getMeetAt().isBefore(LocalDateTime.now())) { - throw new BusinessException(CustomErrorCode.MEETING_ALREADY_STARTED); - } - - // soft delete - meet.delete(); - } - - @Transactional - public void updateTotalCost(String userId, String meetHashId, int totalCost) { - Long meetId = hashIdUtil.decode(meetHashId); - // 비정상적인 meetID가 넘어온 경우 - if (meetId <= 0) { - throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); - } - // 모임 존재여부 - Meet meet = meetRepository.findActiveByMeedId(meetId) - .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); - - // 모임 생성자 여부 - if (!meet.getOwner().getUserId().equals(userId)) { - throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_NOT_OWNER); - } - - // 금액 유효성 검사 - if (totalCost < 0 || totalCost > 10000000) { - throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); - } - - meet.setTotalCost(totalCost); - } - - private String getMeetRole(String userId, Meet meet) { - // NOTE: 유저의 확인 - if (userId == null || userId.isEmpty() || meet == null) { - return "guest"; // userId가 없는 경우 게스트 - } - - // NOTE: 모임의 소유자 확인 - if (meet.getOwner() != null && userId.equals(meet.getOwner().getUserId())) { - return "owner"; // 모임의 소유자 - } - - // NOTE: 유저가 해당 모임에 존재하는지 확인 (삭제된 기록도 고려) - Optional userMeetOpt = userMeetRepository.findUserMeetByUser_UserIdAndMeet_MeetId(userId, - meet.getMeetId()); - - if (!userMeetOpt.isPresent()) { - return "guest"; // 유저가 모임에 존재하지 않으면 게스트 - } - - UserMeet userMeet = userMeetOpt.get(); - - // NOTE: deletedAt이 null이 아니면 탈퇴한 상태 -> guest - if (userMeet.getDeletedAt() != null) { - return "guest"; // 탈퇴한 유저는 게스트 - } - - return "member"; // 탈퇴하지 않은 유저는 멤버 - } + private final UserMeetRepository userMeetRepository; + private final UserRepository userRepository; + private final MeetRepository meetRepository; + private final HashIdUtil hashIdUtil; + + @Transactional + public CreateMeetResponse createMeet(String userId, CreateMeetRequest request) { + User owner = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); + + // meetAt이 1년 이상 이후일 경우 예외 + if (request.meetAt().isAfter(LocalDateTime.now().plusYears(1))) { + throw new BusinessException(CustomErrorCode.INVALID_DATE_TOO_FAR); + } + + Meet meet = Meet.builder() + .meetIntro(request.meetIntro()) + .meetType(request.meetType()) + .address(request.address()) + .addressDescription(request.addressDescription()) + .meetAt(request.meetAt()) + .totalCost(request.totalCost()) + .memberLimit(request.memberLimit()) + .owner(owner) + .build(); + + Meet savedMeet = meetRepository.save(meet); + String meetHashId = hashIdUtil.encode(savedMeet.getMeetId()); + + return new CreateMeetResponse(meetHashId); + } + + @Transactional + public void createUserMeet(String userId, String meetHashId) { + Long meetId = hashIdUtil.decode(meetHashId); + + // Token 받아오면 userId로 변환하는 과정 필요 + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); + + // 모임 조회 + Meet meet = meetRepository.findById(meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); + + // 삭제나 완료된 모임 + if (meet.getDeletedAt() != null || meet.getCompletedAt() != null) { + throw new BusinessException(CustomErrorCode.DENIED_JOIN_ALREADY_ENDED); + } + + // 방장은 참여 불가 + if (meet.getOwner().getUserId().equals(userId)) { + throw new BusinessException(CustomErrorCode.OWNER_CANNOT_JOIN_MEET); + } + + // 중복 참여 방지 및 탈퇴한 사용자 처리 + Optional existUserMeet = userMeetRepository + .findUserMeetByUser_UserIdAndMeet_MeetId(userId, meetId); + + if (existUserMeet.isPresent()) { + UserMeet userMeet = existUserMeet.get(); + + if (userMeet.getDeletedAt() == null) { + // 이미 참여 중 + throw new BusinessException(CustomErrorCode.ALREADY_JOINED_MEET); + } else { + // 탈퇴한 이력 있음 + userMeet.setDeletedAt(null); + userMeet.setPayed(false); + return; // 복구 후 종료 + } + } + + // 정원 초과 여부 체크 + int currentMemberCount = userMeetRepository.countByMeetAndDeletedAtIsNull(meet) + 1; + if (currentMemberCount >= meet.getMemberLimit()) { + throw new BusinessException(CustomErrorCode.MEETING_CAPACITY_FULL); + } + // 참여 정보 저장 + UserMeet userMeet = UserMeet.builder() + .user(user) + .meet(meet) + .isPayed(false) + .build(); + + userMeetRepository.save(userMeet); + } + + public MeetDto getMeet(String userId, String meetHashId) { + // NOTE: 비정상적인 meetID가 넘어온 경우 + Long meetId = hashIdUtil.decode(meetHashId); + + if (meetId <= 0) { + throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); + } + // NOTE: 정상적인 meetID 이지만 Data가 없는 경우 + Meet meet = meetRepository.findById(meetId) + .orElseThrow(() -> new BusinessException( + CustomErrorCode.MEETING_NOT_FOUND + )); + // NOTE: 역할(owner, member, guest) + String meetRule = getMeetRole(userId, meet); + + // NOTE: 참여중인 멤버 + List members = userMeetRepository.findUserDtosByMeetId(meetId) + .stream() + .map(member -> + UserDto.builder() + .userId(hashIdUtil.encode(Long.parseLong(member.userId()))) + .userName(member.userName()) + .isPayed(member.isPayed()) + .build()) + .collect(Collectors.toList()); + + return MeetDto.builder() + // .meetId(meet.getMeetId()) + .meetId(meetHashId) + .ownerName(meet.getOwner().getUserName()) + .meetIntro(meet.getMeetIntro()) + .meetType(meet.getMeetType()) + .address(meet.getAddress()) + .addressDescription(meet.getAddressDescription()) + .meetAt(meet.getMeetAt()) + .totalCost(meet.getTotalCost()) + .memberLimit(meet.getMemberLimit()) + .createdAt(meet.getCreatedAt()) + .updatedAt(meet.getUpdatedAt()) + .deletedAt(meet.getDeletedAt()) + .completedAt(meet.getCompletedAt()) + .meetRule(meetRule) + .members(members) + .build(); + } + + public MeetListResponse getMeetList(String userId, String meetType, Boolean isCompleted, int page) { + if (meetType != null && meetType.length() > 10) { + throw new BusinessException(CustomErrorCode.INVALID_INPUT_PAGE); // meetType이 10자를 초과하면 예외 발생 + } + + // 페이지 번호 검증 + if (page < 1) { + throw new BusinessException(CustomErrorCode.INVALID_INPUT_PAGE); + } + + // 페이징 설정 (기본 페이지 크기 = 10) + Pageable pageable = PageRequest.of(page - 1, 10, Sort.by(Sort.Direction.DESC, "meetAt")); + + // 필터링된 모임 리스트 조회 + Page meetPage = meetRepository.findFilteredMeets(userId, meetType, isCompleted, pageable); + + // 총 페이지 수를 벗어난 경우 + if (page > (meetPage.getTotalElements() / 10) + 1) { + throw new BusinessException(CustomErrorCode.INVALID_INPUT_PAGE); + } + + // MeetSummaryDto로 변환 + List meetSummaryList = meetPage.getContent().stream() + .map(meet -> new MeetSummaryDto( + hashIdUtil.encode(meet.getMeetId()), + meet.getMeetIntro(), + meet.getMeetType(), + meet.getMeetAt(), + meet.getAddress(), + meet.getAddressDescription(), + meet.getTotalCost() > 0, // 비용 여부 (0보다 크면 true) + userMeetRepository.countByMeetAndDeletedAtIsNull(meet) + 1, // 현재 참여 인원 수 + meet.getMemberLimit(), // 최대 인원 수 + meet.getOwner().getUserName() // 모임장 이름 + )) + .toList(); + + return new MeetListResponse( + page, // 페이지 번호 + 10, // 페이지 크기 (고정값) + meetPage.getTotalElements(), // 전체 요소 수 + meetSummaryList // 변환된 모임 리스트 + ); + + } + + @Transactional + public void deleteUserMeet(String userId, String meetHashId) { + Long meetId = hashIdUtil.decode(meetHashId); + // 모임 조회 + Meet meet = meetRepository.findById(meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); + + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); + + // 현재 시간이 모임 시작 시간을 지나면 나갈 수 없음 + if (LocalDateTime.now().isAfter(meet.getMeetAt())) { + throw new BusinessException(CustomErrorCode.MEETING_ALREADY_STARTED); + } + + // 종료된 모임 확인 - 종료된 모임은 나갈 수 없음 + if (meet.getCompletedAt() != null) { + throw new BusinessException(CustomErrorCode.MEETING_ALREADY_ENDED); + } + + // 사용자가 해당 모임에 참여 중인지 확인 + UserMeet userMeet = userMeetRepository.findUserMeetByUser_UserIdAndMeet_MeetIdAndDeletedAtIsNull(userId, meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.PERMISSION_DENIED_NOT_MEMBER)); + + if (userMeet.getDeletedAt() != null) { + throw new BusinessException(CustomErrorCode.ALREADY_EXITED_MEET); + } + + // 모임에서 나가기 처리 + userMeet.setDeletedAt(LocalDateTime.now()); + } + + @Transactional + public UpdateMeetResponse updateMeet(String userId, String meetHashId, UpdateMeetRequest request) { + Long meetId = hashIdUtil.decode(meetHashId); + + // NOTE: 비정상적인 meetID가 넘어온 경우 + if (meetId <= 0) { + throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); + } + + // NOTE: 정상적인 meetID 이지만 Data가 없는 경우 + Meet meet = meetRepository.findById(meetId) + .orElseThrow(() -> new BusinessException( + CustomErrorCode.MEETING_NOT_FOUND + )); + + // NOTE: 요청한 유저의 id이 ownerId와 같은지 확인 -> 토큰 구현 후 구현예정 + if (!meet.getOwner().getUserId().equals(userId)) { + throw new BusinessException(CustomErrorCode.UNAUTHORIZED_USER_NOT_OWNER); + } + // ✅ 현재 참여 인원 수 확인 + int currentMemberCount = userMeetRepository.countByMeet_MeetIdAndDeletedAtIsNull(meetId); + + // ❌ 최소 인원 제한 위반 + if (request.memberLimit() < 2) { + throw new BusinessException(CustomErrorCode.MEMBER_LIMIT_TOO_LOW); + } + + // ❌ 현재 인원보다 낮게 설정한 경우 + if (request.memberLimit() < currentMemberCount) { + throw new BusinessException(CustomErrorCode.MEMBER_LIMIT_LESS_THAN_CURRENT); + } + meet.setMeetIntro(request.meetIntro()); + meet.setMeetType(request.meetType()); + meet.setAddress(request.address()); + meet.setAddressDescription(request.addressDescription()); + meet.setMeetAt(request.meetAt()); + meet.setTotalCost(request.totalCost()); + meet.setMemberLimit(request.memberLimit()); + meet.setUpdatedAt(LocalDateTime.now()); + + meetRepository.save(meet); + + return new UpdateMeetResponse(meet.getMeetId()); + } + + @Transactional + public void completeMeet(String userId, String meetHashId) { + Long meetId = hashIdUtil.decode(meetHashId); + + // 모임 조회 + Meet meet = meetRepository.findById(meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); + + // 모임 생성자인지 확인 + if (!meet.getOwner().getUserId().equals(userId)) { + throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_COMPLETED_NOT_OWNER); + } + + // NOTE: 비용이 없는 경우 바로 종료 + if (meet.getTotalCost() == 0) { + meet.setCompletedAt(LocalDateTime.now()); + return; + } + // NOTE: 정산 완료 여부 확인(비용이 있는 경우) + Long unpaidUsers = userMeetRepository.countUnpaidActiveUsersByMeetId(meetId); + if (unpaidUsers > 0) { + throw new BusinessException(CustomErrorCode.OPERATION_DENIED_SETTLEMENT_INCOMPLETE); + } + + // 모임 종료 처리 + meet.setCompletedAt(LocalDateTime.now()); + } + + @Transactional + public void updatePaymentStatus(String userId, String meetHashId, UpdatePaymentRequest request) { + Long meetId = hashIdUtil.decode(meetHashId); + String targetUserId = String.valueOf(hashIdUtil.decode(request.userId())); + + // 모임 조회 + Meet meet = meetRepository.findById(meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); + + // 종료된 모임이면 예외처리 + if (meet.getCompletedAt() != null && meet.getCompletedAt().isBefore(LocalDateTime.now())) { + throw new BusinessException(CustomErrorCode.ALREADY_COMPLETED_MEET); + } + + // 타겟 사용자 조회 + User user = userRepository.findById(targetUserId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); + + // 방장 사용자 조회 + User owner = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.USER_NOT_FOUND)); + + // 요청한 사람이 방장인지 확인 + if (!owner.getUserId().equals(userId)) { + throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_SETTLEMENT_NOT_OWNER); + } + + // 해당 유저가 해당 모임에 참여 중인지 확인 + UserMeet userMeet = userMeetRepository.findUserMeetByUser_UserIdAndMeet_MeetIdAndDeletedAtIsNull( + user.getUserId(), meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.SETTLEMENT_PERMISSION_DENIED_NOT_MEMBER)); + + // 정산 상태 변경 (true <-> false 토글) + userMeet.setPayed(!userMeet.isPayed()); + } + + @Transactional + public void deleteMeet(String meetHashId, String userId) { + Long meetId = hashIdUtil.decode(meetHashId); + + // 모임 존재여부 + Meet meet = meetRepository.findActiveByMeedId(meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); + + // 모임 생성자 여부 + if (!meet.getOwner().getUserId().equals(userId)) { // 12345 -> currentUser(controller) + throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_NOT_OWNER); + } + // 시작된 모임 삭제 불가 + if (meet.getMeetAt().isBefore(LocalDateTime.now())) { + throw new BusinessException(CustomErrorCode.MEETING_ALREADY_STARTED); + } + + // soft delete + meet.delete(); + } + + @Transactional + public void updateTotalCost(String userId, String meetHashId, int totalCost) { + Long meetId = hashIdUtil.decode(meetHashId); + // 비정상적인 meetID가 넘어온 경우 + if (meetId <= 0) { + throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); + } + // 모임 존재여부 + Meet meet = meetRepository.findActiveByMeedId(meetId) + .orElseThrow(() -> new BusinessException(CustomErrorCode.MEETING_NOT_FOUND)); + + // 모임 생성자 여부 + if (!meet.getOwner().getUserId().equals(userId)) { + throw new BusinessException(CustomErrorCode.PERMISSION_DENIED_NOT_OWNER); + } + + // 금액 유효성 검사 + if (totalCost < 0 || totalCost > 10000000) { + throw new BusinessException(CustomErrorCode.INVALID_INPUT_IN_MEET); + } + + meet.setTotalCost(totalCost); + } + + private String getMeetRole(String userId, Meet meet) { + // NOTE: 유저의 확인 + if (userId == null || userId.isEmpty() || meet == null) { + return "guest"; // userId가 없는 경우 게스트 + } + + // NOTE: 모임의 소유자 확인 + if (meet.getOwner() != null && userId.equals(meet.getOwner().getUserId())) { + return "owner"; // 모임의 소유자 + } + + // NOTE: 유저가 해당 모임에 존재하는지 확인 (삭제된 기록도 고려) + Optional userMeetOpt = userMeetRepository.findUserMeetByUser_UserIdAndMeet_MeetId(userId, + meet.getMeetId()); + + if (!userMeetOpt.isPresent()) { + return "guest"; // 유저가 모임에 존재하지 않으면 게스트 + } + + UserMeet userMeet = userMeetOpt.get(); + + // NOTE: deletedAt이 null이 아니면 탈퇴한 상태 -> guest + if (userMeet.getDeletedAt() != null) { + return "guest"; // 탈퇴한 유저는 게스트 + } + + return "member"; // 탈퇴하지 않은 유저는 멤버 + } } From 7ed0e2b7faf8529f8efa190693bdb62b17d52555 Mon Sep 17 00:00:00 2001 From: JiwonHwang Date: Tue, 8 Apr 2025 01:52:49 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EC=A7=80=EC=9A=B0=EA=B8=B0=20#156?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modie/presentation/v1/controller/ChatHistoryController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java index 4b61ece..adbe018 100644 --- a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java +++ b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java @@ -34,7 +34,6 @@ public class ChatHistoryController { private final MeetRepository meetRepository; // MeetRepository 추가 private final HashIdUtil hashIdUtil; - // 생성자 수정: MeetRepository도 주입받도록 수정 public ChatHistoryController(ChatRepository chatRepository, UserRepository userRepository, MeetRepository meetRepository, HashIdUtil hashIdUtil) { this.chatRepository = chatRepository; From dafd98745fe9549e4078f6d56e8324cafce1a514 Mon Sep 17 00:00:00 2001 From: JiwonHwang Date: Tue, 8 Apr 2025 01:53:51 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=EB=AC=B8=20=EC=A7=80=EC=9A=B0=EA=B8=B0=20?= =?UTF-8?q?#156?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modie/presentation/v1/controller/ChatHistoryController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java index adbe018..89f46fb 100644 --- a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java +++ b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java @@ -50,7 +50,6 @@ public ResponseEntity>> getChatHistory( HttpServletRequest request) { Long meetId = hashIdUtil.decode(meetHashId); - System.out.println("getChatHistory start !! "); // 유저 정보 보완 (userId로 DB에서 조회) meetRepository.findById(meetId) .orElseThrow(() -> new BusinessException( From 81687d389a020dd91274bdcdf4ec35e3f1125221 Mon Sep 17 00:00:00 2001 From: JiwonHwang Date: Tue, 8 Apr 2025 01:58:48 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=20=EB=A7=9E=EC=B6=94=EA=B8=B0=20#156?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/v1/controller/ChatHistoryController.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java index 89f46fb..a655e1e 100644 --- a/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java +++ b/src/main/java/org/ktb/modie/presentation/v1/controller/ChatHistoryController.java @@ -63,8 +63,12 @@ public ResponseEntity>> getChatHistory( if (lastChatId == null || lastChatId == 0) { chatList = chatRepository.findTop25ByMeetIdOrderByCreatedAtDesc(meetId, pageable); } else { - chatList = chatRepository.findByMeetIdAndMessageIdLessThanOrderByCreatedAtDesc(meetId, lastChatId, - pageable); + chatList = chatRepository + .findByMeetIdAndMessageIdLessThanOrderByCreatedAtDesc( + meetId, + lastChatId, + pageable + ); } List chatDtoList = chatList.stream()