Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,49 @@ public class GroupReader {
private final GroupJpaRepository groupJpaRepository;
private final UserGroupJpaRepository userGroupJpaRepository;
private final JoinApplyJpaRepository joinApplyJpaRepository;

public Group findGroup(Long groupId) {
return groupJpaRepository.findById(groupId)
.orElseThrow(() -> new BusinessException(GROUP_NOT_FOUND));
}

public Group findGroupForUpdate(Long groupId) {
return groupJpaRepository.findByIdForUpdate(groupId)
.orElseThrow(() -> new BusinessException(GROUP_NOT_FOUND));
}

public UserGroup findUserGroupByRole(Long groupId, Long userId, Role role) {
return userGroupJpaRepository.findByGroupIdAndUserIdAndRoleAndJoinStatus(
groupId, userId, role, JoinStatus.JOINED)
.orElseThrow(() -> new BusinessException(NOT_AUTHORIZED));
}

public UserGroup findJoinedUserGroup(Long groupId, Long userId) {
return userGroupJpaRepository.findByGroupIdAndUserIdAndJoinStatus(
groupId, userId, JoinStatus.JOINED)
.orElseThrow(() -> new BusinessException(NOT_GROUP_MEMBER));
}

public boolean existsJoinedUserGroup(Long groupId, Long userId) {
return userGroupJpaRepository.existsByGroupIdAndUserIdAndJoinStatus(groupId, userId, JoinStatus.JOINED);
}

public UserGroup findKickTarget(Long groupId, Long ownerId, Long targetUserId) {
if (ownerId.equals(targetUserId)) throw new BusinessException(CANNOT_KICK_SELF);

findUserGroupByRole(groupId, ownerId, Role.OWNER);

return findUserGroupByRole(groupId, targetUserId, Role.MEMBER);
}

public UserGroup findLeaveTarget(Long groupId, Long userId) {
return findUserGroupByRole(groupId, userId, Role.MEMBER);
}

public List<GroupSummaryRes> readMyGroups(Long cursor, int pageSize, Long userId) {
return groupJpaRepository.findMyGroups(userId, pageSize, cursor);
}

public GroupMenuRes readMenu(Long userId, Long groupId) {
Group group = findGroup(groupId);
UserGroup userGroup = userId == null ? null : userGroupJpaRepository
Expand All @@ -64,6 +81,7 @@ public GroupMenuRes readMenu(Long userId, Long groupId) {
Role role = userGroup == null ? Role.GUEST : userGroup.getRole();
return GroupMenuRes.of(group, role);
}

public List<UserGroup> readGroupUsers(Long groupId) {
Group group = findGroup(groupId);
if (group.getGroupKind() == GroupKind.PRIVATE) {
Expand All @@ -72,6 +90,7 @@ public List<UserGroup> readGroupUsers(Long groupId) {

return userGroupJpaRepository.findAllByGroupIdAndJoinStatus(groupId, JoinStatus.JOINED);
}

public JoinApply findJoinApplyByIdAndGroupIdWithUser(Long joinApplyId, Long groupId) {
return joinApplyJpaRepository.findByIdAndGroupIdAndJoinApplyStatus(joinApplyId, groupId, JoinApplyStatus.PENDING)
.orElseThrow(() -> new BusinessException(JOIN_APPLY_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public GroupDetailRes detail(final Long groupId, final Long userId) {
}

Pageable detailRecentPage = PageRequest.of(0, DETAIL_RECENT_SIZE);
int travelCount = Math.toIntExact(travelItineraryJpaRepository.countByGroupIdAndIsDeletedFalse(groupId));
int travelCount = Math.toIntExact(travelItineraryJpaRepository.countByGroupId(groupId));
List<RecentTravelDto> recentTravels;
if (myRole == Role.GUEST) {
recentTravels = List.of();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public TransferCreateResponseDto create(final Long userId, final TransferCreateR

Group group = groupJpaRepository.findById(dto.groupId()).orElseThrow(() -> new BusinessException(GroupErrorCode.GROUP_NOT_FOUND));

TravelItinerary travelItinerary = travelItineraryJpaRepository.findByIdAndGroupIdAndIsDeletedFalseForUpdate(dto.travelItineraryId(), dto.groupId()).orElseThrow(() -> new BusinessException(TravelItineraryErrorCode.TRAVEL_NOT_FOUND));
TravelItinerary travelItinerary = travelItineraryJpaRepository.findByIdAndGroupIdForUpdate(dto.travelItineraryId(), dto.groupId()).orElseThrow(() -> new BusinessException(TravelItineraryErrorCode.TRAVEL_NOT_FOUND));

UserTravelItinerary creatorTravelMembership = userTravelItineraryJpaRepository.findByUserIdAndTravelItineraryId(userId, travelItinerary.getId())
.orElseThrow(() -> new BusinessException(TransferErrorCode.USER_TRAVEL_ITINERARY_NOT_FOUND));
Expand Down
114 changes: 114 additions & 0 deletions src/main/java/org/triple/backend/travel/application/TravelCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package org.triple.backend.travel.application;

import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.triple.backend.global.error.BusinessException;
import org.triple.backend.group.domain.group.Group;
import org.triple.backend.travel.entity.TravelItinerary;
import org.triple.backend.travel.entity.UserRole;
import org.triple.backend.travel.entity.UserTravelItinerary;
import org.triple.backend.travel.exception.TravelItineraryErrorCode;
import org.triple.backend.travel.exception.UserTravelItineraryErrorCode;
import org.triple.backend.travel.repository.TravelItineraryJpaRepository;
import org.triple.backend.travel.repository.TravelReviewImageJpaRepository;
import org.triple.backend.travel.repository.TravelReviewJpaRepository;
import org.triple.backend.travel.repository.UserTravelItineraryJpaRepository;
import org.triple.backend.user.domain.User;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class TravelCommand {
private final TravelReader travelReader;
private final TravelItineraryJpaRepository travelItineraryJpaRepository;
private final TravelReviewJpaRepository travelReviewJpaRepository;
private final TravelReviewImageJpaRepository travelReviewImageJpaRepository;
private final UserTravelItineraryJpaRepository userTravelItineraryJpaRepository;

public TravelItinerary saveTravelItinerary(
final User leader,
final Group group,
final String title,
final LocalDateTime startAt,
final LocalDateTime endAt,
final String description,
final List<User> members
) {
TravelItinerary travelItinerary = TravelItinerary.of(title, startAt, endAt, group, description);
TravelItinerary savedTravelItinerary = travelItineraryJpaRepository.save(travelItinerary);

userTravelItineraryJpaRepository.save(UserTravelItinerary.of(leader, savedTravelItinerary, UserRole.LEADER));

List<UserTravelItinerary> memberTravelItineraries = new ArrayList<>();
for (User member : members) {
memberTravelItineraries.add(UserTravelItinerary.of(member, savedTravelItinerary, UserRole.MEMBER));
}
userTravelItineraryJpaRepository.saveAll(memberTravelItineraries);

savedTravelItinerary.increaseMemberCount(memberTravelItineraries.size());
return savedTravelItinerary;
}

public void addUserTravelItinerary(
final User user,
final TravelItinerary travelItinerary,
final UserRole role
) {
try {
travelItinerary.increaseMemberCount(1);
userTravelItineraryJpaRepository.save(UserTravelItinerary.of(user, travelItinerary, role));
travelItineraryJpaRepository.flush();
} catch (DataIntegrityViolationException e) {
throw new BusinessException(UserTravelItineraryErrorCode.ALREADY_JOINED_TRAVEL);
} catch (OptimisticLockingFailureException e) {
throw new BusinessException(UserTravelItineraryErrorCode.CONCURRENT_TRAVEL_ITINERARY);
}
}

public void updateTravel(final Long travelItineraryId, final String title, final LocalDateTime startAt,
final LocalDateTime endAt, final String description) {
TravelItinerary travel = travelReader.findTravel(travelItineraryId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method performs a redundant database fetch. The caller (TravelUseCase.updateTravel) has already performed authorization checks which likely involved fetching the same entity or its related mappings. Consider passing the TravelItinerary entity directly to this method to avoid unnecessary database round-trips.


try {
travel.changeTitle(title);
travel.changeStartAt(startAt);
travel.changeEndAt(endAt);
travel.changeDescription(description);
travelItineraryJpaRepository.flush();
} catch (OptimisticLockingFailureException e) {
throw new BusinessException(TravelItineraryErrorCode.CONCURRENT_TRAVEL_ITINERARY_UPDATE);
}
}

public void deleteTravel(final TravelItinerary travelItinerary) {
try {
userTravelItineraryJpaRepository.bulkDeleteByTravelId(travelItinerary.getId());
travelItineraryJpaRepository.delete(travelItinerary);
travelItineraryJpaRepository.flush();
} catch (OptimisticLockingFailureException e) {
throw new BusinessException(TravelItineraryErrorCode.CONCURRENT_TRAVEL_ITINERARY_DELETE);
}
}

public void leaveTravel(final Long travelId, final Long userId) {
UserTravelItinerary userTravelItinerary = userTravelItineraryJpaRepository
.findByUserIdAndTravelItineraryIdAndUserRole(userId, travelId, UserRole.MEMBER)
.orElseThrow(() -> new BusinessException(UserTravelItineraryErrorCode.USER_TRAVEL_ITINERARY_NOT_FOUND));

TravelItinerary travelItinerary = userTravelItinerary.getTravelItinerary();

try {
travelItinerary.decreaseMemberCount();
userTravelItineraryJpaRepository.delete(userTravelItinerary);
userTravelItineraryJpaRepository.flush();
} catch (OptimisticLockingFailureException e) {
throw new BusinessException(UserTravelItineraryErrorCode.CONCURRENT_TRAVEL_ITINERARY_LEAVE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.triple.backend.travel.application;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.triple.backend.global.error.BusinessException;
import org.triple.backend.travel.entity.TravelItinerary;
import org.triple.backend.travel.entity.UserRole;
import org.triple.backend.travel.entity.UserTravelItinerary;
import org.triple.backend.travel.exception.TravelItineraryErrorCode;
import org.triple.backend.travel.exception.UserTravelItineraryErrorCode;
import org.triple.backend.travel.presentation.dto.res.TravelItineraryInfoResponseDto;
import org.triple.backend.travel.presentation.dto.res.TravelItineraryCursorResponseDto.TravelSummaryDto;
import org.triple.backend.travel.repository.TravelItineraryJpaRepository;
import org.triple.backend.travel.repository.TravelReviewImageJpaRepository;
import org.triple.backend.travel.repository.TravelReviewJpaRepository;
import org.triple.backend.travel.repository.UserTravelItineraryJpaRepository;

import java.util.List;

@Service
@RequiredArgsConstructor
public class TravelReader {
private final TravelItineraryJpaRepository travelItineraryJpaRepository;
private final TravelReviewJpaRepository travelReviewJpaRepository;
private final TravelReviewImageJpaRepository travelReviewImageJpaRepository;
private final UserTravelItineraryJpaRepository userTravelItineraryJpaRepository;

public long countGroupTravels(final Long groupId) {
return travelItineraryJpaRepository.countByGroupId(groupId);
}

public List<TravelSummaryDto> readGroupTravels(final Long groupId, final int size, final Long cursor) {
return travelItineraryJpaRepository.findGroupTravels(groupId, size, cursor);
}

public TravelItineraryInfoResponseDto getTravelInfo(final TravelItinerary travelItinerary) {
List<UserTravelItinerary> members = userTravelItineraryJpaRepository.findAllByTravelItineraryId(travelItinerary.getId());
return TravelItineraryInfoResponseDto.of(travelItinerary, members);
}

public TravelItinerary findTravel(final Long travelId) {
return travelItineraryJpaRepository.findById(travelId)
.orElseThrow(() -> new BusinessException(TravelItineraryErrorCode.TRAVEL_NOT_FOUND));
}

public UserTravelItinerary findUserTravelByStatus(
final Long userId,
final Long travelId,
final UserRole userRole
) {
if (userRole == null) {
return userTravelItineraryJpaRepository.findByUserIdAndTravelItineraryId(userId, travelId)
.orElseThrow(() -> new BusinessException(UserTravelItineraryErrorCode.USER_TRAVEL_ITINERARY_NOT_FOUND));
}
return userTravelItineraryJpaRepository.findByUserIdAndTravelItineraryIdAndUserRole(userId, travelId, userRole)
.orElseThrow(() -> new BusinessException(TravelItineraryErrorCode.USER_TRAVEL_NOT_FOUND));
Comment on lines +53 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Inconsistent error codes are used for the same logical failure (user not found in travel). Line 54 throws UserTravelItineraryErrorCode.USER_TRAVEL_ITINERARY_NOT_FOUND while line 57 throws TravelItineraryErrorCode.USER_TRAVEL_NOT_FOUND. It is recommended to use a single, consistent error code for these cases to improve maintainability.

}

public TravelItinerary findTravelForUpdate(Long travelId) {
return travelItineraryJpaRepository.findByIdForUpdate(travelId)
.orElseThrow(() -> new BusinessException(TravelItineraryErrorCode.TRAVEL_NOT_FOUND));
}
}
Loading
Loading