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 @@ -11,9 +11,12 @@
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -32,14 +35,22 @@ public CategoryResponse createCategory(CategoryCreateRequest request) {

String categoryName = request.name();

// 이름 중복 검사
// 이름 중복 검사 (동시 요청 시 이 체크를 통과해도 DB 유니크 제약으로 최종 방어됨 - 아래 catch 참고)
if (categoryRepository.existsByName(categoryName)) {
throw new BusinessException(ProblemErrorCode.CATEGORY_NAME_DUPLICATED);
}

ProblemCategory savedCategory = categoryRepository.save(ProblemCategory.create(categoryName));

return categoryMapper.toResponse(savedCategory);
try {
ProblemCategory savedCategory = categoryRepository.saveAndFlush(
ProblemCategory.create(categoryName));
return categoryMapper.toResponse(savedCategory);
} catch (DataIntegrityViolationException e) {
if (isDuplicateNameViolation(e)) {
log.warn("[ProblemCategoryService] 카테고리 이름 중복 제약 조건 위반 발생");
throw new BusinessException(ProblemErrorCode.CATEGORY_NAME_DUPLICATED);
}
throw e;
}
}

/**
Expand Down Expand Up @@ -73,5 +84,15 @@ public CategoryResponse updateCategory(UUID categoryId, CategoryUpdateRequest re
return categoryMapper.toResponse(category);
}


/**
* DB 제약 조건 예외(DataIntegrityViolationException)가 카테고리 이름 유니크 제약(UQ_PROBLEM_CATEGORY_NAME) 위반인지 판단합니다.
*/
private boolean isDuplicateNameViolation(DataIntegrityViolationException e) {
Throwable cause = e.getMostSpecificCause();
String message = cause.getMessage();
if (message == null) {
return false;
}
return message.toLowerCase().contains("uq_problem_category_name");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public enum RoomProblemErrorCode implements ErrorCode {
NOT_FOUND(7001, "NOT_FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 방 문제입니다."),
ROOM_NOT_STARTED(7002, "ROOM_NOT_STARTED", HttpStatus.FORBIDDEN, "아직 시작되지 않은 시험입니다."),
ROOM_ACCESS_DENIED(7003, "ROOM_ACCESS_DENIED", HttpStatus.FORBIDDEN, "해당 방에 접근 권한이 없습니다."),
DUPLICATE_AI_REQUEST(7004, "DUPLICATE_AI_REQUEST", HttpStatus.CONFLICT, "이미 처리 중인 요청입니다. 잠시 후 다시 시도해주세요.");
DUPLICATE_AI_REQUEST(7004, "DUPLICATE_AI_REQUEST", HttpStatus.CONFLICT, "이미 처리 중인 요청입니다. 잠시 후 다시 시도해주세요."),
DUPLICATE_PROBLEM_ORDER(7005, "DUPLICATE_PROBLEM_ORDER", HttpStatus.CONFLICT, "이미 사용 중인 문제 순번입니다.");

private final int numeric;
private final String errorKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand Down Expand Up @@ -81,9 +84,16 @@ public RoomProblemResponse createRoomProblem(
room, category, request.problemOrder(), request.name(),
request.content(), request.explanation(), request.correctAnswer());

RoomProblem saved = roomProblemRepository.save(roomProblem);

return roomProblemMapper.toResponse(saved);
try {
RoomProblem saved = roomProblemRepository.saveAndFlush(roomProblem);
return roomProblemMapper.toResponse(saved);
} catch (DataIntegrityViolationException e) {
if (isDuplicateOrderViolation(e)) {
log.warn("[RoomProblemService] 방 문제 순번 중복 제약 조건 위반 발생");
throw new BusinessException(RoomProblemErrorCode.DUPLICATE_PROBLEM_ORDER);
}
throw e;
}
}

/**
Expand Down Expand Up @@ -119,7 +129,9 @@ public RoomProblemResponse updateRoomProblem(
@Transactional
public void deleteRoomProblem(UUID userId, UUID roomId, UUID roomProblemId) {

Room room = getRoom(roomId);
// 같은 방에 대한 동시 삭제 요청을 직렬화하기 위해 비관적 락으로 조회 (createRoomProblemsByAi 채번과 동일 패턴)
Room room = roomRepository.findByIdForUpdate(roomId)
.orElseThrow(() -> new BusinessException(RoomErrorCode.ROOM_NOT_FOUND));

validateAdmin(userId, room);

Expand Down Expand Up @@ -210,4 +222,16 @@ private void validateAdmin(UUID userId, Room room) {
throw new BusinessException(SpaceErrorCode.NOT_SPACE_ADMIN);
}
}

/**
* DB 제약 조건 예외(DataIntegrityViolationException)가 방 문제 순번 유니크 제약(UQ_ROOM_PROBLEM_ROOM_ORDER) 위반인지 판단합니다.
*/
private boolean isDuplicateOrderViolation(DataIntegrityViolationException e) {
Throwable cause = e.getMostSpecificCause();
String message = cause.getMessage();
if (message == null) {
return false;
}
return message.toLowerCase().contains("uq_room_problem_room_order");
}
}