diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/controller/AICourseController.java b/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/controller/AICourseController.java index 4554ad81..1f693f92 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/controller/AICourseController.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/controller/AICourseController.java @@ -5,7 +5,6 @@ import com.umc.yeogi_gal_lae.api.aiCourse.dto.AICourseIdResponse; import com.umc.yeogi_gal_lae.api.aiCourse.dto.AICourseItineraryResponse; import com.umc.yeogi_gal_lae.api.aiCourse.dto.AICourseResponse; -import com.umc.yeogi_gal_lae.api.aiCourse.dto.DailyItineraryResponse; import com.umc.yeogi_gal_lae.api.aiCourse.repository.AICourseRepository; import com.umc.yeogi_gal_lae.api.aiCourse.service.AICourseService; import com.umc.yeogi_gal_lae.api.place.domain.Place; @@ -61,30 +60,17 @@ public Response getStoredAICourse( @PathVariable Long aiCourseId) { Optional aiCourseOpt = aiCourseRepository.findById(aiCourseId); if (aiCourseOpt.isEmpty()) { - return Response.of(ErrorCode.NOT_FOUND, null); + return Response.of(ErrorCode.NOT_FOUND); } AICourse aiCourse = aiCourseOpt.get(); - // TripPlan 검증: aiCourse에 연결된 TripPlan의 id와 입력받은 tripPlanId가 동일해야 함 if (!aiCourse.getTripPlan().getId().equals(tripPlanId)) { - return Response.of(ErrorCode.NOT_FOUND, null); + return Response.of(ErrorCode.NOT_FOUND); } Map> courseMap = aiCourseService.getStoredAICourseById(aiCourseId); if (courseMap.isEmpty()) { - return Response.of(ErrorCode.NOT_FOUND, null); + return Response.of(ErrorCode.NOT_FOUND); } - // Room 정보 - String roomName = aiCourse.getTripPlan().getRoom().getName(); - int totalRoomMember = (aiCourse.getTripPlan().getRoom().getRoomMembers() != null) - ? aiCourse.getTripPlan().getRoom().getRoomMembers().size() : 0; - // dailyItineraries 변환 - List dailyItineraries = AICourseConverter.toDailyItineraryResponseList(courseMap); - - // 전체 응답 DTO 생성 - AICourseItineraryResponse responseDTO = AICourseItineraryResponse.builder() - .roomName(roomName) - .totalRoomMember(totalRoomMember) - .dailyItineraries(dailyItineraries) - .build(); + AICourseItineraryResponse responseDTO = AICourseConverter.toAICourseItineraryResponse(aiCourse, courseMap); return Response.of(SuccessCode.OK, responseDTO); } diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/converter/AICourseConverter.java b/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/converter/AICourseConverter.java index 7f703b31..9fba6752 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/converter/AICourseConverter.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/converter/AICourseConverter.java @@ -1,10 +1,12 @@ package com.umc.yeogi_gal_lae.api.aiCourse.converter; import com.umc.yeogi_gal_lae.api.aiCourse.domain.AICourse; +import com.umc.yeogi_gal_lae.api.aiCourse.dto.AICourseItineraryResponse; import com.umc.yeogi_gal_lae.api.aiCourse.dto.AICourseResponse; import com.umc.yeogi_gal_lae.api.aiCourse.dto.DailyItineraryResponse; import com.umc.yeogi_gal_lae.api.place.converter.PlaceConverter; import com.umc.yeogi_gal_lae.api.place.domain.Place; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -12,7 +14,8 @@ public class AICourseConverter { - public static List toDailyItineraryResponseList(Map> courseMap) { + public static List toDailyItineraryResponseList(Map> courseMap, + String startDate) { // 기존의 roomName과 totalRoomMember 정보를 제거하고 day, places만 포함하도록 함 return courseMap.entrySet().stream() .map(entry -> DailyItineraryResponse.builder() @@ -31,4 +34,25 @@ public static AICourseResponse toAICourseResponse(AICourse aiCourse) { .roomId(aiCourse.getTripPlan().getRoom().getId()) .build(); } + + public static AICourseItineraryResponse toAICourseItineraryResponse(AICourse aiCourse, + Map> courseMap) { + // TripPlan의 startDate를 "yyyy-MM-dd" 형식으로 변환 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String startDate = aiCourse.getTripPlan().getStartDate().format(formatter); + + // dailyItineraries 생성 (각 DailyItineraryResponse에 startDate 포함) + List dailyItineraries = toDailyItineraryResponseList(courseMap, startDate); + + String roomName = aiCourse.getTripPlan().getRoom().getName(); + int totalRoomMember = (aiCourse.getTripPlan().getRoom().getRoomMembers() != null) + ? aiCourse.getTripPlan().getRoom().getRoomMembers().size() : 0; + + return AICourseItineraryResponse.builder() + .roomName(roomName) + .totalRoomMember(totalRoomMember) + .startDate(startDate) + .dailyItineraries(dailyItineraries) + .build(); + } } diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/dto/AICourseItineraryResponse.java b/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/dto/AICourseItineraryResponse.java index 0f9381f9..758d7e02 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/dto/AICourseItineraryResponse.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/aiCourse/dto/AICourseItineraryResponse.java @@ -1,6 +1,5 @@ package com.umc.yeogi_gal_lae.api.aiCourse.dto; - import java.util.List; import lombok.Builder; import lombok.Getter; @@ -10,5 +9,6 @@ public class AICourseItineraryResponse { private String roomName; private int totalRoomMember; + private String startDate; private List dailyItineraries; } diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/friendship/repository/FriendshipRepository.java b/src/main/java/com/umc/yeogi_gal_lae/api/friendship/repository/FriendshipRepository.java index 99347924..e1174f9a 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/friendship/repository/FriendshipRepository.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/friendship/repository/FriendshipRepository.java @@ -11,7 +11,13 @@ public interface FriendshipRepository extends JpaRepository { Optional findById(Long id); List findByInviterIdOrInviteeId(Long inviterId, Long inviteeId); - Optional findByInviterIdAndInviteeId(Long inviterId, Long inviteeId); + + // inviterId와 inviteeId가 일치하는 Friendship 객체 조회 + @Query("SELECT f FROM Friendship f WHERE (f.inviter.id = :userId AND f.invitee.id = :friendId) OR (f.inviter.id = :friendId AND f.invitee.id = :userId)") + List findByInviterIdAndInviteeIdBothWays(@Param("userId") Long userId, @Param("friendId") Long friendId); + + // 친구 관계가 존재하는지 확인 (양방향) + boolean existsByInviterAndInvitee(User inviter, User invitee); void deleteByInviterOrInvitee(User inviter, User invitee); diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/friendship/service/FriendshipService.java b/src/main/java/com/umc/yeogi_gal_lae/api/friendship/service/FriendshipService.java index 1b54a16e..f9b9e7fc 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/friendship/service/FriendshipService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/friendship/service/FriendshipService.java @@ -55,15 +55,27 @@ public void acceptInvite(String token, String inviteeEmail) { User invitee = userRepository.findByEmail(inviteeEmail) .orElseThrow(() -> new IllegalArgumentException("Invitee not found")); + // 자신이 자신을 초대하는 경우 방지 + if (invite.getInviter().getId().equals(invitee.getId())) { + throw new IllegalArgumentException("자신은 친구 추가할 수 없습니다."); + } + + // 이미 존재하는 친구 관계 확인 (양방향) + boolean isAlreadyFriend = friendshipRepository.existsByInviterAndInvitee(invite.getInviter(), invitee) + || friendshipRepository.existsByInviterAndInvitee(invitee, invite.getInviter()); + + if (isAlreadyFriend) { + throw new IllegalArgumentException("이미 친구 관계가 존재합니다."); + } + + // 새로운 친구 관계 저장 Friendship friendship = Friendship.builder() .inviter(invite.getInviter()) // User 객체 직접 설정 .invitee(invitee) // 초대받은 User 객체 직접 설정 .status(FriendshipStatus.ACCEPT) .build(); - - - friendshipRepository.save(friendship); // 새로운 친구 관계 저장 + friendshipRepository.save(friendship); // 초대 정보 삭제 friendshipInviteRepository.delete(invite); @@ -131,13 +143,15 @@ private List generateMockFriendList() { @Transactional public void deleteFriendship(Long userId, Long friendId) { - // 친구 관계 조회 (양방향 확인) - Friendship friendship = friendshipRepository.findByInviterIdAndInviteeId(userId, friendId) - .or(() -> friendshipRepository.findByInviterIdAndInviteeId(friendId, userId)) // 반대 방향도 확인 - .orElseThrow(() -> new IllegalArgumentException("친구 관계가 아닙니다. ")); + // 양방향 친구 관계를 모두 조회 + List friendships = friendshipRepository.findByInviterIdAndInviteeIdBothWays(userId, friendId); + + if (friendships.isEmpty()) { + throw new IllegalArgumentException("친구 관계가 아닙니다."); + } - // 친구 관계 삭제 - friendshipRepository.delete(friendship); + // 모든 친구 관계 삭제 + friendshipRepository.deleteAll(friendships); } } \ No newline at end of file diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorStatus.java b/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorStatus.java index 19f0a6e7..7a312caa 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorStatus.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorStatus.java @@ -16,7 +16,8 @@ public enum ErrorStatus { // JWT 관련 에러 JWT_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "JWT_500", "JWT 토큰 생성 중 오류가 발생했습니다."), - JWT_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "JWT_401", "유효하지 않은 JWT 토큰입니다."); + JWT_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "JWT_401", "유효하지 않은 JWT 토큰입니다."), + JWT_EXPIRED_TOKEN(HttpStatus.BAD_REQUEST, "JWT_402", "만료된 JWT 토큰입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtAuthenticationFilter.java b/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtAuthenticationFilter.java index 6642bcd5..5a7b844f 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtAuthenticationFilter.java @@ -26,19 +26,28 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - // 스웨거 및 로그인 관련 URL이면 필터 동작 X String requestURI = request.getRequestURI(); + + // 인증이 필요 없는 요청이면 필터를 통과시킴 if (isExcluded(requestURI)) { filterChain.doFilter(request, response); return; } + // JWT 토큰 확인 String token = resolveToken(request); if (token != null && jwtUtil.validateToken(token)) { String email = jwtUtil.extractEmail(token); + + // 현재 로그인한 사용자 정보 SecurityContext에 저장 JwtAuthenticationToken authentication = new JwtAuthenticationToken(email); SecurityContextHolder.getContext().setAuthentication(authentication); + + // Authorization 헤더가 없으면 자동으로 추가 + if (request.getHeader("Authorization") == null) { + request.setAttribute("Authorization", "Bearer " + token); + } } filterChain.doFilter(request, response); diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtUtil.java b/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtUtil.java index f621c78a..09d5edff 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtUtil.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/jwt/JwtUtil.java @@ -3,6 +3,7 @@ import com.umc.yeogi_gal_lae.global.error.AuthHandler; import com.umc.yeogi_gal_lae.global.error.ErrorStatus; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -63,6 +64,8 @@ public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token); return true; + } catch (ExpiredJwtException e) { + throw new AuthHandler(ErrorStatus.JWT_EXPIRED_TOKEN); } catch (JwtException e) { throw new AuthHandler(ErrorStatus.JWT_INVALID_TOKEN); }