Skip to content

#597 대회 랭킹 페이지 오류 수정 및 디자인 개선#605

Merged
Neibce merged 11 commits into
developfrom
597/fix-contest-ranking
Apr 3, 2026
Merged

#597 대회 랭킹 페이지 오류 수정 및 디자인 개선#605
Neibce merged 11 commits into
developfrom
597/fix-contest-ranking

Conversation

@Neibce
Copy link
Copy Markdown
Member

@Neibce Neibce commented Apr 2, 2026

Close #597

Changelog

1. 닉네임을 변경한 사용자가 대회 참가자 목록에 중복 표시되던 문제를 수정했습니다.

  • 참가자 집계 기준을 user_id로 변경
  • 참가자 목록 API 테스트를 추가했습니다.
  • 기존
image
  • 적용 후
image

2. 대회 랭킹 페이지에서 문제 툴팁이 문제 번호 대신 문제 제목을 표시하도록 수정했습니다.

  • 기존
image
  • 적용 후
image

3. 대회 랭킹 페이지에서 로딩 중 '제출 현황이 없습니다' 문구가 먼저 노출되지 않도록 스켈레톤 로딩을 적용하였습니다.

image

4. #597 대회 랭킹에서 오답 제출(WA/CE)과 정답 제출(AC)을 구분할 수 없던 문제를 해결했습니다.

  • ACM/OI 랭킹에서 제출 상태 표시를 개선했습니다.
  • 정답 제출은 시간 정보와 시도 횟수를 툴팁으로 표시
  • 오답 제출은 WA 상태와 횟수를 별도 배지로 표시
  • 기존 ACM
image
  • 적용 후 ACM
image
  • 기존 OI
image
  • 적용 후 OI / OI 디자인은 맘에 들지 않으나 거의 사용 안하므로 일단 보류..
image

Testing

  • cd backend && ./venv/bin/python manage.py test contest.tests.ContestParticipantsAPITest
  • 결과: OK
  • 프론트 작업은 수동 테스트 완료

Ops Impact

N/A

Version Compatibility

N/A

@Neibce Neibce requested review from Copilot, taekoong and wlsgur11 April 2, 2026 05:07
@Neibce Neibce changed the title #597 /fix contest ranking #597 대회 랭킹 페이지 오류 수정 및 디자인 개선 Apr 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 기준으로 그룹핑되는지 테스트 추가

Comment thread frontend/src/pages/oj/views/contest/children/ACMContestRank.vue
Comment thread frontend/src/pages/oj/views/contest/children/ACMContestRank.vue
Comment thread frontend/src/pages/oj/views/contest/children/OIContestRank.vue Outdated
Comment thread frontend/src/pages/oj/components/CustomTooltip.vue
Comment thread backend/contest/views/oj.py Outdated
@wlsgur11
Copy link
Copy Markdown
Contributor

wlsgur11 commented Apr 2, 2026

테스트까지 진행하다니 멋져요

Copy link
Copy Markdown
Contributor

@wlsgur11 wlsgur11 left a comment

Choose a reason for hiding this comment

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

👍🎖️

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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()
    })

Comment thread backend/contest/views/oj.py
Comment thread backend/contest/views/oj.py
Comment thread frontend/src/pages/oj/views/contest/children/ACMContestRank.vue
Comment thread frontend/src/pages/oj/views/contest/children/ACMContestRank.vue
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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()
    })

Comment thread frontend/config/index.js Outdated
Comment thread backend/contest/views/oj.py
Comment thread backend/contest/views/oj.py
Comment thread frontend/src/pages/oj/views/contest/children/ContestProblemList.vue
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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()
    })

Comment on lines 157 to 166
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")

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

contest_id에 대해 Contest.objects.get(id=contest_id)를 수행하면서, 숫자가 아닌 값(예: "abc")이 들어오면 ValueError가 발생해 server-error(500)로 떨어질 수 있습니다. 현재는 Contest.DoesNotExist만 처리하고 있으니 check_is_id(contest_id)로 선검증하거나 ValueError를 함께 잡아서 클라이언트에 일관된 파라미터 오류를 반환하도록 수정하는 게 안전합니다.

Copilot uses AI. Check for mistakes.
Comment on lines 170 to 185
@@ -172,15 +184,20 @@ def get(self, request):
for submission in user_submissions:
user = user_dict.get(submission['user_id'])
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

user_submissions QuerySet을 리스트 컴프리헨션과 루프에서 여러 번 순회하고 있어(프로필 조회 1회, 유저 조회 1회, 결과 생성 1회) 동일한 집계 쿼리가 최대 3번 실행됩니다. user_submissions = list(user_submissions)처럼 한 번만 평가한 뒤 재사용하거나, values_list('user_id', flat=True)를 별도로 만들어 재사용하도록 변경하면 불필요한 DB 부하를 줄일 수 있습니다.

Copilot uses AI. Check for mistakes.
Comment on lines 9 to +17
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)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

ContestUserSubmissionSummarySerializer에서 email 필드 타입이 EmailField에서 CharField로 바뀌면서, 이메일이 존재하는 경우에도 이메일 형식 검증이 사라집니다. 빈 문자열을 허용하려는 목적이라면 EmailField(allow_blank=True, required=False)(또는 allow_null 포함)처럼 타입 의미를 유지하면서 blank를 허용하는 형태가 API 계약 측면에서 더 안전합니다.

Copilot uses AI. Check for mistakes.
@Neibce Neibce requested a review from Copilot April 3, 2026 07:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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}

@Neibce Neibce merged commit fbd3cc1 into develop Apr 3, 2026
4 checks passed
@Neibce Neibce deleted the 597/fix-contest-ranking branch April 3, 2026 07:37
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.

[Bug] 대회 랭킹 페이지에서 오답을 받은 제출임에도 랭킹에 표시됨

3 participants