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
2 changes: 2 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ CREATE TABLE "TBL_ROOM_USER" (
CREATE TABLE "TBL_ROOM_PROBLEM" (
"id" UUID NOT NULL,
"room_id" UUID NOT NULL,
"category_id" UUID NOT NULL,
"problem_order" INTEGER NOT NULL,
"name" VARCHAR(255) NULL,
"content" TEXT NULL,
Expand Down Expand Up @@ -175,6 +176,7 @@ ALTER TABLE "TBL_PROBLEM_COUNTERS" ADD CONSTRAINT "FK_TBL_PROBLEM_TO_TBL_PROBLEM
-- 방 및 독립 시험지 세팅
ALTER TABLE "TBL_ROOM" ADD CONSTRAINT "FK_TBL_SPACE_TO_TBL_ROOM" FOREIGN KEY ("space_id") REFERENCES "TBL_SPACE" ("id");
ALTER TABLE "TBL_ROOM_PROBLEM" ADD CONSTRAINT "FK_TBL_ROOM_TO_TBL_ROOM_PROBLEM" FOREIGN KEY ("room_id") REFERENCES "TBL_ROOM" ("id") ON DELETE CASCADE;
ALTER TABLE "TBL_ROOM_PROBLEM" ADD CONSTRAINT "FK_TBL_PROBLEM_CATEGORY_TO_TBL_ROOM_PROBLEM" FOREIGN KEY ("category_id") REFERENCES "TBL_PROBLEM_CATEGORY" ("id");

-- 방 참여 유저 맵
ALTER TABLE "TBL_ROOM_USER" ADD CONSTRAINT "FK_TBL_ROOM_TO_TBL_ROOM_USER" FOREIGN KEY ("room_id") REFERENCES "TBL_ROOM" ("id") ON DELETE CASCADE;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.momogo.api.report;

import com.momogo.core.domain.report.dto.response.DashboardSummaryResponse;
import com.momogo.core.domain.report.dto.response.MyExamDetailResponse;
import com.momogo.core.domain.report.dto.response.MyExamListItemResponse;
import com.momogo.core.domain.report.dto.response.PersonalReportResponse;
import com.momogo.core.domain.report.dto.response.SingleModeHistoryResponse;
import com.momogo.core.domain.report.dto.response.SingleModeSummaryResponse;
import com.momogo.core.domain.report.dto.response.SpaceRankingResponse;
import com.momogo.core.domain.report.entity.ReportType;
import com.momogo.core.domain.report.service.ReportService;
Expand Down Expand Up @@ -59,4 +64,55 @@ public ResponseEntity<List<SpaceRankingResponse>> getSpaceRanking(
) {
return ResponseEntity.ok(reportService.getSpaceRanking(spaceId));
}

/**
* 메인 대시보드 요약 카드(오늘 푼 문제 수 / 이번 주 정답률 / 참여 완료 시험 수) 실시간 조회
*/
@GetMapping("/summary")
public ResponseEntity<DashboardSummaryResponse> getDashboardSummary(
@AuthenticationPrincipal(expression = "userResponse.id") UUID userId
) {
return ResponseEntity.ok(reportService.getDashboardSummary(userId));
}

/**
* 공간 내 성적 대시보드: 싱글모드 요약(오늘/이번 주 정답 수, 전체 평균 정답률) 조회
*/
@GetMapping("/single/summary")
public ResponseEntity<SingleModeSummaryResponse> getSingleModeSummary(
@AuthenticationPrincipal(expression = "userResponse.id") UUID userId
) {
return ResponseEntity.ok(reportService.getSingleModeSummary(userId));
}

/**
* 공간 내 성적 대시보드: 싱글모드 전체 풀이 이력 조회
*/
@GetMapping("/single/history")
public ResponseEntity<List<SingleModeHistoryResponse>> getSingleModeHistory(
@AuthenticationPrincipal(expression = "userResponse.id") UUID userId
) {
return ResponseEntity.ok(reportService.getSingleModeHistory(userId));
}

/**
* 공간 내 성적 대시보드: 내가 응시(제출 완료)한 평가 시험 목록 조회
*/
@GetMapping("/exams")
public ResponseEntity<List<MyExamListItemResponse>> getMyExamList(
@AuthenticationPrincipal(expression = "userResponse.id") UUID userId
) {
return ResponseEntity.ok(reportService.getMyExamList(userId));
}

/**
* 공간 내 성적 대시보드: 내가 응시한 평가 시험의 문항별 채점 결과(리뷰) 조회
*/
@GetMapping("/exams/{roomId}")
public ResponseEntity<MyExamDetailResponse> getMyExamDetail(
@AuthenticationPrincipal(expression = "userResponse.id") UUID userId,
@PathVariable UUID roomId
) {
return ResponseEntity.ok(reportService.getMyExamDetail(userId, roomId));
}
}
18 changes: 18 additions & 0 deletions momogo-api/src/main/java/com/momogo/api/room/RoomController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.momogo.api.room;

import com.momogo.core.domain.problem.dto.response.GeneratedProblemData;
import com.momogo.core.domain.room.dto.request.RoomAnswerSubmitRequest;
import com.momogo.core.domain.room.dto.request.RoomCreateRequest;
import com.momogo.core.domain.room.dto.request.RoomProblemDraftAiRequest;
import com.momogo.core.domain.room.dto.response.RoomProblemResponse;
import com.momogo.core.domain.room.dto.response.RoomReportResponse;
import com.momogo.core.domain.room.dto.response.RoomResponse;
Expand Down Expand Up @@ -49,6 +51,22 @@ public ResponseEntity<RoomResponse> createRoom(
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

/**
* 시험방 생성 마법사(2단계) 미리보기용 AI 문제 생성 (ADMIN 전용)
* 방이 아직 만들어지기 전 단계이므로 DB에는 저장하지 않고 생성 결과만 반환한다.
* @param spaceId 공간 아이디
* @param request 참고자료/문항 수 요청 DTO
* @return 생성된 문제 초안 목록 (미저장)
*/
@PostMapping("/spaces/{spaceId}/rooms/ai-draft-problems")
public ResponseEntity<List<GeneratedProblemData>> generateDraftProblems(
@AuthenticationPrincipal(expression = "userResponse.id") UUID userId,
@PathVariable UUID spaceId,
@Valid @RequestBody RoomProblemDraftAiRequest request
) {
return ResponseEntity.ok(roomService.generateDraftProblems(userId, spaceId, request));
}

/**
* 평가 시험 상세 조회
* @param userId 유저 아이디
Expand Down
14 changes: 14 additions & 0 deletions momogo-api/src/main/java/com/momogo/api/user/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.momogo.core.domain.user.service.UserService;
import jakarta.validation.Valid;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -45,6 +46,19 @@ public ResponseEntity<UserResponse> createUser(
return ResponseEntity.status(HttpStatus.CREATED).body(userResponse);
}

/**
* 요청 유저와 같은 공간에 소속된 전체 유저 목록을 조회합니다. (응시 대상자 지정 등에 사용)
*
* @param userId 인증 객체에서 추출한 요청 유저 식별자
* @return 같은 공간에 소속된 유저 목록 DTO
*/
@GetMapping
public ResponseEntity<List<UserResponse>> findUsersInMySpace(
@AuthenticationPrincipal(expression = "userResponse.id") UUID userId
) {
return ResponseEntity.ok(userService.findUsersInMySpace(userId));
}

/**
* 로그인된 현재 유저의 상세 정보를 조회합니다.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

/*
* 다른 도메인에서 발행한 이벤트를 받아서 알림을 저장하고 SSE로 실시간 전송하는 리스너.
*
* AFTER_COMMIT 단계는 원본 트랜잭션이 이미 커밋을 마친 뒤(스레드에 바인딩된 트랜잭션 리소스가
* 정리되는 시점) 실행되므로, 이 시점에서 수행하는 DB 쓰기는 REQUIRES_NEW로 완전히 새로운
* 트랜잭션을 열어야 한다. 그렇지 않으면 save()가 예외 없이 반환되어도(엔티티에 ID까지 채워진
* 상태로) 실제로는 어떤 트랜잭션에도 커밋되지 않아 DB에 반영되지 않는 문제가 발생한다.
*/
@Slf4j
@Component
Expand All @@ -37,6 +44,7 @@ public class NotificationEventListener {

// 평가 시험(방) 생성 -> 대상 유저 전원에게 알림
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleRoomCreated(RoomCreatedEvent event) {
Map<UUID, User> userMap = userRepository.findAllById(event.userIds()).stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
Expand All @@ -54,6 +62,7 @@ public void handleRoomCreated(RoomCreatedEvent event) {

// 공간 내 권한 변경 -> 변경된 본인에게 알림
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleUserRoleChanged(SpaceUserRoleChangedEvent event) {
User receiver = userRepository.findById(event.targetUserId()).orElse(null);
if (receiver == null) {
Expand All @@ -66,6 +75,7 @@ public void handleUserRoleChanged(SpaceUserRoleChangedEvent event) {

// 공간에 새 유저 가입 -> 그 공간 ADMIN에게 알림
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleUserJoined(SpaceUserJoinedEvent event) {
User receiver = userRepository.findBySpaceIdAndRole(event.spaceId(), UserRole.ADMIN).orElse(null);
if (receiver == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
import com.momogo.core.domain.problem.repository.ProblemRepository;
import com.momogo.core.domain.space.entity.Space;
import com.momogo.core.domain.space.repository.SpaceRepository;
import com.momogo.core.domain.user.entity.UserProblem;
import com.momogo.core.domain.user.entity.UserProblemId;
import com.momogo.core.domain.user.repository.UserProblemRepository;
import com.momogo.core.domain.user.repository.UserRepository;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
Expand Down Expand Up @@ -49,6 +53,10 @@ public class ProblemServiceImpl implements ProblemService {

private final ProblemPersister problemPersister;

private final UserProblemRepository userProblemRepository;

private final UserRepository userRepository;

/**
* 문제 직접 생성
*
Expand Down Expand Up @@ -241,6 +249,21 @@ public ProblemSolveResponse solveProblem(

counters.recordAttempt(isSolved);

// 유저별 문제 풀이 이력 upsert (싱글모드 대시보드/랭킹 집계의 데이터 소스)
UserProblemId userProblemId = new UserProblemId(userId, problemId);
UserProblem userProblem = userProblemRepository.findById(userProblemId).orElse(null);
if (userProblem != null) {
userProblem.updateAttempt(isSolved, request.userAnswer());
} else {
userProblemRepository.save(UserProblem.builder()
.id(userProblemId)
.user(userRepository.getReferenceById(userId))
.problem(problem)
.isSolved(isSolved)
.userAnswer(request.userAnswer())
.build());
}

return new ProblemSolveResponse(
isSolved,
problem.getCorrectAnswer(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.momogo.core.domain.report.dto.response;

/**
* 메인 대시보드 요약 카드 응답 Dto (싱글모드/종합성과/참여시험)
* @param todaySolvedCount 오늘 정답 처리된 문제 수 (싱글모드)
* @param weeklyAccuracyRate 이번 주(월요일~현재) 실시간 평균 정답률(%)
* @param completedExamCount 응시(제출) 완료한 평가 시험방 수
*/
public record DashboardSummaryResponse(
long todaySolvedCount,
double weeklyAccuracyRate,
long completedExamCount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.momogo.core.domain.report.dto.response;

import java.util.List;
import java.util.UUID;

/**
* 내 평가 시험 리뷰(응시 결과 상세) 응답 Dto
* @param roomId 평가 시험방 아이디
* @param roomName 평가 시험방 이름
* @param description 평가 시험방 설명
* @param score 취득 점수
* @param problems 문항별 채점 결과 목록
*/
public record MyExamDetailResponse(
UUID roomId,
String roomName,
String description,
int score,
List<MyExamProblemResultResponse> problems
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.momogo.core.domain.report.dto.response;

import java.time.OffsetDateTime;
import java.util.UUID;

/**
* 내가 응시한 평가 시험 목록 항목 응답 Dto
* @param roomId 평가 시험방 아이디
* @param roomName 평가 시험방 이름
* @param description 평가 시험방 설명
* @param score 취득 점수 (채점 마감 전이면 0)
* @param totalProblems 총 문항 수
* @param testStartAt 시험 시작 시각
* @param testEndAt 시험 종료 시각
* @param isEnded 채점 마감 여부
*/
public record MyExamListItemResponse(
UUID roomId,
String roomName,
String description,
int score,
int totalProblems,
OffsetDateTime testStartAt,
OffsetDateTime testEndAt,
boolean isEnded
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.momogo.core.domain.report.dto.response;

import java.util.UUID;

/**
* 내 평가 시험 리뷰용 문항별 채점 결과 응답 Dto
* @param problemId 문제 아이디
* @param problemOrder 문항 순서
* @param name 문제 이름
* @param content 문제 내용
* @param userAnswer 내가 제출한 답안
* @param correctAnswer 정답
* @param explanation 해설
* @param isCorrect 정답 여부 (채점 마감 전이면 false)
*/
public record MyExamProblemResultResponse(
UUID problemId,
Integer problemOrder,
String name,
String content,
String userAnswer,
String correctAnswer,
String explanation,
boolean isCorrect
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.momogo.core.domain.report.dto.response;

import java.time.OffsetDateTime;
import java.util.UUID;

/**
* 싱글모드 문제 풀이 이력 응답 Dto
* @param problemId 문제 아이디
* @param problemName 문제 이름
* @param categoryName 문제 카테고리 이름
* @param isSolved 정답 여부 (가장 최근 제출 기준)
* @param userAnswer 가장 최근 제출 답안
* @param correctAnswer 정답
* @param solvedAt 최초 풀이 시각
*/
public record SingleModeHistoryResponse(
UUID problemId,
String problemName,
String categoryName,
boolean isSolved,
String userAnswer,
String correctAnswer,
OffsetDateTime solvedAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.momogo.core.domain.report.dto.response;

/**
* 공간 내 "성적 대시보드" 탭의 싱글모드 요약 카드 응답 Dto
* @param dailySolvedCount 오늘 정답 처리된 문제 수
* @param weeklySolvedCount 이번 주(월요일~현재) 정답 처리된 문제 수
* @param averageCorrectRate 전체 누적 평균 정답률(%)
*/
public record SingleModeSummaryResponse(
long dailySolvedCount,
long weeklySolvedCount,
double averageCorrectRate
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.momogo.core.domain.report.service;

import com.momogo.core.domain.report.dto.response.DashboardSummaryResponse;
import com.momogo.core.domain.report.dto.response.MyExamDetailResponse;
import com.momogo.core.domain.report.dto.response.MyExamListItemResponse;
import com.momogo.core.domain.report.dto.response.PersonalReportResponse;
import com.momogo.core.domain.report.dto.response.SingleModeHistoryResponse;
import com.momogo.core.domain.report.dto.response.SingleModeSummaryResponse;
import com.momogo.core.domain.report.dto.response.SpaceRankingResponse;
import com.momogo.core.domain.report.entity.ReportType;
import java.time.LocalDate;
Expand All @@ -14,4 +19,19 @@ public interface ReportService {

// 공간 대시보드: 이번 주 정답 수 기준 랭킹 조회
List<SpaceRankingResponse> getSpaceRanking(UUID spaceId);

// 메인 대시보드 요약 카드(오늘 푼 문제 수 / 이번 주 정답률 / 참여 완료 시험 수) 실시간 조회
DashboardSummaryResponse getDashboardSummary(UUID userId);

// 공간 내 성적 대시보드: 싱글모드 요약(오늘/이번 주 정답 수, 전체 평균 정답률) 조회
SingleModeSummaryResponse getSingleModeSummary(UUID userId);

// 공간 내 성적 대시보드: 싱글모드 전체 풀이 이력 조회
List<SingleModeHistoryResponse> getSingleModeHistory(UUID userId);

// 공간 내 성적 대시보드: 내가 응시(제출 완료)한 평가 시험 목록 조회
List<MyExamListItemResponse> getMyExamList(UUID userId);

// 공간 내 성적 대시보드: 내가 응시한 평가 시험의 문항별 채점 결과(리뷰) 조회
MyExamDetailResponse getMyExamDetail(UUID userId, UUID roomId);
}
Loading