From 1a4a299b6e3c23dc0ef3de177041d20a7d60d384 Mon Sep 17 00:00:00 2001 From: taekoong Date: Thu, 23 Apr 2026 15:56:00 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=8C=80=ED=9A=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20AI=20=EC=A1=B0=EA=B5=90=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20on/off?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0003_contest_ai_assistant_enabled.py | 18 +++++++++++++ backend/contest/models.py | 2 ++ backend/contest/serializers.py | 2 ++ backend/problem/views/oj.py | 23 ++++++++++++---- frontend/src/i18n/admin/en-US.js | 2 +- .../src/pages/admin/views/contest/Contest.vue | 26 +++++++++++++++++++ frontend/src/pages/oj/api.js | 6 +++-- .../views/problem/problemSolving/Problem.vue | 9 +++++++ .../AIAssistantBtn.vue | 12 ++++++++- .../problemSolvingComponent/BottomDrag.vue | 13 +--------- 10 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 backend/contest/migrations/0003_contest_ai_assistant_enabled.py diff --git a/backend/contest/migrations/0003_contest_ai_assistant_enabled.py b/backend/contest/migrations/0003_contest_ai_assistant_enabled.py new file mode 100644 index 000000000..0ad29fe85 --- /dev/null +++ b/backend/contest/migrations/0003_contest_ai_assistant_enabled.py @@ -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), + ), + ] diff --git a/backend/contest/models.py b/backend/contest/models.py index 090beb481..09b58ecb4 100644 --- a/backend/contest/models.py +++ b/backend/contest/models.py @@ -25,6 +25,8 @@ class Contest(models.Model): # 是否可见 false的话相当于删除 visible = models.BooleanField(default=True) allowed_ip_ranges = JSONField(default=list) + #AI assistant + ai_assistant_enabled = models.BooleanField(default=True) @property def status(self): diff --git a/backend/contest/serializers.py b/backend/contest/serializers.py index d9a884f1d..9ba483812 100644 --- a/backend/contest/serializers.py +++ b/backend/contest/serializers.py @@ -28,6 +28,7 @@ class CreateConetestSeriaizer(serializers.Serializer): real_time_rank = serializers.BooleanField() allow_paste = serializers.BooleanField() allowed_ip_ranges = serializers.ListField(child=serializers.CharField(max_length=32), allow_empty=True) + ai_assistant_enabled = serializers.BooleanField(default=True) class EditConetestSeriaizer(serializers.Serializer): @@ -41,6 +42,7 @@ class EditConetestSeriaizer(serializers.Serializer): 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() class ContestAdminSerializer(serializers.ModelSerializer): diff --git a/backend/problem/views/oj.py b/backend/problem/views/oj.py index fb16ba5cb..86acb9983 100644 --- a/backend/problem/views/oj.py +++ b/backend/problem/views/oj.py @@ -8,7 +8,7 @@ from account.decorators import (check_contest_permission, scheduler_only) from account.models import UserProfile, UserScore -from contest.models import ContestRuleType +from contest.models import Contest, ContestRuleType from submission.models import JudgeStatus, Submission from utils.api import APIView from utils.constants import Difficulty, ProblemField, Tier @@ -142,13 +142,26 @@ 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.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("문제를 찾을 수 없습니다.") def generator(): try: diff --git a/frontend/src/i18n/admin/en-US.js b/frontend/src/i18n/admin/en-US.js index 1d54e8d3c..e887ba83e 100644 --- a/frontend/src/i18n/admin/en-US.js +++ b/frontend/src/i18n/admin/en-US.js @@ -275,7 +275,7 @@ export const m = { Allowed_IP_Ranges: "허용된 IP 범위", CIDR_Network: "CIDR 네트워크", Allow_Paste: "붙여넣기 허용", - Contest_Visible: "활성화하면 참가자에게 대회가 노출됩니다.", + AIAssistant_allow: "AI힌트 기능 허용", // ContestList.vue Contest_List_Page_Title: "대회 목록", diff --git a/frontend/src/pages/admin/views/contest/Contest.vue b/frontend/src/pages/admin/views/contest/Contest.vue index d87ef5ab9..39350ca10 100644 --- a/frontend/src/pages/admin/views/contest/Contest.vue +++ b/frontend/src/pages/admin/views/contest/Contest.vue @@ -124,6 +124,7 @@ > +
{{ $t("m.Allow_Paste") }} @@ -146,6 +147,30 @@ >
+ +
+ + {{ $t("m.AIAssistant_allow") }} + + + + + + +
@@ -216,6 +241,7 @@ export default { real_time_rank: true, visible: true, allow_paste: true, + ai_assistant_enabled: true, allowed_ip_ranges: [{ value: "" }], }, } diff --git a/frontend/src/pages/oj/api.js b/frontend/src/pages/oj/api.js index b51cd1caf..226b76661 100644 --- a/frontend/src/pages/oj/api.js +++ b/frontend/src/pages/oj/api.js @@ -209,8 +209,10 @@ export default { }, }) }, - getProblemLLMHintUrl(problemID) { - return `/api/problem/llm_hint?problem_id=${encodeURIComponent(problemID)}` + getProblemLLMHintUrl(problemID, contestID) { + let url = `/api/problem/llm_hint?problem_id=${encodeURIComponent(problemID)}` + if (contestID) url += `&contest_id=${encodeURIComponent(contestID)}` + return url }, getContestList(offset, limit, searchParams) { let params = { diff --git a/frontend/src/pages/oj/views/problem/problemSolving/Problem.vue b/frontend/src/pages/oj/views/problem/problemSolving/Problem.vue index 3f5f9c289..e84cc45c4 100644 --- a/frontend/src/pages/oj/views/problem/problemSolving/Problem.vue +++ b/frontend/src/pages/oj/views/problem/problemSolving/Problem.vue @@ -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) }, beforeUnmount() { window.removeEventListener("beforeunload", this.unLoadEvent) @@ -233,6 +235,13 @@ export default { this.$Loading.start() this.contestID = this.$route.params.contestID this.problemID = this.$route.params.problemID + + this.$store.dispatch("getContest", this.contestID) + + api.getContest(this.contestID).then((res) => { + this.contestData = res.data.data + console.log("contestData: ", this.contestData) + }) let func = this.$route.name === "problem-details" ? "getProblem" diff --git a/frontend/src/pages/oj/views/problem/problemSolving/problemSolvingComponent/AIAssistantBtn.vue b/frontend/src/pages/oj/views/problem/problemSolving/problemSolvingComponent/AIAssistantBtn.vue index 6e2542a39..08cf5d29a 100644 --- a/frontend/src/pages/oj/views/problem/problemSolving/problemSolvingComponent/AIAssistantBtn.vue +++ b/frontend/src/pages/oj/views/problem/problemSolving/problemSolvingComponent/AIAssistantBtn.vue @@ -1,5 +1,10 @@