#597 대회 랭킹 페이지 오류 수정 및 디자인 개선#605
Conversation
There was a problem hiding this comment.
Pull request overview
대회 랭킹/참가자 목록에서 닉네임 변경으로 인한 중복 표시 및 랭킹 UI/표시 정보를 개선하여, 참가자 집계의 정확성과 랭킹 페이지의 가독성을 높이는 PR입니다.
Changes:
- 참가자 목록 집계 기준을
username에서user_id로 변경하고 관련 API 테스트를 추가 - 랭킹(ACM/OI)에서 문제 툴팁을 문제 번호 → 문제 제목으로 변경, AC/WA 표시를 구분하도록 UI 개선
- 랭킹 로딩 중 빈 상태 문구 대신 스켈레톤 로딩을 적용
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/pages/oj/views/contest/children/OIContestRank.vue | OI 랭킹: 로딩 스켈레톤, 툴팁 제목 표시, 점수 셀 UI 개선 |
| frontend/src/pages/oj/views/contest/children/ACMContestRank.vue | ACM 랭킹: 로딩 스켈레톤, 툴팁 제목 표시, AC/WA 상태 표시 개선 |
| frontend/src/pages/oj/views/contest/children/ContestProblemList.vue | 문제 목록 테이블 스타일(테이블 간격/접힘) 보정 |
| frontend/src/pages/oj/components/CustomTooltip.vue | 툴팁 content prop 타입/기본값 정의 변경 |
| frontend/src/pages/oj/App.vue | 초기화 로직 호출 제거(마운트 시 불필요 호출 정리) |
| frontend/config/index.js | dev proxy 타겟 기본값 및 Referer 헤더 처리 정리 |
| backend/contest/views/oj.py | 참가자 목록 집계 기준을 user_id로 변경하여 중복 참가자 제거 |
| backend/contest/tests.py | 참가자 목록 API에서 user_id 기준으로 그룹핑되는지 테스트 추가 |
|
테스트까지 진행하다니 멋져요 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
frontend/src/store/modules/contest.js:232
- getContestAccess가 new Promise로 감싸져 있지만 실패 시 .catch()에서 resolve/reject를 호출하지 않아 Promise가 영원히 pending 상태로 남을 수 있습니다. catch에서 access를 false로 커밋하고 resolve/reject 처리하거나, Promise wrapper를 제거하고 api.getContestAccess(...) 체인을 그대로 반환하도록 정리하는 것이 안전합니다.
getContestAccess({ commit, rootState }) {
return new Promise((resolve) => {
api
.getContestAccess(rootState.route.params.contestID)
.then((res) => {
commit(types.CONTEST_ACCESS, { access: res.data.data.access })
resolve(res)
})
.catch()
})
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
frontend/src/store/modules/contest.js:232
- getContestAccess가 실패할 경우
.catch()에 핸들러가 없어 Promise가 resolve/reject되지 않은 채로 남습니다. 호출 측에서 대기하는 로직이 있으면 영구 대기/로딩 상태가 될 수 있으니, reject를 받도록 Promise 시그니처를 복구하고 catch에서 reject(또는 access=false로 commit 후 resolve) 처리해 주세요.
getContestAccess({ commit, rootState }) {
return new Promise((resolve) => {
api
.getContestAccess(rootState.route.params.contestID)
.then((res) => {
commit(types.CONTEST_ACCESS, { access: res.data.data.access })
resolve(res)
})
.catch()
})
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
frontend/src/store/modules/contest.js:232
getContestAccess액션이new Promise((resolve) => { ... }).catch()형태로 에러를 처리하지 않고 있어, API 호출 실패 시 Promise가 resolve/reject되지 않은 채 pending 상태로 남습니다. 호출 측에서 로딩이 종료되지 않거나 상태가 갱신되지 않는 문제가 생길 수 있으니,.catch(err => { ...; reject(err) })로 reject를 전달하거나 실패 시에도CONTEST_ACCESS를 기본값으로 커밋하고 resolve/reject를 명확히 처리해주세요.
getContestAccess({ commit, rootState }) {
return new Promise((resolve) => {
api
.getContestAccess(rootState.route.params.contestID)
.then((res) => {
commit(types.CONTEST_ACCESS, { access: res.data.data.access })
resolve(res)
})
.catch()
})
| contest_id = request.GET.get("contest_id") | ||
| if not contest_id: | ||
| return self.error("Invalid parameter, contest_id is required") | ||
|
|
||
| try: | ||
| contest = Contest.objects.get(id=contest_id) | ||
| ensure_created_by(contest, request.user) | ||
| except Contest.DoesNotExist: | ||
| return self.error("Contest does not exist") | ||
|
|
There was a problem hiding this comment.
contest_id에 대해 Contest.objects.get(id=contest_id)를 수행하면서, 숫자가 아닌 값(예: "abc")이 들어오면 ValueError가 발생해 server-error(500)로 떨어질 수 있습니다. 현재는 Contest.DoesNotExist만 처리하고 있으니 check_is_id(contest_id)로 선검증하거나 ValueError를 함께 잡아서 클라이언트에 일관된 파라미터 오류를 반환하도록 수정하는 게 안전합니다.
| @@ -172,15 +184,20 @@ def get(self, request): | |||
| for submission in user_submissions: | |||
| user = user_dict.get(submission['user_id']) | |||
There was a problem hiding this comment.
user_submissions QuerySet을 리스트 컴프리헨션과 루프에서 여러 번 순회하고 있어(프로필 조회 1회, 유저 조회 1회, 결과 생성 1회) 동일한 집계 쿼리가 최대 3번 실행됩니다. user_submissions = list(user_submissions)처럼 한 번만 평가한 뒤 재사용하거나, values_list('user_id', flat=True)를 별도로 만들어 재사용하도록 변경하면 불필요한 DB 부하를 줄일 수 있습니다.
| class ContestUserSubmissionSummarySerializer(serializers.Serializer): | ||
| user_id = serializers.IntegerField() | ||
| username = serializers.CharField() | ||
| email = serializers.EmailField() | ||
| avatar = serializers.CharField() | ||
| school = serializers.CharField() | ||
| major = serializers.CharField() | ||
| username = serializers.CharField(allow_blank=True) | ||
| email = serializers.CharField(allow_blank=True) | ||
| avatar = serializers.CharField(allow_blank=True) | ||
| school = serializers.CharField(allow_blank=True) | ||
| major = serializers.CharField(allow_blank=True) | ||
| submission_count = serializers.IntegerField() | ||
| last_submission_ip = serializers.CharField() | ||
| last_submission_ip = serializers.CharField(allow_blank=True) |
There was a problem hiding this comment.
ContestUserSubmissionSummarySerializer에서 email 필드 타입이 EmailField에서 CharField로 바뀌면서, 이메일이 존재하는 경우에도 이메일 형식 검증이 사라집니다. 빈 문자열을 허용하려는 목적이라면 EmailField(allow_blank=True, required=False)(또는 allow_null 포함)처럼 타입 의미를 유지하면서 blank를 허용하는 형태가 API 계약 측면에서 더 안전합니다.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
frontend/src/store/modules/contest.js:232
getContestAccess액션이new Promise((resolve) => { ... })형태로 바뀌면서, API 호출이 실패할 경우(resolve/reject 모두 호출되지 않아) 반환된 Promise가 영원히 pending 상태가 됩니다. 현재.catch()도 핸들러가 없어 에러가 누락/미처리될 수 있으니,reject를 다시 받아서.catch((err)=>reject(err))(또는 async/await로 return) 형태로 실패 케이스를 반드시 종료시키도록 수정해주세요.
getContestAccess({ commit, rootState }) {
return new Promise((resolve) => {
api
.getContestAccess(rootState.route.params.contestID)
.then((res) => {
commit(types.CONTEST_ACCESS, { access: res.data.data.access })
resolve(res)
})
.catch()
})
backend/contest/views/oj.py:181
user_submissions(annotate된 QuerySet)을 리스트 컴프리헨션/for-loop에서 3번 반복 평가하고 있어 동일한 집계 쿼리가 여러 번 실행될 수 있습니다(user_profiles용,users용, 결과 생성 루프).user_submissions = list(user_submissions)로 한 번만 평가한 뒤 재사용하도록 바꾸면 불필요한 DB 부하를 줄일 수 있습니다.
submissions = Submission.objects.filter(contest_id=contest_id)
latest_submission = submissions.filter(user_id=OuterRef('user_id')).order_by('-create_time', '-id')
user_submissions = submissions.values('user_id').annotate(
submission_count=Count('id'),
last_submission_ip=Subquery(latest_submission.values('ip')[:1]),
fallback_username=Subquery(latest_submission.values('username')[:1])).order_by('user_id')
# UserProfile 정보 가져오기
user_profiles = UserProfile.objects.filter(user_id__in=[sub['user_id'] for sub in user_submissions])
user_profile_dict = {profile.user_id: profile for profile in user_profiles}
# User 정보 가져오기
users = User.objects.filter(id__in=[sub['user_id'] for sub in user_submissions])
user_dict = {user.id: user for user in users}
Close #597
Changelog
1. 닉네임을 변경한 사용자가 대회 참가자 목록에 중복 표시되던 문제를 수정했습니다.
user_id로 변경2. 대회 랭킹 페이지에서 문제 툴팁이 문제 번호 대신 문제 제목을 표시하도록 수정했습니다.
3. 대회 랭킹 페이지에서 로딩 중 '제출 현황이 없습니다' 문구가 먼저 노출되지 않도록 스켈레톤 로딩을 적용하였습니다.
4. #597 대회 랭킹에서 오답 제출(WA/CE)과 정답 제출(AC)을 구분할 수 없던 문제를 해결했습니다.
Testing
cd backend && ./venv/bin/python manage.py test contest.tests.ContestParticipantsAPITestOKOps Impact
N/A
Version Compatibility
N/A