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
Expand Up @@ -120,7 +120,7 @@ public List<DirectChatRoomCreateDTO.MemberInfo> toMemberInfo(List<ChatRoomMember
return members.stream()
.map(m -> DirectChatRoomCreateDTO.MemberInfo.builder()
.memberId(m.getMember().getId())
.memberName(m.getMember().getMemberName())
.memberName(m.getMember().getDisplayName())
.build()
).collect(Collectors.toList());
}
Expand Down Expand Up @@ -154,7 +154,7 @@ public ChatCommonDTO.MessageInfo toCommonMessageInfo(
boolean shouldAnonymizeSender = !isSystemMessage && (isSenderWithdrawn || sender == null);
String senderName = isSystemMessage
? SYSTEM_USER_NAME
: shouldAnonymizeSender ? UNKNOWN_USER_NAME : sender.getMemberName();
: shouldAnonymizeSender ? UNKNOWN_USER_NAME : sender.getDisplayName();
String displayProfileImageUrl = isSystemMessage || shouldAnonymizeSender ? null : senderProfileImageUrl;

return ChatCommonDTO.MessageInfo.builder()
Expand Down Expand Up @@ -194,7 +194,7 @@ public ChatRoomDetailDTO.MemberInfo toChatRoomDetailMemberInfo(Member member, St
boolean isWithdrawn = member == null || member.isWithdrawn();
return ChatRoomDetailDTO.MemberInfo.builder()
.memberId(isWithdrawn ? null : member.getId())
.memberName(isWithdrawn ? UNKNOWN_USER_NAME : member.getMemberName())
.memberName(isWithdrawn ? UNKNOWN_USER_NAME : member.getDisplayName())
.profileImgUrl(isWithdrawn ? null : memberProfileImgUrl)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public WebSocketMessageDTO.MessageResponse toSendMessageResponse(
.messageType(savedMessage.getType())
.images(files)
.senderId(sender.getId())
.senderName(sender.getMemberName())
.senderName(sender.getDisplayName())
.senderProfileImageUrl(senderProfileImageUrl)
.timestamp(savedMessage.getCreatedAt())
.unreadCount(unreadCount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ public DirectChatRoomCreateDTO.Response createDirectChatRoom(Long memberId, Long
if (existingRoom.isPresent()) {
List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(existingRoom.get().getId());
log.info("===이미 존재하는 채팅방===");
return chatConverter.toDirectChatRoomCreateDTO(existingRoom.get(), members, target.getMemberName());
return chatConverter.toDirectChatRoomCreateDTO(existingRoom.get(), members, target.getDisplayName());
}

ChatRoom newRoom = ChatRoom.createDirectChatRoom();
chatRoomRepository.save(newRoom);

ChatRoomMember member1 = ChatRoomMember.createJoined(newRoom, me, target.getMemberName());
ChatRoomMember member2 = ChatRoomMember.createPending(newRoom, target, me.getMemberName());
ChatRoomMember member1 = ChatRoomMember.createJoined(newRoom, me, target.getDisplayName());
ChatRoomMember member2 = ChatRoomMember.createPending(newRoom, target, me.getDisplayName());
chatRoomMemberRepository.saveAll(List.of(member1, member2));

log.info("[개인 채팅방 생성 완료] chatRoomId: {}, sender: {}, receiver: {}", newRoom.getId(), memberId, targetMemberId);
return chatConverter.toDirectChatRoomCreateDTO(newRoom, List.of(member1, member2), target.getMemberName());
return chatConverter.toDirectChatRoomCreateDTO(newRoom, List.of(member1, member2), target.getDisplayName());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private ChatRoomDetailDTO.ChatRoomInfo buildChatRoomInfo(ChatRoom chatRoom, Chat
Member member = counterPart.getMember();
isCounterPartWithdrawn = isUnavailableMember(member);

displayName = isCounterPartWithdrawn ? ChatConverter.UNKNOWN_USER_NAME : member.getMemberName();
displayName = isCounterPartWithdrawn ? ChatConverter.UNKNOWN_USER_NAME : member.getDisplayName();
profileImageUrl = isCounterPartWithdrawn
? null
: imageUrlResolver.resolve(member.getProfileImg(), ProfileImg::getImgKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public void publishChatNotificationEvent(
List<Long> activeSubscribers) {
String notificationTitle = chatRoom.getType() == ChatRoomType.PARTY
? chatRoom.getParty().getPartyName()
: sender.getNickname();
: sender.getDisplayName();
String notificationContent = chatRoom.getType() == ChatRoomType.PARTY
? sender.getNickname() + ": " + savedMessage.getDisplayContent()
? sender.getDisplayName() + ": " + savedMessage.getDisplayContent()
: savedMessage.getDisplayContent();

eventPublisher.publishEvent(ChatNotificationEvent.create(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package umc.cockple.demo.domain.exercise.service.command;

import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import umc.cockple.demo.domain.exercise.domain.Exercise;
Expand All @@ -10,7 +11,9 @@
import umc.cockple.demo.domain.exercise.service.command.model.ExerciseGameHostChangeCommand;
import umc.cockple.demo.domain.exercise.service.command.result.ExerciseGameHostChangeResult;
import umc.cockple.demo.domain.exercise.service.support.reader.ExerciseReader;
import umc.cockple.demo.domain.game.events.GameHostAssignedEvent;
import umc.cockple.demo.domain.member.service.query.lookup.MemberPartyLookupService;
import umc.cockple.demo.domain.party.domain.Party;

@Service
@Transactional
Expand All @@ -20,6 +23,7 @@ public class ExerciseGameHostCommandService {
private final ExerciseReader exerciseReader;
private final ExerciseValidator exerciseValidator;
private final MemberPartyLookupService memberPartyLookupService;
private final ApplicationEventPublisher eventPublisher;

public ExerciseGameHostChangeResult changeGameHost(
Long exerciseId,
Expand All @@ -35,7 +39,22 @@ public ExerciseGameHostChangeResult changeGameHost(
ExerciseErrorCode.INVALID_GAME_HOST_CANDIDATE));

exercise.changeGameHost(command.participantId());
publishGameHostAssigned(exercise, command.participantId());

return new ExerciseGameHostChangeResult(exercise.getId(), exercise.getGameHostId());
}

/**
* 새로 지정된 게임 진행자 본인에게 게임판 알림 발송
*/
private void publishGameHostAssigned(Exercise exercise, Long gameHostMemberId) {
Party party = exercise.getParty();
eventPublisher.publishEvent(GameHostAssignedEvent.assigned(
exercise.getGameBoard().getId(),
party.getId(),
party.getPartyName(),
party.getPartyImg() != null ? party.getPartyImg().getImgKey() : null,
gameHostMemberId
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public ExerciseCancelResult cancelParticipation(Long exerciseId, Long memberId)
log.info("운동 참여 취소 완료 - exerciseId: {}, memberId: {}, 현재 참여자 수: {}",
exercise.getId(), member.getId(), exercise.getNowCapacity());

return new ExerciseCancelResult(member.getMemberName(), exercise.getNowCapacity());
return new ExerciseCancelResult(member.getDisplayName(), exercise.getNowCapacity());
}

public ExerciseCancelResult cancelParticipationByManager(
Expand Down Expand Up @@ -195,7 +195,7 @@ private ExerciseCancelResult cancelMemberParticipation(Exercise exercise, Long p
memberExerciseRepository.delete(memberExercise);
publishAttendanceChangedEvent(exercise, participant.getId());

return new ExerciseCancelResult(participant.getMemberName(), exercise.getNowCapacity());
return new ExerciseCancelResult(participant.getDisplayName(), exercise.getNowCapacity());
}

private void publishAttendanceChangedEvent(Exercise exercise, Long subjectMemberId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private ExerciseGameHostResult.Participant toParticipant(
imageUrlResolver.resolve(member.getProfileImg(), ProfileImg::getImgKey),
memberParty.getRole(),
member.getId().equals(gameHostId),
member.getMemberName(),
member.getDisplayName(),
member.getGender(),
member.getLevel(),
lastExerciseDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public ExerciseMyGuestListResult getMyInvitedGuests(Long exerciseId, Long member
participantPositionCalculator.calculate(allParticipants, exercise.getMaxCapacity());
Map<Long, ExerciseParticipantPosition> guestPositionById = indexGuestPositions(participantPositions);

String inviterName = member.getMemberName();
String inviterName = member.getDisplayName();
List<ExerciseMyGuestListResult.GuestInfo> guestInfoList =
buildGuestInfoList(myGuests, guestPositionById, inviterName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private ExerciseParticipantSnapshot toParticipantSnapshot(
return new ExerciseParticipantSnapshot(
member.getId(),
imageUrlResolver.resolve(member.getProfileImg(), ProfileImg::getImgKey),
member.getMemberName(),
member.getDisplayName(),
member.getGender(),
member.getLevel(),
memberExercise.getExerciseMemberShipStatus(),
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/umc/cockple/demo/domain/game/domain/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public class Game extends BaseEntity {

private LocalDateTime completedAt; // 게임 완료 시각

// 자동 완료 스케줄러와 사용자 요청(완료/이동/삭제)이 같은 게임을 동시에 바꾸는 것을 막는 낙관적 락.
@Version
@Column(nullable = false)
private Long version;

@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<GamePlayer> players = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public static GameBoardMember create(String name, Gender gender, Level level, Ag
public static GameBoardMember createFromMember(Member member, LocalDate exerciseDate) {
return GameBoardMember.builder()
.member(member)
.name(member.getMemberName())
.name(member.getDisplayName())
.gender(member.getGender())
.level(member.getLevel())
.ageGroup(AgeGroup.fromBirthDate(member.getBirth(), exerciseDate))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,23 @@ public List<GameBoardMember> filterAvailable(
.toList();
}

public boolean hasBlockedMember(
/**
* 수동 게임 편성 정책
*/
public boolean hasWaitingConflict(
List<GameBoardMember> members,
List<Game> activeGames,
LocalDateTime now) {
Set<Long> blockedMemberIds = blockedMemberIds(activeGames, now);
return members.stream().anyMatch(member -> blockedMemberIds.contains(member.getId()));
List<Game> activeGames) {
Set<Long> waitingMemberIds = new HashSet<>();
for (Game game : activeGames) {
if (game.getStatus() != GameStatus.WAITING) {
continue;
}
game.getPlayers().stream()
.map(GamePlayer::getGameBoardMember)
.map(GameBoardMember::getId)
.forEach(waitingMemberIds::add);
}
return members.stream().anyMatch(member -> waitingMemberIds.contains(member.getId()));
}

private Set<Long> blockedMemberIds(List<Game> activeGames, LocalDateTime now) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package umc.cockple.demo.domain.game.events;

import java.time.LocalDateTime;
import java.util.UUID;

/**
* 운동(게임판)의 게임 진행자로 특정 회원이 지정되었음을 알리는 이벤트
*
* @param gameBoardId 진행자가 지정된 게임판 (알림 destination resourceId)
* @param partyId 게임판이 속한 모임 ID (레거시 호환용)
* @param partyName 알림 제목으로 사용할 모임 이름
* @param imageKey 알림 이미지 키 (모임 이미지, 없으면 null)
* @param recipientMemberId 알림 수신 대상 회원 ID (새로 지정된 진행자 본인)
*/
public record GameHostAssignedEvent(
Long gameBoardId,
Long partyId,
String partyName,
String imageKey,
Long recipientMemberId,
LocalDateTime occurredAt,
UUID eventId
) {
public static GameHostAssignedEvent assigned(
Long gameBoardId,
Long partyId,
String partyName,
String imageKey,
Long recipientMemberId
) {
return new GameHostAssignedEvent(
gameBoardId, partyId, partyName, imageKey, recipientMemberId,
LocalDateTime.now(), UUID.randomUUID()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package umc.cockple.demo.domain.game.events;

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

/**
* 대기 게임이 코트에 배치되어 시작되었음을 알리는 이벤트
*
* @param gameBoardId 시작된 게임이 속한 게임판
* @param partyId 게임판이 속한 모임 ID
* @param partyName 알림 제목으로 사용할 모임 이름
* @param imageKey 알림 이미지 키
* @param courtName 게임이 배치된 코트 이름
* @param recipientMemberIds 알림 수신 대상 회원 ID
*/
public record GameStartedEvent(
Long gameBoardId,
Long partyId,
String partyName,
String imageKey,
String courtName,
List<Long> recipientMemberIds,
LocalDateTime occurredAt,
UUID eventId
) {
public GameStartedEvent {
recipientMemberIds = List.copyOf(recipientMemberIds);
}

public static GameStartedEvent started(
Long gameBoardId,
Long partyId,
String partyName,
String imageKey,
String courtName,
List<Long> recipientMemberIds
) {
return new GameStartedEvent(
gameBoardId, partyId, partyName, imageKey, courtName,
recipientMemberIds, LocalDateTime.now(), UUID.randomUUID()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public record Response(
List<CourtInfo> courts,
List<WaitingInfo> waitings
) {
public Response forBroadcast() {
return new Response(false, courtCount, courts, waitings);
}
}

public record CourtInfo(
Expand Down Expand Up @@ -41,6 +44,7 @@ public record WaitingInfo(
public record PlayerInfo(
Long gameBoardMemberId,
String name,
String profileImageUrl, // 게스트 등 회원 계정이 없으면 null
String level, // 급수 한글 표기 (예: "준자강", "A조")
int playerOrder
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ private GameBoardDTO.PlayerInfo toPlayerInfo(GameBoardResult.PlayerView player)
return new GameBoardDTO.PlayerInfo(
player.gameBoardMemberId(),
player.name(),
player.profileImageUrl(),
player.level().getKoreanName(),
player.playerOrder());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ private GameBoardDTO.Response loadBoardDto(RealtimeRequestContext context, Long
* 자신의 구독 응답으로 받은 값을 유지하도록 한다.
*/
private GameBoardDTO.Response toBroadcastBoard(GameBoardDTO.Response board) {
return new GameBoardDTO.Response(false, board.courtCount(), board.courts(), board.waitings());
return board.forBroadcast();
}

private Long requireGameBoardId(GameRealtimePayload payload) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ boolean existsByGameBoardMemberIdAndStatusIn(
@Query("select distinct g from Game g " +
"left join fetch g.court " +
"left join fetch g.players p " +
"left join fetch p.gameBoardMember " +
"left join fetch p.gameBoardMember gbm " +
"left join fetch gbm.member m " +
"left join fetch m.profileImg " +
"where g.gameBoard.id = :gameBoardId and g.status in :statuses")
List<Game> findByGameBoardIdAndStatusInWithPlayers(
@Param("gameBoardId") Long gameBoardId,
Expand Down Expand Up @@ -60,4 +62,12 @@ List<Long> findCompletedGameIds(
"left join fetch p.gameBoardMember " +
"where g.id in :ids")
List<Game> findByIdInWithPlayers(@Param("ids") Collection<Long> ids);

@Query("select distinct g from Game g " +
"left join fetch g.players p " +
"left join fetch p.gameBoardMember " +
"where g.status = :status and g.startedAt is not null and g.startedAt < :threshold")
List<Game> findByStatusAndStartedAtBeforeWithPlayers(
@Param("status") GameStatus status,
@Param("threshold") LocalDateTime threshold);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package umc.cockple.demo.domain.game.service.command;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
* 시작 후 일정 시간이 지난 진행 게임을 주기적으로 스캔해 자동 완료 처리
*/
@Component
@RequiredArgsConstructor
@Slf4j
@Profile("!integrationtest")
public class GameAutoCompleteScheduler {

private static final long SCAN_INTERVAL_MS = 60_000L;

private final GameAutoCompleteService gameAutoCompleteService;

@Scheduled(initialDelay = SCAN_INTERVAL_MS, fixedDelay = SCAN_INTERVAL_MS)
public void run() {
try {
gameAutoCompleteService.autoCompleteStaleGames();
} catch (ObjectOptimisticLockingFailureException e) {
log.warn("게임 자동 완료 중 동시 수정 감지 - 이번 주기 롤백, 다음 주기에 재시도합니다.");
} catch (Exception e) {
log.error("게임 자동 완료 스케줄러 실행 실패", e);
}
}
}
Loading
Loading