feat: add exam room listing, grading review, and PDF/report fixes - #42
Conversation
# Conflicts: # momogo-frontend/src/pages/DashboardPage.tsx
📝 WalkthroughWalkthrough시험방 목록 조회, 관리자 채점 검토·수동 채점, 시험 상태 검증, PDF 리포트 개선과 함께 프론트엔드의 상태 복원, 채점 모달, 리포트 및 테이블 표시가 확장되었습니다. Changes시험방 및 채점 흐름
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: 최종 채점 확정
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 컴멘트에 명시하세요
isEnded가RoomResponse/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
⛔ Files ignored due to path filters (2)
momogo-core/src/main/resources/fonts/NanumGothic-Bold.ttfis excluded by!**/*.ttfmomogo-core/src/main/resources/fonts/NanumGothic-Regular.ttfis excluded by!**/*.ttf
📒 Files selected for processing (17)
momogo-api/src/main/java/com/momogo/api/room/RoomController.javamomogo-core/src/main/java/com/momogo/core/domain/report/dto/response/SpaceRankingResponse.javamomogo-core/src/main/java/com/momogo/core/domain/report/repository/SpaceRankingRepository.javamomogo-core/src/main/java/com/momogo/core/domain/report/service/ReportServiceImpl.javamomogo-core/src/main/java/com/momogo/core/domain/room/dto/request/ManualGradeRequest.javamomogo-core/src/main/java/com/momogo/core/domain/room/dto/response/AnswerGradingItem.javamomogo-core/src/main/java/com/momogo/core/domain/room/dto/response/RoomGradingResponse.javamomogo-core/src/main/java/com/momogo/core/domain/room/dto/response/TakerGradeReport.javamomogo-core/src/main/java/com/momogo/core/domain/room/mapper/RoomMapper.javamomogo-core/src/main/java/com/momogo/core/domain/room/repository/RoomRepository.javamomogo-core/src/main/java/com/momogo/core/domain/room/service/RoomService.javamomogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.javamomogo-core/src/main/resources/fonts/OFL.txtmomogo-frontend/src/App.tsxmomogo-frontend/src/pages/DashboardPage.tsxmomogo-frontend/src/pages/SpacePage.tsxmomogo-frontend/src/styles/theme.css
| // 공간 내 평가 시험방 목록 조회 (최근 생성 순) | ||
| List<Room> findAllBySpaceIdOrderByCreatedAtDesc(UUID spaceId); | ||
|
|
There was a problem hiding this comment.
🚀 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'
doneRepository: 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()}')
PYRepository: MoMoGo-QuizPlatform/MoMoGo
Length of output: 5151
목록 조회 쿼리의 space N+1 처리를 추가하세요.
findAllBySpaceIdOrderByCreatedAtDesc는 Room.space가 LAZY인 엔티티를 그대로 반환합니다. 이 결과는 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.
| // 상태 검증 - 이미 마감된 시험은 재입장 차단 | ||
| 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); | ||
| } |
There was a problem hiding this comment.
🎯 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
| @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); | ||
| } |
There was a problem hiding this comment.
🚀 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"
fiRepository: 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 조인으로 읽어오세요.
findRoomGrading은 findByRoomProblemRoomId로 UserRoomAnswer.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.
| @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()); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'saveGradingResults' -C6Repository: 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)
PYRepository: 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.
| 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([]); | ||
| } | ||
| }; |
There was a problem hiding this comment.
📐 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.
| 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.
| // 리포트 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'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🩺 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:
- 1: bookorbit/bookorbit@811f6c5
- 2: https://github.com/lebachhiep/netproxy-console-v2/blob/main/CLAUDE.md
- 3: Refactor authentication private-mailhub/backend-api#18
- 4: https://oneuptime.com/blog/post/2026-01-15-react-native-jwt-refresh-tokens/view
- 5: https://github.com/chris-tse/opencode-web/blob/0ab3c855/src/services/api.ts
- 6: https://github.com/vercel/storage/blob/f23cb891/packages/blob/src/api.ts
- 7: https://github.com/dataelement/Clawith/blob/3e3889f9/frontend/src/services/api.ts
🏁 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 || trueRepository: 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 || trueRepository: 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.
| <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> |
There was a problem hiding this comment.
🎯 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.
| <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.
작업 내용
PDF 리포트 깨짐 등 실사용 중 발견된 문제들 위주.
변경 사항
채점
채점 확정이 무조건 문자열 완전일치로 재채점하면서 AI 채점 결과를 덮어쓰던 버그 수정 (이미 채점된 답안은 유지, null인 것만 자동 채점)GET /rooms/{roomId}/grading,PATCH /rooms/{roomId}/grading/{answerId})부정행위 방지
getRoomProblems)실시간 평가시험 목록
GET /api/spaces/{id}/rooms신규 추가)PDF 리포트
기타 UX
체크리스트
참고 사항
관련 이슈
Summary by CodeRabbit
새 기능
개선