-
Notifications
You must be signed in to change notification settings - Fork 3
#601 대회 생성 페이지에 AI 조교 사용 여부 설정 기능 추가 #616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Generated by Django 3.2.25 on 2026-04-23 05:36 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('contest', '0002_contest_allow_paste'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='contest', | ||
| name='ai_assistant_enabled', | ||
| field=models.BooleanField(default=True), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,9 +7,10 @@ | |
| from django.http import HttpResponseBadRequest, HttpResponseNotFound, StreamingHttpResponse | ||
| from django.contrib.auth import get_user_model | ||
|
|
||
| from account.decorators import (check_contest_permission, scheduler_only) | ||
| from account.decorators import (check_contest_permission, check_contest_password, scheduler_only) | ||
| from utils.constants import CONTEST_PASSWORD_SESSION_KEY | ||
| from account.models import UserProfile, UserScore | ||
| from contest.models import ContestRuleType | ||
| from contest.models import Contest, ContestRuleType, ContestStatus, ContestType | ||
| from submission.models import JudgeStatus, Submission | ||
| from utils.api import APIView | ||
| from utils.constants import Difficulty, ProblemField, Tier | ||
|
|
@@ -143,13 +144,38 @@ def get(self, request): | |
| return self._error_response("로그인이 필요합니다.", err="permission-denied") | ||
|
|
||
| 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.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("문제를 찾을 수 없습니다.") | ||
|
Comment on lines
+151
to
+173
|
||
| else: | ||
| try: | ||
| problem = Problem.objects.get(_id=problem_id, contest_id__isnull=True, visible=True) | ||
| except Problem.DoesNotExist: | ||
| return self._error_response("문제를 찾을 수 없습니다.") | ||
|
Comment on lines
146
to
+178
|
||
|
|
||
| hint_log = None | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -275,7 +275,7 @@ export const m = { | |
| Allowed_IP_Ranges: "허용된 IP 범위", | ||
| CIDR_Network: "CIDR 네트워크", | ||
| Allow_Paste: "붙여넣기 허용", | ||
| Contest_Visible: "활성화하면 참가자에게 대회가 노출됩니다.", | ||
| AIAssistant_allow: "AI힌트 기능 허용", | ||
|
Comment on lines
275
to
+278
|
||
|
|
||
|
Comment on lines
275
to
279
|
||
| // ContestList.vue | ||
| Contest_List_Page_Title: "대회 목록", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -124,6 +124,7 @@ | |
| ></span> | ||
| </label> | ||
| </div> | ||
|
|
||
| <div class="toggle-item"> | ||
| <span id="allow_paste_label" class="toggle-label"> | ||
| {{ $t("m.Allow_Paste") }} | ||
|
|
@@ -146,6 +147,30 @@ | |
| ></span> | ||
| </label> | ||
| </div> | ||
|
|
||
| <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> | ||
|
Comment on lines
+151
to
+160
|
||
|
|
||
| <label class="spj-toggle"> | ||
| <input | ||
| type="checkbox" | ||
| v-model="contest.ai_assistant_enabled" | ||
| aria-labelledby="allow_ai_label" | ||
| /> | ||
| <span | ||
| class="spj-toggle-track" | ||
| :class="{ 'is-on': contest.ai_assistant_enabled }" | ||
| ></span> | ||
| </label> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
|
|
@@ -216,6 +241,7 @@ export default { | |
| real_time_rank: true, | ||
| visible: true, | ||
| allow_paste: true, | ||
| ai_assistant_enabled: true, | ||
| allowed_ip_ranges: [{ value: "" }], | ||
| }, | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -187,6 +187,7 @@ export default { | |||||||||||||||||||||||||||||||||
| rightPainActiveTab: "editor", | ||||||||||||||||||||||||||||||||||
| lastSubmissionId: null, | ||||||||||||||||||||||||||||||||||
| isInitialized: false, | ||||||||||||||||||||||||||||||||||
| contestData: null, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| beforeRouteEnter(to, from, next) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -215,6 +216,7 @@ export default { | |||||||||||||||||||||||||||||||||
| this.init() | ||||||||||||||||||||||||||||||||||
| window.addEventListener("beforeunload", this.unLoadEvent) | ||||||||||||||||||||||||||||||||||
| this.isInitialized = true | ||||||||||||||||||||||||||||||||||
| console.log("contest:", this.contest) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| console.log("contest:", this.contest) |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mounted()에 남아있는 console.log는 운영 환경에서 불필요한 로그/노이즈를 유발합니다. 디버깅 목적이라면 제거하거나(권장) 필요 시 개발 환경에서만 출력되도록 가드 처리해 주세요.
| console.log("contest:", this.contest) |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
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/로직에서 사용하도록 연결해 주세요.
| api.getContest(this.contestID).then((res) => { | |
| this.contestData = res.data.data | |
| console.log("contestData: ", this.contestData) | |
| }) |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
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)만 사용하도록 정리해 주세요.
| 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) | |
| }) | |
| } |
There was a problem hiding this comment.
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)하도록 처리하는 편이 안전합니다.