Conversation
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
mounted()에 console.log("contest:", this.contest) 디버그 로그가 남아 있습니다. 프로덕션 콘솔 오염 및 개인정보 노출 가능성이 있으니 제거해 주세요.
| console.log("contest:", this.contest) |
| api.getContest(this.contestID).then((res) => { | ||
| this.contestData = res.data.data | ||
| console.log("contestData: ", this.contestData) | ||
| }) |
There was a problem hiding this comment.
contestData를 data에 추가하고 api.getContest()로 값을 세팅하지만, 이 컴포넌트 내에서 contestData가 사용되지 않고 console.log만 남아 있습니다. 불필요한 상태/네트워크 호출이므로 제거하거나, 실제로 필요한 UI/로직에서 사용하도록 연결해 주세요.
| api.getContest(this.contestID).then((res) => { | |
| this.contestData = res.data.data | |
| console.log("contestData: ", this.contestData) | |
| }) |
| @@ -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), | |||
| ) | |||
There was a problem hiding this comment.
이제 requestHint()에서 대회 문제도 힌트를 요청할 수 있게 되었지만, fetchHintHistory()는 this.contestID가 있으면 즉시 return 하도록 되어 있어(대회에서는 히스토리/사용횟수 갱신이 전혀 안 됨) 남은 횟수 표시가 항상 5회로 고정되고 hintsExhausted로 버튼 비활성화도 되지 않습니다. 백엔드가 5회 제한을 적용하고 있으니, (1) 대회용 히스토리/잔여횟수 조회 API를 추가하거나 ai_hint_history가 contest_id를 지원하게 하고, (2) 프론트에서 대회일 때도 사용횟수를 갱신하도록 로직을 정리해 주세요.
| 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("문제를 찾을 수 없습니다.") |
There was a problem hiding this comment.
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 조건을 포함하도록 수정해 주세요.
| 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("문제를 찾을 수 없습니다.") |
There was a problem hiding this comment.
contest_id 경로가 새로 추가되었는데 이에 대한 테스트가 보이지 않습니다. 기존 ProblemLLMHintAPITest에 (1) contest_id를 넘기면 대회 문제에 대해 정상 SSE 응답이 오는지, (2) ai_assistant_enabled=False일 때 app-error가 내려오는지, (3) 비밀번호/시작 전 등 대회 권한 조건이 지켜지는지 케이스를 추가해 주세요.
| Allowed_IP_Ranges: "허용된 IP 범위", | ||
| CIDR_Network: "CIDR 네트워크", | ||
| Allow_Paste: "붙여넣기 허용", | ||
| Contest_Visible: "활성화하면 참가자에게 대회가 노출됩니다.", | ||
| AIAssistant_allow: "AI힌트 기능 허용", |
There was a problem hiding this comment.
m.AIAssistant_allow 키가 en-US에만 추가되어 있고 frontend/src/i18n/admin/zh-CN.js, zh-TW.js에는 동일 키가 없습니다. 다국어 지원을 위해 다른 로케일 파일에도 동일 키를 추가(번역 값 포함)해 주세요. 그렇지 않으면 해당 언어에서 키 문자열이 그대로 노출됩니다.
| <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> |
There was a problem hiding this comment.
새로 추가한 AI 허용 토글의 라벨 span이 class="toggle_label"로 되어 있는데, 이 파일 내에서 정의/사용하는 클래스는 .toggle-label입니다. 현재 상태면 스타일이 적용되지 않으므로 기존 토글들과 동일하게 toggle-label로 수정해 주세요.
| this.$store.dispatch("getContest", this.contestID) | ||
|
|
||
| api.getContest(this.contestID).then((res) => { | ||
| this.contestData = res.data.data | ||
| console.log("contestData: ", this.contestData) | ||
| }) |
There was a problem hiding this comment.
init()에서 this.contestID가 없는(일반 문제 풀이) 라우트에서도 getContest 디스패치와 api.getContest(this.contestID) 호출이 실행되어 /api/contest?id=undefined 요청이 발생할 수 있습니다. 또한 동일 데이터를 Vuex 액션과 직접 API 호출로 이중 조회하고 있어 중복입니다. contestID가 있을 때만 조회하도록 가드하고, 한쪽(Vuex 액션 또는 로컬 state)만 사용하도록 정리해 주세요.
| 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) | |
| }) | |
| } |
| 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() |
There was a problem hiding this comment.
EditConetestSeriaizer에서 ai_assistant_enabled를 필수(BooleanField())로 추가하면서, 구버전 프론트엔드/클라이언트가 해당 필드를 보내지 않으면 대회 수정 PUT이 바로 400으로 실패합니다(롤링 배포/호환성 리스크). Create serializer처럼 default=True를 주거나 required=False로 두고 뷰에서 기존 값 유지(fallback)하도록 처리하는 편이 안전합니다.
| ai_assistant_enabled = serializers.BooleanField() | |
| ai_assistant_enabled = serializers.BooleanField(required=False) |
| Allowed_IP_Ranges: "허용된 IP 범위", | ||
| CIDR_Network: "CIDR 네트워크", | ||
| Allow_Paste: "붙여넣기 허용", | ||
| Contest_Visible: "활성화하면 참가자에게 대회가 노출됩니다.", | ||
| AIAssistant_allow: "AI힌트 기능 허용", | ||
|
|
There was a problem hiding this comment.
관리자 i18n 키(m.AIAssistant_allow)를 en-US에만 추가하면, zh-CN/zh-TW 로케일에서는 해당 토글 라벨이 누락(키 그대로 노출)될 수 있습니다. 동일 키를 다른 admin 로케일 파일에도 추가해 주세요.
| this.init() | ||
| window.addEventListener("beforeunload", this.unLoadEvent) | ||
| this.isInitialized = true | ||
| console.log("contest:", this.contest) |
There was a problem hiding this comment.
mounted()에 남아있는 console.log는 운영 환경에서 불필요한 로그/노이즈를 유발합니다. 디버깅 목적이라면 제거하거나(권장) 필요 시 개발 환경에서만 출력되도록 가드 처리해 주세요.
| console.log("contest:", this.contest) |
| 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("문제를 찾을 수 없습니다.") |
There was a problem hiding this comment.
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로 내려주세요.
Changelog
대회 생성/수정 UI에 AI 조교 허용 토글 추가
문제 풀이 시 제출 버튼 좌측의 'AI조교 힌트받기' 버튼 활성화 여부
ai_assistant_enabled값 기준으로 표시Testing
2026-04-23.4.26.25.mov
Ops Impact
DB 마이그레이션 필요: contest 테이블에 ai_assistant_enabled 컬럼이 추가되었습니다.
- 기존 대회 레코드는 기본값 True로 설정되므로 기존 동작 유지됨
- 배포 전 python manage.py migrate 실행 필요
Version Compatibility