Skip to content

feat: add exam room listing, grading review, and PDF/report fixes - #42

Merged
SungHuii merged 2 commits into
developfrom
feature/problem-refactor
Jul 24, 2026
Merged

SungHuii merged 2 commits into
developfrom
feature/problem-refactor

Conversation

@Junkov0

@Junkov0 Junkov0 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

작업 내용

  • 평가시험(정기 평가) 기능 전반에서 발견된 버그 수정 및 UX 개선. 채점 확정 로직 결함, 재응시 차단 누락,
    PDF 리포트 깨짐 등 실사용 중 발견된 문제들 위주.

변경 사항

채점

  • 채점 확정이 무조건 문자열 완전일치로 재채점하면서 AI 채점 결과를 덮어쓰던 버그 수정 (이미 채점된 답안은 유지, null인 것만 자동 채점)
  • AI 채점 진행 중에는 확정 불가하도록 차단
  • "AI 채점" / "채점 확정" 버튼을 "채점하기" 모달 하나로 통합 — 응시자별 제출답안/모범답안 보면서 수동으로 정답/오답 오버라이드 가능 (즉시 저장)
  • 채점 검토 API 신규 추가 (GET /rooms/{roomId}/grading, PATCH /rooms/{roomId}/grading/{answerId})

부정행위 방지

  • 이미 답안 제출한 시험방 재입장 시 문제지 재조회 가능하던 버그 차단 (백엔드 getRoomProblems)
  • 방이 최종 마감(전체 채점 확정)되기 전에는 "결과 보기"로 정답/해설을 볼 수 없도록 차단 — 같은 방에서 다른 응시자가 아직 풀고 있는 상황에서 컨닝 악용 가능했던 문제
  • 위 타이밍 문제 때문에 정답을 맞혔는데도 "오답"으로 잘못 표시되던 버그도 같이 해결됨 (미채점 상태를 오답으로 오인 표시하던 것)

실시간 평가시험 목록

  • 시험방 목록 조회 GET API가 아예 없어서 항상 빈 화면으로 보이던 문제 수정 (GET /api/spaces/{id}/rooms 신규 추가)
  • 대기실 진입 시 뜨던 가짜 접속자 더미 데이터 제거

PDF 리포트

  • 폰트 미임베드로 인해 뷰어에 따라 한글이 아예 안 보이던 문제 → 나눔고딕 폰트 임베드
  • 영어 위주 레이아웃을 전부 한글로 교체, Room ID 대신 시험방 이름 표시, 통계 카드/표 형태로 구조화
  • PDF 다운로드가 인증 헤더 누락 + 잘못된 URL로 항상 실패하던 문제 수정
  • 리포트 모달이 프론트/백엔드 필드명 불일치로 진입 시 흰 화면 뜨던 크래시 수정

기타 UX

  • 정답률/오답률 등 소수점 무한 표시 → 소수점 첫째자리로 통일
  • 새로고침하면 무조건 대시보드로 튕기던 문제 → 현재 보던 화면 유지
  • 데이터 많아지면 끝없이 길어지던 대시보드 표 3종에 높이 고정 + 스크롤 적용, 모달 전반에도 동일 적용
  • 랭킹/채점/리포트 등 이름 뜨는 곳에 프로필 사진 표시

체크리스트

  • 테스트 코드 작성 완료
  • 리뷰어 지정 완료

참고 사항

관련 이슈

Summary by CodeRabbit

  • 새 기능

    • 평가시험 방 목록 조회와 관리자 채점 검토 기능을 추가했습니다.
    • 답안별 수동 채점 및 최종 채점 확정을 지원합니다.
    • 채점 결과와 시험 리포트에 프로필 이미지를 표시합니다.
    • PDF 리포트 디자인과 한글 표시를 개선했습니다.
    • 마지막으로 보던 화면과 탭을 유지합니다.
  • 개선

    • 시험 종료 전 결과 및 해설 노출을 방지합니다.
    • 채점 대기·완료 상태에 따라 버튼을 구분해 표시합니다.
    • 평균 정답률을 소수점 한 자리로 표시합니다.
    • 긴 표와 모달에서 더욱 편리하게 스크롤할 수 있습니다.

@Junkov0
Junkov0 requested review from SungHuii and idktomorrow July 24, 2026 07:53
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

시험방 목록 조회, 관리자 채점 검토·수동 채점, 시험 상태 검증, PDF 리포트 개선과 함께 프론트엔드의 상태 복원, 채점 모달, 리포트 및 테이블 표시가 확장되었습니다.

Changes

시험방 및 채점 흐름

Layer / File(s) Summary
시험방 목록 조회 계약과 구현
momogo-api/.../RoomController.java, momogo-core/.../room/{service,repository,mapper}/*
공간별 시험방 목록 API와 서비스·저장소·매핑 구현이 추가되며 생성일 내림차순으로 방을 반환합니다.
채점 검토와 수동 채점 흐름
momogo-api/.../RoomController.java, momogo-core/.../room/{dto,service}/*
관리자 채점 검토 조회와 답안별 수동 정오 반영 API가 추가되고, 시험 종료·응시자·AI 채점 상태 검증과 기존 정오 판정 보존이 적용됩니다.
시험 결과와 PDF 리포트 변경
momogo-core/.../report/*, momogo-core/.../room/service/RoomServiceImpl.java, momogo-core/src/main/resources/fonts/OFL.txt
시험 종료 전 결과 조회가 제한되고, 프로필 이미지가 포함된 결과와 한글 폰트·통계·응시자 표를 포함한 PDF 리포트가 생성됩니다.
프론트엔드 화면 상태와 API 데이터 연결
momogo-frontend/src/App.tsx, momogo-frontend/src/pages/SpacePage.tsx
대시보드·공간·탭 상태를 sessionStorage에 저장·복원하고, 시험방·리포트·채점 응답 구조와 API 호출을 갱신합니다.
채점 검토 모달과 시험 상태 UI
momogo-frontend/src/pages/SpacePage.tsx, momogo-frontend/src/styles/theme.css
AI 채점 조회·실행, 수동 채점, 최종 확정과 제출·종료 상태별 시험 UI를 추가하고 모달 스크롤을 지원합니다.
대시보드·리포트 표시와 테이블 스타일
momogo-frontend/src/pages/{DashboardPage,SpacePage}.tsx
정답률 소수점 표시, 프로필 이미지, 응시자 결과 표시, 고정 테이블 헤더와 아바타 스타일을 적용합니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 관리자
  participant SpacePage
  participant RoomController
  participant RoomServiceImpl
  관리자->>SpacePage: 채점하기 선택
  SpacePage->>RoomController: GET 채점 검토 데이터
  RoomController->>RoomServiceImpl: getRoomGrading()
  RoomServiceImpl-->>SpacePage: 답안별 채점 결과
  SpacePage->>RoomController: PATCH 수동 채점
  RoomController->>RoomServiceImpl: manualGradeAnswer()
  RoomServiceImpl-->>SpacePage: 정오 판정 반영
  SpacePage->>RoomController: 최종 채점 확정
Loading

Possibly related PRs

Suggested reviewers: sunghuii, jaejo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 시험 방 목록, 채점 검토, PDF/리포트 수정이라는 PR의 핵심 변경을 간결하게 잘 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/problem-refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@SungHuii SungHuii left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

수정하시느라 수고하셨습니다 💯 👍

@idktomorrow idktomorrow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

열일하는 클로드 아주 칭찬해 👍

@SungHuii
SungHuii merged commit 4c63317 into develop Jul 24, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (4)
momogo-frontend/src/App.tsx (1)

8-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

loadStoredView의 타입 캐스팅 검증 누락 — 스키마 불일치 시 크래시 위험

JSON.parse(raw) as ViewState는 파싱 실패(문법 오류)만 try/catch로 방어하고, 파싱은 성공했지만 실제 ViewState 형태와 다른 경우(배포 후 스키마 변경, 사용자가 devtools로 sessionStorage 조작 등)는 전혀 검증하지 않습니다. 이 경우 view.space.name처럼 하위 필드에 바로 접근하는 렌더링 코드가 TypeError를 던질 수 있고, 별도 ErrorBoundary가 보이지 않으므로 앱 전체가 흰 화면으로 죽을 수 있습니다.

최소한 type 필드와 필수 서브필드 존재 여부를 확인하는 가벼운 런타임 가드를 추가하는 게 안전합니다.

🛡️ 개선 제안
 const loadStoredView = (): ViewState => {
   try {
     const raw = sessionStorage.getItem(VIEW_STORAGE_KEY);
-    if (raw) return JSON.parse(raw) as ViewState;
+    if (raw) {
+      const parsed = JSON.parse(raw);
+      if (parsed?.type === 'dashboard') return parsed as ViewState;
+      if (parsed?.type === 'space' && parsed.space?.id) return parsed as ViewState;
+    }
   } catch {
     // 저장된 값이 손상된 경우 기본 대시보드로 폴백
   }
   return { type: 'dashboard' };
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@momogo-frontend/src/App.tsx` around lines 8 - 28, Update loadStoredView to
validate the parsed sessionStorage value at runtime before returning it as
ViewState. Add lightweight guards for the dashboard variant and the space
variant, including required space fields and valid tab values, and return {
type: 'dashboard' } for malformed or schema-incompatible data.
momogo-frontend/src/pages/SpacePage.tsx (2)

2305-2312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

동일한 아바타 렌더링 패턴이 5곳에 중복되어 있습니다 — 공유 컴포넌트로 추출을 권장합니다

src={x.profileImageUrl || '/basic.png'} + 원형 이미지 스타일 조합이 랭킹 테이블, 응시 대상자 목록, 리포트 모달, 채점 검토 모달, 슈퍼관리자 유저 테이블에서 각각 독립적으로 반복됩니다. 근본 원인은 공유 Avatar 컴포넌트가 없다는 것 하나입니다. 지금은 문제없이 동작하지만, 이후 기본 이미지 경로를 바꾸거나 onError 폴백(깨진 URL일 때 기본 이미지로 대체)을 추가하려면 5곳을 전부 손봐야 하는 구조입니다.

  • momogo-frontend/src/pages/SpacePage.tsx#L2305-L2312: 채점 검토 모달의 응시자 아바타 렌더링을 공유 <Avatar src={item.userProfileImageUrl} alt={item.userName} /> 컴포넌트로 교체하세요.
  • momogo-frontend/src/pages/SpacePage.tsx#L1352-L1357: 랭킹 테이블의 아바타 렌더링을 동일한 공유 컴포넌트로 교체하세요.
  • momogo-frontend/src/pages/SpacePage.tsx#L1943-L1954: 응시 대상자 선택 목록의 아바타(선택 시 테두리 스타일은 유지하되 img 부분만) 공유 컴포넌트로 교체하세요.
  • momogo-frontend/src/pages/SpacePage.tsx#L2242-L2249: 리포트 결과 모달의 학생명 아바타를 공유 컴포넌트로 교체하세요.
  • momogo-frontend/src/pages/DashboardPage.tsx#L1525-L1534: 슈퍼관리자 유저 테이블의 아바타 렌더링을 공유 컴포넌트로 교체하세요.

공유 컴포넌트에 onError={(e) => (e.currentTarget.src = '/basic.png')} 같은 폴백을 한 번만 넣어두면 깨진 프로필 이미지 URL에 대한 방어도 자연스럽게 5곳 모두에 적용됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@momogo-frontend/src/pages/SpacePage.tsx` around lines 2305 - 2312, 추가한 공유
Avatar 컴포넌트로 아바타 렌더링을 통합하고, 기본 이미지 및 깨진 URL에 대한 onError 폴백을 해당 컴포넌트에 한 번만 구현하세요.
SpacePage.tsx의 2305-2312, 1352-1357, 1943-1954, 2242-2249와 DashboardPage.tsx의
1525-1534에서 기존 img를 Avatar로 교체하되, 응시 대상자 목록의 선택 상태 테두리 스타일은 유지하고 이미지 부분만 교체하세요.

1255-1282: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

isEnded의 의미가 채점 최종 마감이라는 점을 주석과 DTO 컴멘트에 명시하세요

isEndedRoomResponse/MyExamListItemResponse의 “채점 마감 여부”로 사용되고, finalizeTest()finalizeGrade() 경로에서 호출되므로 현재 UI 분기는 채점 확정 완료 시점에서 동작합니다. 이대로 두어도 되지만, isEnded가 “시험 시간 경과”가 아님을 읽는 사람과 향후 수정자가 즉시 알 수 있도록 주석/주석 스타일을 통일하면 유지보수에서 오해를 줄일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@momogo-frontend/src/pages/SpacePage.tsx` around lines 1255 - 1282, Clarify
the meaning of isEnded in the relevant RoomResponse and MyExamListItemResponse
DTO comments and the SpacePage.tsx UI comment: document that it indicates
grading finalization/closure, not exam-time expiration. Use consistent wording
and comment style, and preserve the existing result-display branching behavior.
momogo-core/src/main/java/com/momogo/core/domain/report/service/ReportServiceImpl.java (1)

181-186: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

결과 노출 차단 경계에 회귀 테스트를 추가해 주세요.

isEnded=false, isEnded=null일 때 REPORT_NOT_READY가 발생하고, true일 때만 정답·해설이 반환되는지 서비스 테스트로 고정하면 이후 상태 로직 변경으로 인한 정보 노출 회귀를 예방할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@momogo-core/src/main/java/com/momogo/core/domain/report/service/ReportServiceImpl.java`
around lines 181 - 186, ReportServiceImpl의 결과 조회 서비스 테스트에 isEnded 상태별 경계 회귀 테스트를
추가하세요. isEnded가 false 또는 null이면 REPORT_NOT_READY 예외가 발생하는지 검증하고, true일 때만 정답·해설이
반환되는지 확인하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@momogo-core/src/main/java/com/momogo/core/domain/room/repository/RoomRepository.java`:
- Around line 20-22: Update RoomRepository.findAllBySpaceIdOrderByCreatedAtDesc
to avoid lazy Room.space N+1 queries by using a fetch join for space or by
passing the known spaceId directly into RoomMapper.toResponseList. Also apply
Pageable-based limiting to this list query so responses remain bounded for large
spaces.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java`:
- Around line 216-229: Introduce a distinct RoomErrorCode such as
ALREADY_ATTENDED for the already-submitted participant case, and update the
RoomServiceImpl check on roomUser.getIsAttended() to throw it instead of
ALREADY_ENDED. Keep ALREADY_ENDED exclusively for the room-level getIsEnded()
condition and ensure the new code follows the existing RoomErrorCode
conventions.
- Around line 704-728: Prevent AI grading from overwriting manual grading
decisions: update RoomServiceImpl.manualGradeAnswer to reject changes while
room.getIsAiGradingInProgress() is true, and update saveGradingResults to skip
answers whose getIsCorrect() is already non-null. Preserve normal AI result
application for answers without a finalized manual grade.
- Around line 672-702: Update the repository method findByRoomProblemRoomId used
by getRoomGrading to fetch join UserRoomAnswer.user in its JPQL query. Ensure
the returned answers already contain initialized user data before sorting and
AnswerGradingItem mapping, while preserving the existing filtering and result
behavior.

In `@momogo-frontend/src/pages/SpacePage.tsx`:
- Around line 310-319: Update the loadRooms catch block to capture the thrown
error and log it with console.error, following the existing error-logging
pattern used by loadCategories, loadDashboardData, and loadMembersForInvitation,
while preserving the setRooms([]) fallback.
- Around line 2349-2380: Update the “채점 확정” button invoking
handleFinalizeFromGrading to be disabled when gradingData?.isAiGradingInProgress
is true, matching the existing guard on the “AI 채점 실행” button while preserving
its current click handler.
- Around line 768-793: Update handleDownloadPdf to use the shared authenticated
request flow, or reuse its refreshAccessToken-once-after-401 behavior, instead
of issuing the direct fetch without retry. Ensure a 401 response refreshes the
access token and retries the PDF request with the new Authorization header
before converting the successful response to a Blob, while preserving the
existing download and error-toast behavior.

---

Nitpick comments:
In
`@momogo-core/src/main/java/com/momogo/core/domain/report/service/ReportServiceImpl.java`:
- Around line 181-186: ReportServiceImpl의 결과 조회 서비스 테스트에 isEnded 상태별 경계 회귀 테스트를
추가하세요. isEnded가 false 또는 null이면 REPORT_NOT_READY 예외가 발생하는지 검증하고, true일 때만 정답·해설이
반환되는지 확인하세요.

In `@momogo-frontend/src/App.tsx`:
- Around line 8-28: Update loadStoredView to validate the parsed sessionStorage
value at runtime before returning it as ViewState. Add lightweight guards for
the dashboard variant and the space variant, including required space fields and
valid tab values, and return { type: 'dashboard' } for malformed or
schema-incompatible data.

In `@momogo-frontend/src/pages/SpacePage.tsx`:
- Around line 2305-2312: 추가한 공유 Avatar 컴포넌트로 아바타 렌더링을 통합하고, 기본 이미지 및 깨진 URL에 대한
onError 폴백을 해당 컴포넌트에 한 번만 구현하세요. SpacePage.tsx의 2305-2312, 1352-1357, 1943-1954,
2242-2249와 DashboardPage.tsx의 1525-1534에서 기존 img를 Avatar로 교체하되, 응시 대상자 목록의 선택 상태
테두리 스타일은 유지하고 이미지 부분만 교체하세요.
- Around line 1255-1282: Clarify the meaning of isEnded in the relevant
RoomResponse and MyExamListItemResponse DTO comments and the SpacePage.tsx UI
comment: document that it indicates grading finalization/closure, not exam-time
expiration. Use consistent wording and comment style, and preserve the existing
result-display branching behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8de2a3d9-5c2d-4ce1-b854-f4176d979b01

📥 Commits

Reviewing files that changed from the base of the PR and between bc5f8c1 and 64bbe51.

⛔ Files ignored due to path filters (2)
  • momogo-core/src/main/resources/fonts/NanumGothic-Bold.ttf is excluded by !**/*.ttf
  • momogo-core/src/main/resources/fonts/NanumGothic-Regular.ttf is excluded by !**/*.ttf
📒 Files selected for processing (17)
  • momogo-api/src/main/java/com/momogo/api/room/RoomController.java
  • momogo-core/src/main/java/com/momogo/core/domain/report/dto/response/SpaceRankingResponse.java
  • momogo-core/src/main/java/com/momogo/core/domain/report/repository/SpaceRankingRepository.java
  • momogo-core/src/main/java/com/momogo/core/domain/report/service/ReportServiceImpl.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/dto/request/ManualGradeRequest.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/dto/response/AnswerGradingItem.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/dto/response/RoomGradingResponse.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/dto/response/TakerGradeReport.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/mapper/RoomMapper.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/repository/RoomRepository.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomService.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java
  • momogo-core/src/main/resources/fonts/OFL.txt
  • momogo-frontend/src/App.tsx
  • momogo-frontend/src/pages/DashboardPage.tsx
  • momogo-frontend/src/pages/SpacePage.tsx
  • momogo-frontend/src/styles/theme.css

Comment on lines +20 to +22
// 공간 내 평가 시험방 목록 조회 (최근 생성 순)
List<Room> findAllBySpaceIdOrderByCreatedAtDesc(UUID spaceId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Room 엔티티 파일을 찾아 space 필드 애노테이션 확인
fd -e java Room.java -p 'entity' --exec cat -n {}

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Candidate Room.java files"
git ls-files '*Room.java' | sed -n '1,120p'

echo
echo "## Candidate RoomRepository.java files"
git ls-files '*RoomRepository.java' | sed -n '1,120p'

echo
echo "## Candidate RoomMapper.java files"
git ls-files '*RoomMapper.java' | sed -n '1,120p'

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Room.java"
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/entity/Room.java | sed -n '1,220p'

echo
echo "## RoomRepository.java"
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/repository/RoomRepository.java | sed -n '1,160p'

echo
echo "## RoomMapper.java"
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/mapper/RoomMapper.java | sed -n '1,160p'

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 5773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Call sites for findAllBySpaceIdOrderByCreatedAtDesc"
rg -n "findAllBySpaceIdOrderByCreatedAtDesc|toResponseList\(" momogo-core/src/main/java -S

echo
echo "## Response DTO and Space mapping access paths"
fd -e java 'RoomResponse.java|Space.*Response.*java' -S . | while read -r f; do
  echo "--- ${f#"$PWD/"}"
  cat -n "$f" | sed -n '1,160p'
done

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 1569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## RoomServiceImpl relevant section"
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java | sed -n '160,215p'

echo
echo "## RoomResponse.java"
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/dto/response/RoomResponse.java | sed -n '1,220p'

echo
echo "## RoomProblemResponse.java"
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/dto/response/RoomProblemResponse.java | sed -n '1,180p'

echo
echo "## RoomRepository usages (excluding repository/mapper definitions)"
python3 - <<'PY'
import subprocess, pathlib, re
files = subprocess.check_output(['git','ls-files'], text=True).splitlines()
for f in files:
    if not f.endswith('.java'):
        continue
    if f.endswith('RoomRepository.java') or f.endswith('RoomMapper.java'):
        continue
    txt = pathlib.Path(f).read_text()
    if 'findAllBySpaceIdOrderByCreatedAtDesc' in txt or 'toResponseList(' in txt:
        print(f'--- {f}')
        for i,line in enumerate(txt.splitlines(),1):
            if 'findAllBySpaceIdOrderByCreatedAtDesc' in line or 'toResponseList(' in line:
                print(f'{i}: {line.strip()}')
PY

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 5151


목록 조회 쿼리의 space N+1 처리를 추가하세요.

findAllBySpaceIdOrderByCreatedAtDescRoom.spaceLAZY인 엔티티를 그대로 반환합니다. 이 결과는 RoomMapper.toResponseList에서 spaceId = space.id로 매핑되므로 조회된 방 개수만큼 Space 전용 Select SQL이 발생합니다. 사용자가 이미 spaceId를 알고 조회하므로, join fetch r.space를 추가하거나 mapper 입력에 spaceId를 직접 전달해 연관관계 로딩을 피하는 선택지가 있습니다. 목록도 Pageable로 제한해 대규모 공간의 방이 누적되더라도 응답이 무한 증식하지 않도록 만들면 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/repository/RoomRepository.java`
around lines 20 - 22, Update RoomRepository.findAllBySpaceIdOrderByCreatedAtDesc
to avoid lazy Room.space N+1 queries by using a fetch join for space or by
passing the known spaceId directly into RoomMapper.toResponseList. Also apply
Pageable-based limiting to this list query so responses remain bounded for large
spaces.

Comment on lines +216 to 229
// 상태 검증 - 이미 마감된 시험은 재입장 차단
if (Boolean.TRUE.equals(room.getIsEnded())) {
throw new BusinessException(RoomErrorCode.ALREADY_ENDED);
}

// 응시 대상 유저 자격 검증 (RoomUser 매핑 테이블 존재 확인)
RoomUserId roomUserId = new RoomUserId(roomId, userId);
if (!roomUserRepository.existsById(roomUserId)) {
throw new BusinessException(RoomErrorCode.NOT_ROOM_PARTICIPANT);
RoomUser roomUser = roomUserRepository.findById(roomUserId)
.orElseThrow(() -> new BusinessException(RoomErrorCode.NOT_ROOM_PARTICIPANT));

// 상태 검증 - 이미 답안을 제출한 응시자의 재입장(재조회) 차단
if (Boolean.TRUE.equals(roomUser.getIsAttended())) {
throw new BusinessException(RoomErrorCode.ALREADY_ENDED);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"이미 응시 완료" 상태에 ALREADY_ENDED 에러코드를 재사용하고 있어요.

방이 마감된 경우(217-219)와, 응시자가 이미 답안을 제출한 경우(226-229)가 동일한 RoomErrorCode.ALREADY_ENDED를 던지고 있습니다. 두 상황은 도메인적으로 전혀 다릅니다:

  • 217-219: 시험방 자체가 마감됨
  • 226-229: 시험방은 살아있지만 이 유저가 이미 제출을 완료함(재입장 차단)

프론트엔드에서 이 코드를 매핑해 "시험이 종료되었습니다" 같은 메시지를 보여준다면, 아직 시험이 끝나지 않았는데도 응시자에게 혼란을 줄 수 있습니다. 별도 에러코드(예: ALREADY_ATTENDED)를 도입하면 클라이언트가 정확한 안내 문구를 노출할 수 있고, 추후 디버깅/로그 분석 시에도 원인 구분이 쉬워집니다. 이런 공통 예외 처리의 명확성은 유지보수성 측면에서 꽤 중요한 부분입니다.

🛠️ 제안 예시
     // 상태 검증 - 이미 답안을 제출한 응시자의 재입장(재조회) 차단
     if (Boolean.TRUE.equals(roomUser.getIsAttended())) {
-      throw new BusinessException(RoomErrorCode.ALREADY_ENDED);
+      throw new BusinessException(RoomErrorCode.ALREADY_ATTENDED);
     }

근거: 경로 지침의 "공통 예외 처리" 확인 항목에 따라 작성했습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java`
around lines 216 - 229, Introduce a distinct RoomErrorCode such as
ALREADY_ATTENDED for the already-submitted participant case, and update the
RoomServiceImpl check on roomUser.getIsAttended() to throw it instead of
ALREADY_ENDED. Keep ALREADY_ENDED exclusively for the room-level getIsEnded()
condition and ensure the new code follows the existing RoomErrorCode
conventions.

Source: Path instructions

Comment on lines +672 to +702
@Override
public RoomGradingResponse getRoomGrading(UUID adminUserId, UUID roomId) {

log.info("[RoomService] 채점 검토 화면 조회 - adminUserId: {}, roomId: {}", adminUserId, roomId);

// 방 존재 및 관리자 권한 검증
Room room = findRoomOrThrow(roomId);
validateSpaceAdmin(adminUserId, room);

// 응시자별 문제별 제출 답안 전체 조회 (문제 순서 -> 응시자 이름 순 정렬)
List<UserRoomAnswer> answers = userRoomAnswerRepository.findByRoomProblemRoomId(roomId);
List<AnswerGradingItem> items = answers.stream()
.sorted(Comparator
.comparing((UserRoomAnswer a) -> a.getRoomProblem().getProblemOrder())
.thenComparing(a -> a.getUser().getName()))
.map(a -> new AnswerGradingItem(
a.getId(),
a.getUser().getId(),
a.getUser().getName(),
a.getUser().getProfileImageUrl(),
a.getRoomProblem().getId(),
a.getRoomProblem().getProblemOrder(),
a.getRoomProblem().getName(),
a.getUserAnswer(),
a.getRoomProblem().getCorrectAnswer(),
a.getIsCorrect()
))
.toList();

return new RoomGradingResponse(room.getId(), room.getName(), room.getIsAiGradingInProgress(), items);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e java UserRoomAnswer.java -p 'entity' --exec cat -n {}
fd -e java UserRoomAnswerRepository.java --exec cat -n {}

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 1317


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg 'UserRoomAnswer\.java|RoomServiceImpl\.java|AnswerGradingItem\.java|UserRoomAnswerRepository\.java|User\.java' || true

echo
echo "== UserRoomAnswer.java =="
USER_ANSWER_FILE="$(git ls-files | rg 'UserRoomAnswer\.java$' | head -n1 || true)"
if [ -n "${USER_ANSWER_FILE:-}" ]; then
  cat -n "$USER_ANSWER_FILE" | sed -n '1,220p'
fi

echo
echo "== User entity fields relevant to lazy proxy =="
USER_FILE="$(git ls-files | rg 'domain/.*/entity/User\.java$|core/domain/.*/entity/User\.java$|.*User\.java$' | while read f; do grep -q 'ProfileImageUrl\|profileImageUrl\|profile_image_url' "$f" && echo "$f"; done | head -n1 || true)"
if [ -n "${USER_FILE:-}" ]; then
  cat -n "$USER_FILE" | sed -n '1,180p'
fi

echo
echo "== getRoomGrading area =="
SERVICE_FILE="$(git ls-files | rg 'RoomServiceImpl\.java$' | head -n1 || true)"
if [ -n "${SERVICE_FILE:-}" ]; then
  cat -n "$SERVICE_FILE" | sed -n '640,715p'
fi

echo
echo "== other getUser usages in RoomService nearby =="
if [ -n "${SERVICE_FILE:-}" ]; then
  rg -n "getUser\(\)\.(getName|getProfileImageUrl|getId)|AnswerGradingItem|finalizeGrade|getRoomReport|userRoomAnswerRepository" "$SERVICE_FILE"
fi

echo
echo "== JPA lazy/fetched declarations around UserRoomAnswer.user =="
if [ -n "${USER_ANSWER_FILE:-}" ]; then
  rg -n "(`@ManyToOne`|FetchType|join fetch u\.user|join fetch u\.roomProblem|field .*user|\.user)" "$USER_ANSWER_FILE"
fi

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 13438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AnswerGradingItem =="
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/dto/response/AnswerGradingItem.java | sed -n '1,160p'

echo
echo "== getRoomGrade and finalizeGrade relevant sections =="
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java | sed -n '303,350p'
cat -n momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java | sed -n '404,445p'

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 5146


getRoomGrading에서 UserRoomAnswer.user를 fetch 조인으로 읽어오세요.

findRoomGradingfindByRoomProblemRoomIdUserRoomAnswer.user를 지연 로딩 상태로 먼저 조회한 후, 정렬과 DTO 매핑에서 user.name, user.profileImageUrl에 접근합니다. 필드 값을 가져오려면 지연 프록시가 실제로 초기화되어야 하므로 답안 수만큼 추가 SELECT가 발생할 수 있습니다.

UserRoomAnswer.user는 실제 필드 값이 필요하므로, Repository.findByRoomProblemRoomId의 JPQL에 join fetch u.user를 추가해 한 번에 채점 검토 데이터를 읽어오세요.

🔧 제안 diff
-  `@Query`("select u from UserRoomAnswer u join fetch u.roomProblem rp where rp.room.id = :roomId")
+  `@Query`("select u from UserRoomAnswer u join fetch u.roomProblem rp join fetch u.user where rp.room.id = :roomId")
   List<UserRoomAnswer> findByRoomProblemRoomId(`@Param`("roomId") java.util.UUID roomId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java`
around lines 672 - 702, Update the repository method findByRoomProblemRoomId
used by getRoomGrading to fetch join UserRoomAnswer.user in its JPQL query.
Ensure the returned answers already contain initialized user data before sorting
and AnswerGradingItem mapping, while preserving the existing filtering and
result behavior.

Comment on lines +704 to +728
@Override
@Transactional
public void manualGradeAnswer(UUID adminUserId, UUID roomId, UUID answerId, ManualGradeRequest request) {

log.info("[RoomService] 수동 채점 오버라이드 - adminUserId: {}, roomId: {}, answerId: {}, isCorrect: {}",
adminUserId, roomId, answerId, request.isCorrect());

// 방 존재 및 관리자 권한 검증
Room room = findRoomOrThrow(roomId);
validateSpaceAdmin(adminUserId, room);

// 이미 채점 확정(마감)된 시험방은 수동 채점 변경 불가
if (Boolean.TRUE.equals(room.getIsEnded())) {
throw new BusinessException(RoomErrorCode.ALREADY_ENDED);
}

// 답안 존재 검증 및 해당 방 소속 여부 검증
UserRoomAnswer answer = userRoomAnswerRepository.findById(answerId)
.orElseThrow(() -> new BusinessException(RoomErrorCode.PROBLEM_NOT_FOUND));
if (!answer.getRoomProblem().getRoom().getId().equals(roomId)) {
throw new BusinessException(RoomErrorCode.PROBLEM_NOT_FOUND);
}

answer.grade(request.isCorrect());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'saveGradingResults' -C6

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -u

echo "== files matching RoomServiceImpl/UserRoomAnswer/Room =="
git ls-files | rg 'RoomServiceImpl\.java|UserRoomAnswer\.java|Room\.java|Problem.*Ai|Grading' || true

echo
echo "== locate service interface declarations =="
rg -n 'public interface RoomService|interface RoomService|class RoomServiceImpl|saveGradingResults|finalizeGrade|manualGradeAnswer|startAiGrading|isAiGradingInProgress' -S . --glob '*.java'

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 4383


🏁 Script executed:

#!/bin/bash
set -u

echo "== RoomServiceImpl relevant methods =="
sed -n '303,335p;647,750p' momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java | cat -n

echo
echo "== Room start/end AI methods =="
sed -n '40,95p' momogo-core/src/main/java/com/momogo/core/domain/room/entity/Room.java | cat -n

echo
echo "== UserRoomAnswer grade method =="
sed -n '1,160p' momogo-core/src/main/java/com/momogo/core/domain/room/entity/UserRoomAnswer.java | cat -n

echo
echo "== AI event listener relevant section =="
sed -n '1,125p' momogo-ai/src/main/java/com/momogo/ai/grading/listener/AiGradingEventListener.java | cat -n

echo
echo "== Behavioral probe of current saveGradingResults/user-grade logic from source text =="
python3 - <<'PY'
from pathlib import Path
p = Path('momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java')
s = p.read_text()
method = s[s.index('public void saveGradingResults'):s.index('\n  }\n', s.index('public void saveGradingResults'))+5]
print('uses null check before answer.grade:', ('answer.getIsCorrect() != null' in method) or ('answer.getIsCorrect() == null' in method))
print('always calls answer.grade for non-null gradingResults value:')
sub = s[s.index('public void saveGradingResults'):s.index('\n  }\n', s.index('public void saveGradingResults'))+5]
print('answer.grade(isCorrect)' in sub)
sub2 = s[s.index('public void manualGradeAnswer'):s.index('\n  }\n', s.index('public void manualGradeAnswer'))+5]
print('manualGradeAnswer checks getIsAiGradingInProgress:', 'getIsAiGradingInProgress' in sub2)
print('manualGradeAnswer checks getIsCorrect before grade:', 'answer.getIsCorrect() != null' in sub2 or 'answer.getIsCorrect() == null' in sub2)
PY

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 16526


AI 채점 진행 중 수동 채점 변경을 방어가 필요합니다.

saveGradingResults가 AI 채점 비동기 완료 후 answer.grade(isCorrect)로 답안의 isCorrect를 무조건 덮어쓰고, manualGradeAnswer는 그 진행 여부를 검사하지 않습니다. RoomServiceImpl.manualGradeAnswer(706-714)에서도 room.getIsAiGradingInProgress()를 검증하거나, RoomServiceImpl.saveGradingResults(719-728)에서 answer.getIsCorrect() != null인 답안이 이미 수동 확정한 답안이므로 AI 결과로 다시 변경하지 않게 하면, 관리자가 수동 정정해 둔 채점 결과를 비동기 AI 결과가 뒤따라 덮어씌우는 일관성 문제를 막을 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java`
around lines 704 - 728, Prevent AI grading from overwriting manual grading
decisions: update RoomServiceImpl.manualGradeAnswer to reject changes while
room.getIsAiGradingInProgress() is true, and update saveGradingResults to skip
answers whose getIsCorrect() is already non-null. Preserve normal AI result
application for answers without a finalized manual grade.

Comment on lines 310 to 319
const loadRooms = async () => {
try {
const data = await request<RoomResponse[]>(`/api/spaces/${space.id}/rooms/list`, {
const data = await request<RoomResponse[]>(`/api/spaces/${space.id}/rooms`, {
method: 'GET',
}).catch(async () => {
// 백엔드에 전용 맵핑 리스트가 없을 시 전체 목록으로 조회 필터링 fallback
return request<RoomResponse[]>(`/api/spaces/${space.id}/rooms`, { method: 'GET' });
});
if (data) setRooms(data);
} catch {
// 룸 리스트 불러오기 임시 하드코딩 mock 구조
setRooms([]);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

loadRooms 실패 시 에러 정보가 완전히 사라집니다

catch { setRooms([]); }는 에러 객체를 아예 받지 않아 로깅조차 안 됩니다. 같은 파일의 loadCategories, loadDashboardData, loadMembersForInvitation은 최소 console.error로 남기는데, 여기만 조용히 삼켜버려서 운영 중 "시험방 목록이 안 보인다"는 이슈가 들어와도 원인 파악이 어려워집니다.

🔍 개선 제안
   const loadRooms = async () => {
     try {
       const data = await request<RoomResponse[]>(`/api/spaces/${space.id}/rooms`, {
         method: 'GET',
       });
       if (data) setRooms(data);
-    } catch {
+    } catch (err) {
+      console.error('시험방 목록 조회 실패:', err);
       setRooms([]);
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const loadRooms = async () => {
try {
const data = await request<RoomResponse[]>(`/api/spaces/${space.id}/rooms/list`, {
const data = await request<RoomResponse[]>(`/api/spaces/${space.id}/rooms`, {
method: 'GET',
}).catch(async () => {
// 백엔드에 전용 맵핑 리스트가 없을 시 전체 목록으로 조회 필터링 fallback
return request<RoomResponse[]>(`/api/spaces/${space.id}/rooms`, { method: 'GET' });
});
if (data) setRooms(data);
} catch {
// 룸 리스트 불러오기 임시 하드코딩 mock 구조
setRooms([]);
}
};
const loadRooms = async () => {
try {
const data = await request<RoomResponse[]>(`/api/spaces/${space.id}/rooms`, {
method: 'GET',
});
if (data) setRooms(data);
} catch (err) {
console.error('시험방 목록 조회 실패:', err);
setRooms([]);
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@momogo-frontend/src/pages/SpacePage.tsx` around lines 310 - 319, Update the
loadRooms catch block to capture the thrown error and log it with console.error,
following the existing error-logging pattern used by loadCategories,
loadDashboardData, and loadMembersForInvitation, while preserving the
setRooms([]) fallback.

Comment on lines +768 to 793
// 리포트 PDF 다운로드 (Authorization 헤더 인증이 필요해 window.open 대신 인증된 fetch로 blob을 받아 저장)
const handleDownloadPdf = async (roomId: string) => {
try {
const headers: Record<string, string> = {};
const token = getAccessToken();
if (token) headers['Authorization'] = `Bearer ${token}`;

const response = await fetch(`/api/rooms/${roomId}/report/download`, {
headers,
credentials: 'include',
});
if (!response.ok) throw new Error('PDF 다운로드에 실패했습니다.');

const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `room_${roomId}_report.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (err: any) {
showToast(err.message || 'PDF 다운로드에 실패했습니다.', 'error');
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

services/api.ts에 refreshAccessToken 같은 내부 함수가 export되어 있는지, 혹은 유사한 재시도 헬퍼를 blob 응답에도 재사용 가능한지 저장소 코드를 확인해 주세요.

💡 Result:

서비스 저장소 내 services/api.ts 구조는 프로젝트마다 상이하지만, 일반적인 구현 패턴을 분석한 결과는 다음과 같습니다. 1. refreshAccessToken 함수 여부: 많은 프로젝트에서 services/api.ts 또는 lib/api.ts 내에 refreshAccessToken 함수를 포함하고 있으며, 외부에서 재사용할 수 있도록 export 하는 경우가 많습니다 [1][2][3]. 주로 Axios 인터셉터나 페치(fetch) 래퍼 내부에서 토큰 만료(401 에러) 시 이를 호출하여 인증을 갱신하는 방식으로 사용됩니다 [4][1][2]. 단, 일부 프로젝트는 이를 내부 함수로만 제한하거나 별도의 인증 서비스(authService) 모듈로 분리하여 관리하기도 하므로, 현재 작업 중인 구체적인 저장소의 코드를 확인해야 합니다 [2]. 2. Blob 응답을 위한 재시도 헬퍼 재사용 가능 여부: 일반적인 재시도 로직(Retry helper)은 Response 객체나 fetch 요청을 인자로 받아 처리하므로, 응답 형태가 Blob이더라도 해당 헬퍼가 응답의 status를 확인하거나 에러를 파싱하는 방식에 따라 재사용이 가능할 수 있습니다 [5][6]. - 주의사항: Blob 데이터는 일반적인 JSON 응답과 달리 response.json()으로 파싱할 수 없습니다. 재시도 헬퍼가 내부적으로 응답 본문을 읽으려 할 경우(예: 에러 메시지 추출), Blob에 최적화된 처리(예: response.blob(), response.text() 또는 response.arrayBuffer() 등)가 추가되어 있는지 확인해야 합니다 [5][6][7]. - 결론: 재시도 헬퍼가 본문 파싱에 의존적이지 않고 HTTP 상태 코드(4xx, 5xx 등)만을 기준으로 동작한다면 즉시 재사용 가능하지만, 에러 본문을 분석하는 로직이 있다면 Blob 응답 처리를 위한 분기 처리가 필요합니다 [5][6]. 저장소의 services/api.ts 코드를 열어 refreshAccessToken의 export 여부를 먼저 확인하시고, 재시도 헬퍼가 파라미터로 응답 객체를 어떻게 다루고 있는지(특히 파싱 부분)를 검토하시기 바랍니다.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)services/api\.ts$|SpacePage\.tsx$|axios|auth|token' | head -200

echo
echo "== SpacePage relevant lines =="
if [ -f momogo-frontend/src/pages/SpacePage.tsx ]; then
  sed -n '720,815p' momogo-frontend/src/pages/SpacePage.tsx
fi

echo
echo "== api request implementation candidate =="
if [ -f momogo-frontend/src/services/api.ts ]; then
  ast-grep outline momogo-frontend/src/services/api.ts --view compact || true
  nl -ba momogo-frontend/src/services/api.ts | sed -n '1,260p'
else
  fd -a 'api\.ts' . | sed -n '1,50p'
fi

echo
echo "== usages of request helper / refreshAccessToken =="
rg -n "request\(|refreshAccessToken|getAccessToken|Authorization|401|report/download" momogo-frontend/src || true

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 5801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== services/api.ts =="
cat -n momogo-frontend/src/services/api.ts | sed -n '1,280p'

echo
echo "== services/auth.ts =="
cat -n momogo-frontend/src/services/auth.ts | sed -n '1,260p'

echo
echo "== SpacePage imports and request usages =="
cat -n momogo-frontend/src/pages/SpacePage.tsx | sed -n '1,80p'
echo
rg -n "import .*request|request<|request\(|getAccessToken|setAccessToken|refreshAccessToken|report/download|openReport|downloadPdf" momogo-frontend/src/pages/SpacePage.tsx momogo-frontend/src/services || true

Repository: MoMoGo-QuizPlatform/MoMoGo

Length of output: 22167


PDF 다운로드도 401 재시도 보안망을 적용하세요

request()는 401 응답 시 refreshAccessToken()으로 토큰을 재발급한 뒤 원 요청을 한 번 재시도하지만, handleDownloadPdf는 직접 fetch를 해서 이 안전망이 없습니다. 채점 확정 후 리포트까지 시간이 흐를 수 있으므로, 만료된 액세스 토큰으로 다운로드 요청을 시도하면 JSON 응답 대신 “PDF 다운로드에 실패했습니다” 토스트만 보입니다. 401 감지 후 토큰 재발급하고 Blob 요청을 다시 보내는 로직을 공유하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@momogo-frontend/src/pages/SpacePage.tsx` around lines 768 - 793, Update
handleDownloadPdf to use the shared authenticated request flow, or reuse its
refreshAccessToken-once-after-401 behavior, instead of issuing the direct fetch
without retry. Ensure a 401 response refreshes the access token and retries the
PDF request with the new Authorization header before converting the successful
response to a Blob, while preserving the existing download and error-toast
behavior.

Comment on lines +2349 to +2380
<div style={styles.modalActions}>
<button
type="button"
className="btn btn-secondary"
onClick={() => gradingRoomId && loadGradingData(gradingRoomId)}
disabled={gradingLoading}
>
새로고침
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleAiGradeInModal}
disabled={gradingData?.isAiGradingInProgress}
>
AI 채점 실행
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowGradingModal(false);
setGradingData(null);
setGradingRoomId(null);
}}
>
닫기
</button>
<button type="button" className="btn btn-primary" onClick={handleFinalizeFromGrading}>
채점 확정
</button>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"채점 확정" 버튼에 AI 채점 진행 중 차단 가드가 빠졌습니다

PR 목표에 "Blocks grading confirmation while AI grading is in progress"가 명시되어 있는데, 실제로는 "AI 채점 실행" 버튼만 disabled={gradingData?.isAiGradingInProgress}를 갖고 있고, "채점 확정" 버튼(handleFinalizeFromGrading)에는 동일한 가드가 없습니다. 백엔드가 서버 측에서 막아준다 해도, 관리자는 클릭 후에야 실패 토스트로 알게 되어 혼란스럽고, 서버가 이를 검증하지 않는다면 진행 중인 AI 채점 결과가 확정 시점 스냅샷과 어긋날 위험도 있습니다.

🔒 개선 제안
-              <button type="button" className="btn btn-primary" onClick={handleFinalizeFromGrading}>
+              <button
+                type="button"
+                className="btn btn-primary"
+                onClick={handleFinalizeFromGrading}
+                disabled={gradingData?.isAiGradingInProgress}
+              >
                 채점 확정
               </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div style={styles.modalActions}>
<button
type="button"
className="btn btn-secondary"
onClick={() => gradingRoomId && loadGradingData(gradingRoomId)}
disabled={gradingLoading}
>
새로고침
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleAiGradeInModal}
disabled={gradingData?.isAiGradingInProgress}
>
AI 채점 실행
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowGradingModal(false);
setGradingData(null);
setGradingRoomId(null);
}}
>
닫기
</button>
<button type="button" className="btn btn-primary" onClick={handleFinalizeFromGrading}>
채점 확정
</button>
</div>
<div style={styles.modalActions}>
<button
type="button"
className="btn btn-secondary"
onClick={() => gradingRoomId && loadGradingData(gradingRoomId)}
disabled={gradingLoading}
>
새로고침
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleAiGradeInModal}
disabled={gradingData?.isAiGradingInProgress}
>
AI 채점 실행
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowGradingModal(false);
setGradingData(null);
setGradingRoomId(null);
}}
>
닫기
</button>
<button
type="button"
className="btn btn-primary"
onClick={handleFinalizeFromGrading}
disabled={gradingData?.isAiGradingInProgress}
>
채점 확정
</button>
</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@momogo-frontend/src/pages/SpacePage.tsx` around lines 2349 - 2380, Update the
“채점 확정” button invoking handleFinalizeFromGrading to be disabled when
gradingData?.isAiGradingInProgress is true, matching the existing guard on the
“AI 채점 실행” button while preserving its current click handler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants