Skip to content

#601 대회 생성 페이지에 AI 조교 사용 여부 설정 기능 추가#616

Open
taekoong wants to merge 3 commits intodevelopfrom
admin-ai-assistant-onoff
Open

#601 대회 생성 페이지에 AI 조교 사용 여부 설정 기능 추가#616
taekoong wants to merge 3 commits intodevelopfrom
admin-ai-assistant-onoff

Conversation

@taekoong
Copy link
Copy Markdown
Contributor

Changelog

  • 대회 생성/수정 UI에 AI 조교 허용 토글 추가

    • 대회 주최자가 AI 조교 기능을 활성화할 수 있도록 토글을 추가하였습니다.
  • 문제 풀이 시 제출 버튼 좌측의 'AI조교 힌트받기' 버튼 활성화 여부

    • 대회 문제: ai_assistant_enabled 값 기준으로 표시
    • AI조교 사용이 거부된 대회 내 문제는 AI조교 힌트받기 버튼이 보이지 않고,
    • 허용된 대회 내 문제는 AI조교 버튼이 보이며, AI조교 기능이 정상 작동하도록 하였습니다.

Testing

2026-04-23.4.26.25.mov

Ops Impact

DB 마이그레이션 필요: contest 테이블에 ai_assistant_enabled 컬럼이 추가되었습니다.
- 기존 대회 레코드는 기본값 True로 설정되므로 기존 동작 유지됨
- 배포 전 python manage.py migrate 실행 필요

Version Compatibility

  • 마이그레이션 미적용 상태에서 대회 생성/수정 API 호출 시 오류 발생 가능

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

대회 생성/수정 및 대회 문제 풀이 화면에서 AI 조교(힌트) 기능의 허용 여부를 대회 단위로 제어할 수 있도록 확장한 PR입니다. DB에 contest.ai_assistant_enabled 컬럼을 추가하고, 프론트에서는 토글/버튼 노출 및 힌트 요청 시 contest_id를 함께 전달하도록 변경합니다.

Changes:

  • Admin 대회 생성/수정 화면에 ai_assistant_enabled 토글 추가 및 기본값(True) 반영
  • 대회 문제 풀이 화면에서 AI 조교 버튼 노출을 contest.ai_assistant_enabled로 제어
  • LLM 힌트 SSE API가 contest_id를 받아 대회 문제에도 힌트를 제공하도록 확장(+ 마이그레이션 추가)

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
frontend/src/pages/oj/views/problem/problemSolving/problemSolvingComponent/BottomDrag.vue 힌트 요청 URL에 contestID를 포함하도록 변경(대회 힌트 요청 경로 연결)
frontend/src/pages/oj/views/problem/problemSolving/problemSolvingComponent/AIAssistantBtn.vue 대회에서는 contest.ai_assistant_enabled에 따라 AI 버튼을 조건부 렌더링
frontend/src/pages/oj/views/problem/problemSolving/Problem.vue 대회 정보 로딩 시도 추가(현재 중복 호출/가드 이슈 및 디버그 로그 포함)
frontend/src/pages/oj/api.js LLM 힌트 URL 생성 함수가 contest_id 쿼리를 지원하도록 변경
frontend/src/pages/admin/views/contest/Contest.vue Admin에서 AI 힌트 허용 토글 UI 추가(라벨 클래스 오타 이슈 존재)
frontend/src/i18n/admin/en-US.js Admin i18n에 AIAssistant_allow 키 추가(타 로케일 누락 이슈 존재)
backend/problem/views/oj.py LLM 힌트 API가 contest_id로 대회 문제를 조회/검증하도록 확장(권한검증 미흡 이슈 존재)
backend/contest/serializers.py 대회 생성/수정 serializer에 ai_assistant_enabled 필드 추가
backend/contest/models.py Contest 모델에 ai_assistant_enabled 필드 추가
backend/contest/migrations/0003_contest_ai_assistant_enabled.py contest.ai_assistant_enabled 컬럼 마이그레이션 추가

this.init()
window.addEventListener("beforeunload", this.unLoadEvent)
this.isInitialized = true
console.log("contest:", this.contest)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

mounted()console.log("contest:", this.contest) 디버그 로그가 남아 있습니다. 프로덕션 콘솔 오염 및 개인정보 노출 가능성이 있으니 제거해 주세요.

Suggested change
console.log("contest:", this.contest)

Copilot uses AI. Check for mistakes.
Comment on lines +241 to +244
api.getContest(this.contestID).then((res) => {
this.contestData = res.data.data
console.log("contestData: ", this.contestData)
})
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

contestData를 data에 추가하고 api.getContest()로 값을 세팅하지만, 이 컴포넌트 내에서 contestData가 사용되지 않고 console.log만 남아 있습니다. 불필요한 상태/네트워크 호출이므로 제거하거나, 실제로 필요한 UI/로직에서 사용하도록 연결해 주세요.

Suggested change
api.getContest(this.contestID).then((res) => {
this.contestData = res.data.data
console.log("contestData: ", this.contestData)
})

Copilot uses AI. Check for mistakes.
Comment on lines 151 to 162
@@ -170,7 +158,7 @@ export default {
const thinkingIndex = this.messages.length - 1

const eventSource = new EventSource(
api.getProblemLLMHintUrl(this.problemID),
api.getProblemLLMHintUrl(this.problemID, this.contestID),
)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

이제 requestHint()에서 대회 문제도 힌트를 요청할 수 있게 되었지만, fetchHintHistory()this.contestID가 있으면 즉시 return 하도록 되어 있어(대회에서는 히스토리/사용횟수 갱신이 전혀 안 됨) 남은 횟수 표시가 항상 5회로 고정되고 hintsExhausted로 버튼 비활성화도 되지 않습니다. 백엔드가 5회 제한을 적용하고 있으니, (1) 대회용 히스토리/잔여횟수 조회 API를 추가하거나 ai_hint_historycontest_id를 지원하게 하고, (2) 프론트에서 대회일 때도 사용횟수를 갱신하도록 로직을 정리해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +160
if contest_id:
try:
contest = Contest.objects.get(id=contest_id)
except Contest.DoesNotExist:
return self._error_response("대회를 찾을 수 없습니다.")
if not contest.ai_assistant_enabled:
return self._error_response("이 대회에서는 AI 조교를 사용할 수 없습니다.")
try:
problem = Problem.objects.get(_id=problem_id, contest_id=contest_id)
except Problem.DoesNotExist:
return self._error_response("문제를 찾을 수 없습니다.")
Copy link

Copilot AI Apr 27, 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)로 대회를 가져온 뒤 ai_assistant_enabled만 확인하므로, 로그인한 사용자가 contest_id/problem_id를 추측하기만 하면 비공개(비밀번호) 대회/시작 전 대회/visible=False 대회 및 visible=False 문제에 대해서도 AI 힌트를 요청할 수 있습니다. ContestProblemAPI가 사용하는 check_contest_permission(check_type="problems")와 동일한 기준(visible=True, 비밀번호 세션, 시작 전 차단 등)으로 검증하고, 문제 조회도 visible=True 조건을 포함하도록 수정해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines 145 to +165
problem_id = request.GET.get("problem_id")
contest_id = request.GET.get("contest_id")
if not problem_id:
return self._error_response("문제 번호가 필요합니다.")

try:
problem = Problem.objects.get(_id=problem_id, contest_id__isnull=True, visible=True)
except Problem.DoesNotExist:
return self._error_response("문제를 찾을 수 없습니다.")
if contest_id:
try:
contest = Contest.objects.get(id=contest_id)
except Contest.DoesNotExist:
return self._error_response("대회를 찾을 수 없습니다.")
if not contest.ai_assistant_enabled:
return self._error_response("이 대회에서는 AI 조교를 사용할 수 없습니다.")
try:
problem = Problem.objects.get(_id=problem_id, contest_id=contest_id)
except Problem.DoesNotExist:
return self._error_response("문제를 찾을 수 없습니다.")
else:
try:
problem = Problem.objects.get(_id=problem_id, contest_id__isnull=True, visible=True)
except Problem.DoesNotExist:
return self._error_response("문제를 찾을 수 없습니다.")
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

contest_id 경로가 새로 추가되었는데 이에 대한 테스트가 보이지 않습니다. 기존 ProblemLLMHintAPITest에 (1) contest_id를 넘기면 대회 문제에 대해 정상 SSE 응답이 오는지, (2) ai_assistant_enabled=False일 때 app-error가 내려오는지, (3) 비밀번호/시작 전 등 대회 권한 조건이 지켜지는지 케이스를 추가해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines 275 to +278
Allowed_IP_Ranges: "허용된 IP 범위",
CIDR_Network: "CIDR 네트워크",
Allow_Paste: "붙여넣기 허용",
Contest_Visible: "활성화하면 참가자에게 대회가 노출됩니다.",
AIAssistant_allow: "AI힌트 기능 허용",
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

m.AIAssistant_allow 키가 en-US에만 추가되어 있고 frontend/src/i18n/admin/zh-CN.js, zh-TW.js에는 동일 키가 없습니다. 다국어 지원을 위해 다른 로케일 파일에도 동일 키를 추가(번역 값 포함)해 주세요. 그렇지 않으면 해당 언어에서 키 문자열이 그대로 노출됩니다.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +160
<div class="toggle-item">
<span id="allow_ai_label" class="toggle_label">
{{ $t("m.AIAssistant_allow") }}
<el-tooltip
content="대회 진행 중 AI힌트 기능을 허용합니다."
placement="top"
>
<i class="el-icon-question help-icon"></i>
</el-tooltip>
</span>
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

새로 추가한 AI 허용 토글의 라벨 span이 class="toggle_label"로 되어 있는데, 이 파일 내에서 정의/사용하는 클래스는 .toggle-label입니다. 현재 상태면 스타일이 적용되지 않으므로 기존 토글들과 동일하게 toggle-label로 수정해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +239 to +244
this.$store.dispatch("getContest", this.contestID)

api.getContest(this.contestID).then((res) => {
this.contestData = res.data.data
console.log("contestData: ", this.contestData)
})
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

init()에서 this.contestID가 없는(일반 문제 풀이) 라우트에서도 getContest 디스패치와 api.getContest(this.contestID) 호출이 실행되어 /api/contest?id=undefined 요청이 발생할 수 있습니다. 또한 동일 데이터를 Vuex 액션과 직접 API 호출로 이중 조회하고 있어 중복입니다. contestID가 있을 때만 조회하도록 가드하고, 한쪽(Vuex 액션 또는 로컬 state)만 사용하도록 정리해 주세요.

Suggested change
this.$store.dispatch("getContest", this.contestID)
api.getContest(this.contestID).then((res) => {
this.contestData = res.data.data
console.log("contestData: ", this.contestData)
})
if (this.contestID) {
api.getContest(this.contestID).then((res) => {
this.contestData = res.data.data
console.log("contestData: ", this.contestData)
})
}

Copilot uses AI. Check for mistakes.
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 11 out of 11 changed files in this pull request and generated 4 comments.

real_time_rank = serializers.BooleanField()
allow_paste = serializers.BooleanField()
allowed_ip_ranges = serializers.ListField(child=serializers.CharField(max_length=32))
ai_assistant_enabled = serializers.BooleanField()
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

EditConetestSeriaizer에서 ai_assistant_enabled를 필수(BooleanField())로 추가하면서, 구버전 프론트엔드/클라이언트가 해당 필드를 보내지 않으면 대회 수정 PUT이 바로 400으로 실패합니다(롤링 배포/호환성 리스크). Create serializer처럼 default=True를 주거나 required=False로 두고 뷰에서 기존 값 유지(fallback)하도록 처리하는 편이 안전합니다.

Suggested change
ai_assistant_enabled = serializers.BooleanField()
ai_assistant_enabled = serializers.BooleanField(required=False)

Copilot uses AI. Check for mistakes.
Comment on lines 275 to 279
Allowed_IP_Ranges: "허용된 IP 범위",
CIDR_Network: "CIDR 네트워크",
Allow_Paste: "붙여넣기 허용",
Contest_Visible: "활성화하면 참가자에게 대회가 노출됩니다.",
AIAssistant_allow: "AI힌트 기능 허용",

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

관리자 i18n 키(m.AIAssistant_allow)를 en-US에만 추가하면, zh-CN/zh-TW 로케일에서는 해당 토글 라벨이 누락(키 그대로 노출)될 수 있습니다. 동일 키를 다른 admin 로케일 파일에도 추가해 주세요.

Copilot uses AI. Check for mistakes.
this.init()
window.addEventListener("beforeunload", this.unLoadEvent)
this.isInitialized = true
console.log("contest:", this.contest)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

mounted()에 남아있는 console.log는 운영 환경에서 불필요한 로그/노이즈를 유발합니다. 디버깅 목적이라면 제거하거나(권장) 필요 시 개발 환경에서만 출력되도록 가드 처리해 주세요.

Suggested change
console.log("contest:", this.contest)

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +173
if contest_id:
try:
contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True)
except Contest.DoesNotExist:
return self._error_response("대회를 찾을 수 없습니다.", err="permission-denied")

if not request.user.is_contest_admin(contest):
if contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST:
if not check_contest_password(
request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get(contest.id),
contest.password,
):
return self._error_response("비밀번호가 올바르지 않거나 만료되었습니다.", err="permission-denied")

if contest.status == ContestStatus.CONTEST_NOT_START:
return self._error_response("아직 시작하지 않은 대회입니다.", err="permission-denied")

if not contest.ai_assistant_enabled:
return self._error_response("이 대회에서는 AI 조교를 사용할 수 없습니다.")
try:
problem = Problem.objects.get(_id=problem_id, contest_id=contest_id, visible=True)
except Problem.DoesNotExist:
return self._error_response("문제를 찾을 수 없습니다.")
Copy link

Copilot AI Apr 27, 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, ...) / Problem.objects.get(... contest_id=contest_id ...)에서 contest_id가 숫자가 아니면 Django가 ValueError(“expected a number”)를 발생시켜 500으로 터질 수 있습니다(DoesNotExist로는 잡히지 않음). contest_id를 정수로 파싱/검증(check_is_id 등)한 뒤 조회하거나 ValueError/TypeError까지 함께 catch해서 SSE app-error로 내려주세요.

Copilot uses AI. Check for mistakes.
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