Skip to content
Open
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
@@ -0,0 +1,153 @@
package com.ssafy.pookie.game.chat.filter;

import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

@Component
public class BurstAllowedChatFilter {

private static final int BURST_LIMIT = 3;
private static final long COOLDOWN_MS = 1000;
private static final long BURST_RESET_MS = 5000;
private static final int MAX_MESSAGE_LENGTH = 200;
private static final long CLEANUP_INTERVAL_MS = 60000;

private static final Pattern SPAM_PATTERN = Pattern.compile("^(.)\\1{9,}$");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("^\\s*$");
private static final Pattern ALLOWED_REACTIONS = Pattern.compile("^[ㅋㅎㅠㄷㄱㅇ와헉]{1,10}$");

private final ConcurrentMap<String, UserChatState> userStates = new ConcurrentHashMap<>();
private volatile long lastCleanup = System.currentTimeMillis();

public boolean canSend(String userId, String message) {
if (!isValidInput(userId, message)) {
return false;
}

if (isSpamMessage(message)) {
return false;
}

performPeriodicCleanup();

UserChatState userState = getUserState(userId);
return userState.canSendMessage(message);
}

private boolean isValidInput(String userId, String message) {
return userId != null && !userId.trim().isEmpty() &&
message != null && !WHITESPACE_PATTERN.matcher(message).matches() &&
message.length() <= MAX_MESSAGE_LENGTH;
}

private boolean isSpamMessage(String message) {
// 단순하게: 스팸 패턴만 체크 (길이 10개 이상 동일 문자 반복)
return SPAM_PATTERN.matcher(message).matches();
}

private UserChatState getUserState(String userId) {
return userStates.computeIfAbsent(userId, k -> new UserChatState());
}

private void performPeriodicCleanup() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastCleanup > CLEANUP_INTERVAL_MS) {
cleanupOldUsers(currentTime);
lastCleanup = currentTime;
}
}

private void cleanupOldUsers(long currentTime) {
userStates.entrySet().removeIf(entry ->
currentTime - entry.getValue().getLastActivity() > BURST_RESET_MS * 2
);
}

private static class UserChatState {
private int burstCount = 0;
private long lastMessageTime = 0;
private long burstStartTime = 0;
private String lastMessage = "";
private long lastActivity = System.currentTimeMillis();

public synchronized boolean canSendMessage(String message) {
long currentTime = System.currentTimeMillis();
lastActivity = currentTime;

if (isRepeatedMessage(message)) {
return false;
}

if (shouldResetBurst(currentTime)) {
resetBurst(currentTime);
}

// 감정 표현은 버스트 카운트에서 제외
boolean isEmotionalExpression = isEmotionalExpression(message);

if (burstCount < BURST_LIMIT || isEmotionalExpression) {
recordMessage(message, currentTime, !isEmotionalExpression);
return true;
}

if (isInCooldown(currentTime)) {
return false;
}

recordMessage(message, currentTime, true);
return true;
}

private boolean isRepeatedMessage(String message) {
// 감정 표현은 반복 허용
if (message.equals("ㅋㅋㅋ") || message.equals("ㅠㅠ") || message.equals("ㅎㅎ") ||
message.equals("ㄷㄷ") || message.equals("ㄱㄱ") || message.equals("ㅇㅇ") ||
message.equals("와") || message.equals("헉")) {
return false;
}

return message.equals(lastMessage);
}

private boolean shouldResetBurst(long currentTime) {
return burstStartTime > 0 && (currentTime - burstStartTime) > BURST_RESET_MS;
}

private void resetBurst(long currentTime) {
burstCount = 0;
burstStartTime = currentTime;
}

private boolean isInCooldown(long currentTime) {
return lastMessageTime > 0 && (currentTime - lastMessageTime) < COOLDOWN_MS;
}

private boolean isEmotionalExpression(String message) {
return message.equals("ㅋㅋㅋ") || message.equals("ㅠㅠ") || message.equals("ㅎㅎ") ||
message.equals("ㄷㄷ") || message.equals("ㄱㄱ") || message.equals("ㅇㅇ") ||
message.equals("와") || message.equals("헉");
}

private void recordMessage(String message, long currentTime, boolean countToBurst) {
if (countToBurst && burstCount == 0) {
burstStartTime = currentTime;
}
if (countToBurst) {
burstCount++;
}
lastMessageTime = currentTime;
lastMessage = message;
}

private void recordMessage(String message, long currentTime) {
recordMessage(message, currentTime, true);
}

public long getLastActivity() {
return lastActivity;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.ssafy.pookie.inventory.model.InventoryItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
Expand All @@ -10,7 +12,10 @@
@Repository
public interface InventoryItemRepository extends JpaRepository<InventoryItem, Long> {
Optional<InventoryItem> findByUserAccountIdxAndStoreItem_Idx(Long userAccountIdx, Long itemIdx);
List<InventoryItem> findAllByUserAccountIdx(Long userAccountIdx);

@Query("SELECT i FROM InventoryItem i JOIN FETCH i.storeItem WHERE i.userAccountIdx = :userAccountIdx")
List<InventoryItem> findAllByUserAccountIdx(@Param("userAccountIdx") Long userAccountIdx);

Optional<InventoryItem> findByUserAccountIdxAndIdx(Long userAccountIdx, Long inventoryIdx);

}
Loading