1616import com .example .cp_main_be .domain .social .follow .domain .repository .FollowRepository ;
1717import com .example .cp_main_be .global .common .CustomApiException ;
1818import com .example .cp_main_be .global .common .ErrorCode ;
19- import com .example .cp_main_be .global .exception .AvatarNotFoundException ;
20- import com .example .cp_main_be .global .exception .UserNotFoundException ;
2119import java .time .LocalDateTime ;
2220import java .time .ZoneId ;
23- import java .util .Comparator ;
24- import java .util .List ;
25- import java .util .Objects ;
26- import java .util .UUID ;
21+ import java .util .*;
2722import java .util .stream .Collectors ;
2823import lombok .RequiredArgsConstructor ;
2924import org .springframework .security .core .Authentication ;
3631@ Transactional
3732public class UserService {
3833
34+ private static final int MAX_FRIEND_WATERING_PER_DAY = 3 ;
35+
3936 private final UserRepository userRepository ;
4037 private final LevelService levelService ;
4138 private final AvatarRepository avatarRepository ;
@@ -45,8 +42,8 @@ public class UserService {
4542 public void addExperience (Long actorId , int points ) {
4643 User user =
4744 userRepository
48- .findById (actorId )
49- .orElseThrow (() -> new UserNotFoundException (ErrorCode .USER_NOT_FOUND . getMessage () ));
45+ .findById (actorId ) // ID로 최신 유저 정보를 조회합니다.
46+ .orElseThrow (() -> new CustomApiException (ErrorCode .USER_NOT_FOUND ));
5047 user .addExperience (points );
5148 levelService .checkLevelUp (user );
5249 }
@@ -59,7 +56,7 @@ public void updateAvatar(User user, AvatarChangeRequest request, Long avatarId)
5956 Avatar avatar =
6057 avatarRepository
6158 .findById (avatarId )
62- .orElseThrow (() -> new AvatarNotFoundException ( "아바타를 찾을 수 없습니다." ));
59+ .orElseThrow (() -> new CustomApiException ( ErrorCode . AVATAR_NOT_FOUND ));
6360
6461 // [버그 수정] AvatarMaster(원본)가 아닌 Avatar(개별 인스턴스)의 imageUrl을 변경해야 합니다.
6562 // Avatar 엔티티에 imageUrl 필드가 있어야 합니다.
@@ -74,8 +71,12 @@ public void updateAvatar(User user, AvatarChangeRequest request, Long avatarId)
7471 }
7572
7673 public void updateNickname (User user , String newNickname ) {
77-
78- user .updateProfile (newNickname , null );
74+ // [수정] stale한 user 객체 대신, ID로 최신 정보를 조회해서 사용합니다.
75+ User managedUser =
76+ userRepository
77+ .findById (user .getId ())
78+ .orElseThrow (() -> new CustomApiException (ErrorCode .USER_NOT_FOUND ));
79+ managedUser .updateProfile (newNickname , null );
7980 userRepository .save (user );
8081 }
8182
@@ -86,21 +87,21 @@ public void saveUser(User user) {
8687 public void deleteUser (Long userId ) {
8788 User user =
8889 userRepository
89- .findById (userId )
90- .orElseThrow (() -> new UserNotFoundException ( "사용자를 찾을 수 없습니다." ));
90+ .findById (userId ) // ID로 최신 유저 정보를 조회합니다.
91+ .orElseThrow (() -> new CustomApiException ( ErrorCode . USER_NOT_FOUND ));
9192 userRepository .delete (user );
9293 }
9394
9495 public User findUserById (Long id ) {
9596 return this .userRepository
9697 .findById (id )
97- .orElseThrow (() -> new UserNotFoundException ( "해당 ID의 사용자를 찾을 수 없습니다 : " + id ));
98+ .orElseThrow (() -> new CustomApiException ( ErrorCode . USER_NOT_FOUND ));
9899 }
99100
100101 public User findUserByUuid (UUID uuid ) {
101102 return this .userRepository
102103 .findByUuid (uuid )
103- .orElseThrow (() -> new UserNotFoundException ( "해당 UUID의 사용자를 찾을 수 없습니다 : " + uuid ));
104+ .orElseThrow (() -> new CustomApiException ( ErrorCode . USER_NOT_FOUND ));
104105 }
105106
106107 public List <User > findAllUsers () {
@@ -128,11 +129,11 @@ public User getCurrentUser() {
128129 if (principal instanceof String uuidString ) {
129130 UUID userUuid = UUID .fromString (uuidString );
130131 return userRepository
131- .findByUuid (userUuid )
132- .orElseThrow (() -> new IllegalArgumentException ( "현재 로그인한 사용자를 찾을 수 없습니다." ));
132+ .findByUuid (userUuid ) // UUID로 최신 유저 정보를 조회합니다.
133+ .orElseThrow (() -> new CustomApiException ( ErrorCode . USER_NOT_FOUND ));
133134 }
134135
135- throw new IllegalArgumentException ( "인증 정보를 찾을 수 없습니다." );
136+ throw new CustomApiException ( ErrorCode . INVALID_TOKEN , "인증 정보를 찾을 수 없습니다." );
136137 }
137138
138139 /**
@@ -147,9 +148,10 @@ public List<Long> getMyGardenIds(User user) {
147148 User managedUser =
148149 userRepository
149150 .findByIdWithGardens (user .getId ())
150- .orElseThrow (() -> new UserNotFoundException ( "사용자를 찾을 수 없습니다." ));
151+ .orElseThrow (() -> new CustomApiException ( ErrorCode . USER_NOT_FOUND ));
151152
152153 return managedUser .getGardens ().stream ()
154+ .filter (garden -> !garden .isLocked ()) // [추가] 잠겨있지 않은(isLocked=false) 텃밭만 필터링합니다.
153155 .sorted (Comparator .comparing (Garden ::getSlotNumber ))
154156 .map (Garden ::getId )
155157 .collect (Collectors .toList ());
@@ -165,12 +167,12 @@ public List<Long> getMyGardenIds(User user) {
165167 public UserProfileResponse getUserProfile (Long currentUserId , Long profileUserId ) {
166168 User currentUser =
167169 userRepository
168- .findById (currentUserId )
169- .orElseThrow (() -> new CustomApiException (ErrorCode .NOT_FOUND ));
170+ .findById (currentUserId ) // ID로 최신 유저 정보를 조회합니다.
171+ .orElseThrow (() -> new CustomApiException (ErrorCode .USER_NOT_FOUND ));
170172 User profileUser =
171173 userRepository
172174 .findByIdWithGardensAndAvatars (profileUserId )
173- .orElseThrow (() -> new CustomApiException (ErrorCode .NOT_FOUND ));
175+ .orElseThrow (() -> new CustomApiException (ErrorCode .USER_NOT_FOUND ));
174176
175177 // [수정] 프로필 이미지 URL을 사용자의 첫 번째 아바타 이미지로 설정
176178 String profileImageUrl =
@@ -210,29 +212,28 @@ public UserProfileResponse getUserProfile(Long currentUserId, Long profileUserId
210212 int todayWateringCountForOthers =
211213 friendWateringLogRepository .countByWaterGiverAndWateredAtAfter (
212214 currentUser , startOfWateringDay );
213- Long leftWaterCountForOthers =
214- (long ) (3 - todayWateringCountForOthers ); // MAX_FRIEND_WATERING_PER_DAY = 3
215+ long leftWaterCountForOthers =
216+ Math .max (0 , (long ) MAX_FRIEND_WATERING_PER_DAY - todayWateringCountForOthers );
217+
218+ // [성능 개선] N+1 문제를 해결하기 위해, 오늘 내가 물 준 정원 ID 목록을 한 번에 조회합니다.
219+ Set <Long > wateredGardenIds =
220+ friendWateringLogRepository .findWateredGardenIdsByGiverAndDate (
221+ currentUser .getId (), startOfWateringDay );
215222
216223 // 3. 프로필 주인의 정원 목록 및 물주기 가능 여부 계산
217224 List <UserGardenDetailResponse > userGardens =
218225 profileUser .getGardens ().stream ()
226+ .sorted (Comparator .comparing (Garden ::getSlotNumber ))
219227 .map (
220228 garden -> {
221- // 현재 접속 유저가 이 정원에 오늘 물을 줄 수 있는지 여부
222- boolean isWateringAbleByMe = false ;
223- // 조건: 1) 아직 오늘 남에게 물 줄 수 있는 횟수가 남아있어야 하고 (leftWaterCountForOthers > 0)
224- // 2) 오늘 이 정원에 내가 물을 준 적이 없어야 한다.
225- if (leftWaterCountForOthers > 0 ) {
226- boolean alreadyWateredByMe =
227- friendWateringLogRepository
228- .existsByWaterGiverAndWateredGardenAndWateredAtAfter (
229- currentUser , garden , startOfWateringDay );
230- isWateringAbleByMe = !alreadyWateredByMe ;
231- }
229+ // DB를 반복 조회하는 대신, 미리 조회한 Set에서 확인하여 성능을 개선합니다.
230+ boolean alreadyWateredByMe = wateredGardenIds .contains (garden .getId ());
231+ boolean isWateringAbleByMe = leftWaterCountForOthers > 0 && !alreadyWateredByMe ;
232232
233233 HomeResponseDto .AvatarInfo avatarInfoForGarden =
234234 HomeResponseDto .AvatarInfo .builder ()
235- .avatarId (garden .getAvatar ().getId ())
235+ // 아바타가 없는 텃밭이 있을 수 있는 예외 케이스를 방어합니다.
236+ .avatarId (garden .getAvatar () != null ? garden .getAvatar ().getId () : null )
236237 .avatarName (garden .getAvatar ().getNickname ())
237238 .avatarImageUrl (garden .getAvatar ().getAvatarMaster ().getDefaultImageUrl ())
238239 .build ();
0 commit comments