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 @@ -4,11 +4,14 @@
import endolphin.backend.domain.discussion.entity.DiscussionParticipant;
import endolphin.backend.domain.user.dto.UserIdNameDto;
import endolphin.backend.domain.user.entity.User;
import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
Expand Down Expand Up @@ -116,4 +119,21 @@ Page<Discussion> findFinishedDiscussions(@Param("userId") Long userId,
"WHERE d.discussionStatus = 'ONGOING' " +
"AND dp.user.id IN :userIds")
List<Object[]> findOffsetsByUserIds(@Param("userIds") List<Long> userIds);

@Modifying
@Query("DELETE "
+ "FROM DiscussionParticipant dp "
+ "WHERE dp.discussion.id = :discussionId "
+ "AND dp.user.id = :userId")
void deleteByDiscussionIdAndUserId(
@Param("discussionId") Long discussionId,
@Param("userId") Long userId);

@Query("SELECT dp.userOffset "
+ "FROM DiscussionParticipant dp "
+ "WHERE dp.discussion.id = :discussionId "
+ "ORDER BY dp.userOffset ASC")
List<Long> findOffsetsByDiscussionId(
@Param("discussionId") Long discussionId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import endolphin.backend.global.dto.ListResponse;
import endolphin.backend.global.error.exception.ApiException;
import endolphin.backend.global.error.exception.ErrorCode;
import endolphin.backend.global.redis.DiscussionBitmapService;
import endolphin.backend.global.util.TimeUtil;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -34,37 +35,34 @@
@Transactional
public class DiscussionParticipantService {

private static final int MAX_PARTICIPANT = 15;

private final DiscussionParticipantRepository discussionParticipantRepository;
private final UserService userService;
private static final int MAX_PARTICIPANT = 15;
private final SharedEventService sharedEventService;

public void addDiscussionParticipant(Discussion discussion, User user) {
Long offset = discussionParticipantRepository.findMaxOffsetByDiscussionId(
List<Long> offsets = discussionParticipantRepository.findOffsetsByDiscussionId(
discussion.getId());

offset += 1;

if (offset >= MAX_PARTICIPANT) {
if (offsets.size() >= MAX_PARTICIPANT) {
throw new ApiException(ErrorCode.DISCUSSION_PARTICIPANT_EXCEED_LIMIT);
}

DiscussionParticipant participant;
if (offset == 0) {
participant = DiscussionParticipant.builder()
.discussion(discussion)
.user(user)
.isHost(true)
.userOffset(offset)
.build();
} else {
participant = DiscussionParticipant.builder()
.discussion(discussion)
.user(user)
.isHost(false)
.userOffset(offset)
.build();
long offset = 0L;
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3; offsets가 오름차순으로 정렬되어있는데 contains를 여러번 수행하지 않고 offsets에 대해 반복을 수행하며 찾는건 어떤가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋은 아이디어 감사합니다

Copy link
Contributor Author

Choose a reason for hiding this comment

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

수정하였습니다!

for (; offset < offsets.size(); ++offset) {
if (offsets.get((int) offset) != offset)
break;
}

boolean isHost = (offset == 0);
DiscussionParticipant participant = DiscussionParticipant.builder()
.discussion(discussion)
.user(user)
.isHost(isHost)
.userOffset(offset)
.build();

discussionParticipantRepository.save(participant);
}

Expand Down Expand Up @@ -320,4 +318,8 @@ public Map<Long, Map<Discussion, Long>> getOngoingDiscussionOffsetsByUserIds(
)
);
}

public void deleteDiscussionParticipant(Long discussionId, Long userId) {
discussionParticipantRepository.deleteByDiscussionIdAndUserId(discussionId, userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

import endolphin.backend.domain.discussion.entity.Discussion;
import endolphin.backend.domain.discussion.enums.DiscussionStatus;
import io.lettuce.core.dynamic.annotation.Param;
import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface DiscussionRepository extends JpaRepository<Discussion, Long> {

List<Discussion> findByDiscussionStatusNot(DiscussionStatus discussionStatus);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT d FROM Discussion d WHERE d.id = :discussionId")
Optional<Discussion> findByIdForUpdate(@Param("discussionId") Long discussionId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ public Discussion getDiscussionById(Long discussionId) {
}

public JoinDiscussionResponse joinDiscussion(Long discussionId, JoinDiscussionRequest request) {
Discussion discussion = getDiscussionById(discussionId);
Discussion discussion = discussionRepository.findByIdForUpdate(discussionId)
.orElseThrow(() -> new ApiException(ErrorCode.DISCUSSION_NOT_FOUND));

if (discussion.getDiscussionStatus() != DiscussionStatus.ONGOING) {
throw new ApiException(ErrorCode.DISCUSSION_NOT_ONGOING);
Expand Down Expand Up @@ -395,6 +396,17 @@ public DiscussionResponse updateDiscussion(Long discussionId, UpdateDiscussionRe
);
}

public void exitDiscussion(Long discussionId, Long userId) {
Discussion discussion = discussionRepository.findByIdForUpdate(discussionId)
.orElseThrow(() -> new ApiException(ErrorCode.DISCUSSION_NOT_FOUND));

Long offset = discussionParticipantService.getDiscussionParticipantOffset(discussionId, userId);

discussionBitmapService.deleteUsersFromDiscussion(discussionId, offset);

discussionParticipantService.deleteDiscussionParticipant(discussionId, userId);
}

private boolean isTimeChanged(Discussion discussion, UpdateDiscussionRequest request) {
return !discussion.getDateRangeStart().equals(request.dateRangeStart())
|| !discussion.getDateRangeEnd().equals(request.dateRangeEnd())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package endolphin.backend.global.redis;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.RedisCommands;
import org.springframework.data.redis.connection.RedisCommandsProvider;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisKeyCommands;
import org.springframework.data.redis.connection.RedisSetCommands;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisCommand;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

Expand Down Expand Up @@ -168,4 +177,36 @@ public Map<Long, byte[]> getDataOfDiscussionId(Long discussionId) {
return map;
});
}

public void deleteUsersFromDiscussion(Long discussionId, long bitOffset) {
String pattern = discussionId + ":*";
ScanOptions scanOptions = ScanOptions.scanOptions().match(pattern).count(1000).build();

List<byte[]> keys = redisTemplate.execute((RedisConnection connection) -> {
List<byte[]> k = new ArrayList<>();
RedisKeyCommands keyCommands = connection.keyCommands();
try (Cursor<byte[]> cursor = keyCommands.scan(scanOptions)) {
while (cursor.hasNext()) {
k.add(cursor.next());
}
}
return k;
});

if (keys == null || keys.isEmpty()) {
return;
}

redisTemplate.executePipelined(new RedisCallback<Object>() {

@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
RedisCommands redisCommands = connection.commands();
for (byte[] key : keys) {
redisCommands.setBit(key, bitOffset, false);
}
return null;
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -79,8 +80,8 @@ void addDiscussionParticipant_ShouldAddSuccessfully() {
.picture("profile.jpg")
.build();

given(discussionParticipantRepository.findMaxOffsetByDiscussionId(discussion.getId()))
.willReturn(0L);
given(discussionParticipantRepository.findOffsetsByDiscussionId(discussion.getId()))
.willReturn(new ArrayList<>());

// When
discussionParticipantService.addDiscussionParticipant(discussion, user);
Expand Down Expand Up @@ -109,8 +110,8 @@ void addDiscussionParticipant_ShouldThrowExceptionWhenExceedLimit() {
.picture("profile.jpg")
.build();

given(discussionParticipantRepository.findMaxOffsetByDiscussionId(discussion.getId()))
.willReturn(15L); // 최대 인원 초과 상황
given(discussionParticipantRepository.findOffsetsByDiscussionId(discussion.getId()))
.willReturn(List.of(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L)); // 최대 인원 초과 상황

// When & Then
ApiException exception = assertThrows(ApiException.class, () -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ public void joinDiscussion_withCorrectPassword_returnsTrue() {
User currentUser = new User();
ReflectionTestUtils.setField(currentUser, "id", 1L);

when(discussionRepository.findById(discussionId)).thenReturn(Optional.of(discussion));
when(discussionRepository.findByIdForUpdate(discussionId)).thenReturn(Optional.of(discussion));
when(userService.getCurrentUser()).thenReturn(currentUser);
when(passwordCountService.tryEnter(currentUser.getId(), discussion, correctPassword)).thenReturn(0);

Expand Down Expand Up @@ -629,7 +629,7 @@ public void joinDiscussion_withIncorrectPassword_returnsFalse() {
User currentUser = new User();
ReflectionTestUtils.setField(currentUser, "id", 1L);

when(discussionRepository.findById(discussionId)).thenReturn(Optional.of(discussion));
when(discussionRepository.findByIdForUpdate(discussionId)).thenReturn(Optional.of(discussion));
when(userService.getCurrentUser()).thenReturn(currentUser);
when(passwordCountService.tryEnter(currentUser.getId(), discussion, incorrectPassword)).thenReturn(1);

Expand All @@ -656,7 +656,7 @@ public void joinDiscussion_withNullPassword_throwsApiException() {
User currentUser = new User();
ReflectionTestUtils.setField(currentUser, "id", 1L);

when(discussionRepository.findById(discussionId)).thenReturn(Optional.of(discussion));
when(discussionRepository.findByIdForUpdate(discussionId)).thenReturn(Optional.of(discussion));
when(userService.getCurrentUser()).thenReturn(currentUser);

// 비밀번호가 null인 경우, passwordCountService.tryEnter가 예외를 던지도록 모킹
Expand All @@ -675,7 +675,7 @@ public void joinDiscussion_withNullPassword_throwsApiException() {
public void joinDiscussion_withInvalidDiscussionId_throwsApiException() {
// Given
Long discussionId = 999L;
when(discussionRepository.findById(discussionId)).thenReturn(Optional.empty());
when(discussionRepository.findByIdForUpdate(discussionId)).thenReturn(Optional.empty());

// When & Then
assertThatThrownBy(() ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,28 @@ public void testGetDataOfDiscussionId() {
BitSet bs = BitSet.valueOf(data);
assertThat(bs.get(5)).as("Offset 5의 비트는 true여야 합니다").isTrue();
}

@Test
@DisplayName("참여자 비트 전체 삭제 테스트")
public void testSetUserBitsFalse() {
// given
Long discussionId = 100L;
long bitOffset = 3L;
long minuteKey = TimeUtil.convertToMinute(LocalDateTime.now());
bitmapService.initializeBitmap(discussionId, minuteKey);
bitmapService.initializeBitmap(discussionId, minuteKey + 30);

bitmapService.setBitValue(discussionId, minuteKey, bitOffset, true);
bitmapService.setBitValue(discussionId, minuteKey + 30, bitOffset, true);

// when
bitmapService.deleteUsersFromDiscussion(discussionId, bitOffset);

// then
boolean data = bitmapService.getBitValue(discussionId, minuteKey, bitOffset);
assertThat(data).isFalse();

data = bitmapService.getBitValue(discussionId, minuteKey + 30, bitOffset);
assertThat(data).isFalse();
}
}
Loading