diff --git a/backend/contents/views/oj.py b/backend/contents/views/oj.py index 3ba6c3035..5dc485b69 100644 --- a/backend/contents/views/oj.py +++ b/backend/contents/views/oj.py @@ -3,32 +3,32 @@ from contest.models import Contest from problem.models import Problem from utils.api import APIView +from django.core.cache import cache from django.utils.timezone import now import requests import xml.etree.ElementTree as ET from utils.constants import RSS_FEED_URL +HOME_STATS_CACHE_KEY = "home_statistics_v2" +HOME_STATS_CACHE_TTL = 60 * 10 # 10분 + +RSS_CACHE_KEY = "home_rss_feed" +RSS_CACHE_TTL = 60 * 30 # 30분 + class GetHomeStatisticsAPI(APIView): def get(self, request): - """ - 총 문제 수, 채점이 완료된 문제 수, 마감된 대회 수를 반환하는 API - """ - problems = Problem.objects.all().filter(visible=True) + cached = cache.get(HOME_STATS_CACHE_KEY) + if cached: + return self.success(cached) - # 총 문제 수 + problems = Problem.objects.filter(visible=True) total_problem_length = problems.count() - - # 한번이라도 accept가 된 문제 수 accepted_problem_length = problems.filter(accepted_number__gt=0).count() - # 마감된 대회 수 - contests = Contest.objects.select_related("created_by").filter(visible=True) - cur = now() - contests = contests.filter(end_time__gt=cur) - ended_contest_length = contests.count() + ended_contest_length = Contest.objects.filter(visible=True).count() home_statistics = { "total_problem_length": total_problem_length, @@ -36,7 +36,9 @@ def get(self, request): "ended_contest_length": ended_contest_length, } - return self.success(HomeStatistics(home_statistics).data) + data = HomeStatistics(home_statistics).data + cache.set(HOME_STATS_CACHE_KEY, data, HOME_STATS_CACHE_TTL) + return self.success(data) class GetHomeAnnouncementAPI(APIView): @@ -55,33 +57,32 @@ def get(self, request): """ RSS 공지사항을 JSON으로 파싱하여 반환하는 API """ - - # RSS 피드 가져오기 - response = requests.get(RSS_FEED_URL) - - if response.status_code == 200: - # XML 파싱 - root = ET.fromstring(response.content) - - # 필요한 정보 추출 - BASE_URL = "https://swedu.pusan.ac.kr" - items = [] - for item in root.findall('.//item')[:5]: - link = item.find('link').text or '' - # RSS가 상대 경로(/bbs/...)로 반환할 경우 base URL을 붙여줌 - if link and not link.startswith('http'): - link = BASE_URL + link - item_dict = { - 'title': item.find('title').text.rstrip("}"), - 'link': link, - 'pubDate': item.find('pubDate').text - } - items.append(item_dict) - - # Serializer를 사용하여 데이터 직렬화 - serializer = RSSItemSerializer(items, many=True) - - # JsonResponse로 반환 - return self.success(serializer.data) - else: - self.error("Failed to fetch RSS feed") + cached = cache.get(RSS_CACHE_KEY) + if cached: + return self.success(cached) + + try: + response = requests.get(RSS_FEED_URL, timeout=5) + except requests.RequestException: + return self.error("Failed to fetch RSS feed") + + if response.status_code != 200: + return self.error("Failed to fetch RSS feed") + + root = ET.fromstring(response.content) + BASE_URL = "https://swedu.pusan.ac.kr" + items = [] + for item in root.findall('.//item')[:5]: + link = item.find('link').text or '' + if link and not link.startswith('http'): + link = BASE_URL + link + item_dict = { + 'title': item.find('title').text.rstrip("}"), + 'link': link, + 'pubDate': item.find('pubDate').text + } + items.append(item_dict) + + data = RSSItemSerializer(items, many=True).data + cache.set(RSS_CACHE_KEY, data, RSS_CACHE_TTL) + return self.success(data) diff --git a/backend/problem/urls/oj.py b/backend/problem/urls/oj.py index d50141df9..4406a3934 100644 --- a/backend/problem/urls/oj.py +++ b/backend/problem/urls/oj.py @@ -2,7 +2,7 @@ from ..views.oj import (BonusProblemAPI, ContestProblemAPI, MostDifficultProblemAPI, PickOneAPI, ProblemAPI, ProblemLLMHintAPI, ProblemTagAPI, RecommendProblemAPI, UpdateBonusProblemAPI, - UpdateWeeklyStatsAPI, AIHintHistoryAPI) + UpdateWeeklyStatsAPI, AIHintHistoryAPI, WeeklyTopProblemsAPI) urlpatterns = [ url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api"), @@ -10,6 +10,7 @@ url(r"^problem/llm_hint/?$", ProblemLLMHintAPI.as_view(), name="problem_llm_hint_api"), url(r"^problem/ai_hint_history/?$", AIHintHistoryAPI.as_view(), name="problem_ai_hint_history_api"), url(r"^problem/bonus/?$", BonusProblemAPI.as_view(), name="bonus_problem_api"), + url(r"^problem/weekly_top/?$", WeeklyTopProblemsAPI.as_view(), name="weekly_top_problems_api"), url(r"^pickone/?$", PickOneAPI.as_view(), name="pick_one_api"), url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api"), url(r"^recommend_problem/?$", RecommendProblemAPI.as_view(), name="recommend_problem_api"), diff --git a/backend/problem/views/oj.py b/backend/problem/views/oj.py index dbdbeb18d..af383f7c6 100644 --- a/backend/problem/views/oj.py +++ b/backend/problem/views/oj.py @@ -44,6 +44,34 @@ def get(self, request): return self.success(problems[random.randint(0, count - 1)]._id) +class WeeklyTopProblemsAPI(APIView): + + def get(self, request): + problems = Problem.objects.filter( + contest_id__isnull=True, + visible=True, + ).values("_id", "title", "difficulty", "field", "curr_week_info") + + sorted_problems = sorted( + problems, + key=lambda p: p["curr_week_info"].get("accepted", 0), + reverse=True, + )[:3] + + data = [ + { + "_id": p["_id"], + "title": p["title"], + "difficulty": p["difficulty"], + "field": p["field"], + "accepted": p["curr_week_info"].get("accepted", 0), + "submission": p["curr_week_info"].get("submission", 0), + } + for p in sorted_problems + ] + return self.success(data) + + class BonusProblemAPI(APIView): def get(self, request): diff --git a/backend/submission/views/oj.py b/backend/submission/views/oj.py index f8d22e9b2..6b1a9f415 100644 --- a/backend/submission/views/oj.py +++ b/backend/submission/views/oj.py @@ -1,6 +1,7 @@ import ipaddress from django.db.models import Q, Count +from django.utils.dateparse import parse_datetime from account.decorators import login_required, check_contest_permission from utils.testcase_cache import TestCaseCacheManager @@ -191,6 +192,12 @@ def get(self, request): if result: submissions = submissions.filter(result=result) + since = request.GET.get("since") + if since: + since_dt = parse_datetime(since) + if since_dt: + submissions = submissions.filter(create_time__gte=since_dt) + data = self.paginate_data(request, submissions) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) diff --git a/docs/content/about/codeplace.html b/docs/content/about/codeplace.html new file mode 100644 index 000000000..850927dd2 --- /dev/null +++ b/docs/content/about/codeplace.html @@ -0,0 +1,869 @@ + + + + + + PNU CodePlace — 부산대학교 알고리즘 플랫폼 + + + + + + + + + +
+
+
+
+
PNU.CODEPLACE — v2.0
+

+ 알고리즘을
+ 정복하라
+ CODE PLACE +

+

+ 부산대학교 공식 코딩 플랫폼.
+ 실전 코딩테스트, 수준별 알고리즘 문제, 실시간 채점까지— +

+ +
+ +
+ + +
+
+
실시간 채점
+
다크모드 지원
+
개인 맞춤 문제 추천
+
학과별 랭킹
+
티어 시스템
+
온라인 코드 에디터
+
코딩테스트 & 대회
+
코드 하이라이팅
+ +
실시간 채점
+
다크모드 지원
+
개인 맞춤 문제 추천
+
학과별 랭킹
+
티어 시스템
+
온라인 코드 에디터
+
코딩테스트 & 대회
+
코드 하이라이팅
+
+
+ + +
+
+
+
+ 1,200+ +
+
// 알고리즘 문제
+
+
+
+ 6 +
+
// 티어 등급
+
+
+
24/7
+
// 채점 서버 운영
+
+
+
PNU
+
// 부산대학교 공식
+
+
+
+ + +
+
+ +

주요 기능

+
+
+ 01 + +
코딩테스트 & 대회
+

+ 실전과 동일한 환경에서 코딩테스트를 준비하세요. 정기 대회 참가로 + 실력을 검증하고 경쟁하며 성장할 수 있습니다. +

+
+
+ 02 + 🎯 +
개인 맞춤 문제 추천
+

+ 푼 문제의 난이도와 부족한 영역을 분석해 매주 최적의 문제를 + 추천합니다. 보너스 점수 문제와 주간 챌린지도 제공됩니다. +

+
+
+ 03 + 🏆 +
랭킹 & 티어 시스템
+

+ 총점 기반 전체 랭킹, 오늘의 급상승 랭킹, 학과별 순위까지. 새싹부터 + 다이아몬드까지 6단계 티어로 성장을 시각화합니다. +

+
+
+ 04 + 💻 +
통합 코드 에디터
+

+ 웹에서 바로 코딩, 실행, 제출까지. 다크모드와 코드 하이라이팅을 + 지원하는 쾌적한 온라인 개발 환경을 제공합니다. +

+
+
+
+
+ + +
+
+ +

티어 등급

+
+
+
🌱
+
새싹
+
// Lv.1
+
+
+
🥉
+
브론즈
+
// Lv.2
+
+
+
🥈
+
실버
+
// Lv.3
+
+
+
🥇
+
골드
+
// Lv.4
+
+
+
🏆
+
플래티넘
+
// Lv.5
+
+
+
💎
+
다이아몬드
+
// Lv.6
+
+
+
+
+ + +
+
+ +

개발 스택

+
+
+
// Frontend
+
+ Vue.js 2.5 + Vuex 3.0 + Node.js 16 + ECharts 3.8 + Element UI 2.0 + iView 2.8 +
+
+
+
// Backend
+
+ Python 3.11 + Django 3.2 + DRF 3.14 + PostgreSQL 14 + Redis 7.0 +
+
+
+
// DevOps
+
+ K3s 1.33 + Kube-VIP + Longhorn + Harbor 2.14 + GitHub Actions +
+
+
+
+
+ + + + + + + diff --git a/frontend/src/assets/code-place-banner.png b/frontend/src/assets/code-place-banner.png new file mode 100644 index 000000000..e8e24fe48 Binary files /dev/null and b/frontend/src/assets/code-place-banner.png differ diff --git a/frontend/src/assets/code-place-logo.png b/frontend/src/assets/code-place-logo.png new file mode 100644 index 000000000..ca025c6d4 Binary files /dev/null and b/frontend/src/assets/code-place-logo.png differ diff --git a/frontend/src/assets/images/contest-empty-calendar.svg b/frontend/src/assets/images/contest-empty-calendar.svg new file mode 100644 index 000000000..7c913c8ac --- /dev/null +++ b/frontend/src/assets/images/contest-empty-calendar.svg @@ -0,0 +1,75 @@ + + + + + + + + + + S + M + T + W + T + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/contest-empty-clipboard.svg b/frontend/src/assets/images/contest-empty-clipboard.svg new file mode 100644 index 000000000..47182a308 --- /dev/null +++ b/frontend/src/assets/images/contest-empty-clipboard.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/oj/App.vue b/frontend/src/pages/oj/App.vue index 2a3dbbd68..f80146459 100644 --- a/frontend/src/pages/oj/App.vue +++ b/frontend/src/pages/oj/App.vue @@ -85,7 +85,7 @@ h1.main-title { --title_font-size: 24px; --title_font-weight: 700; - --point-color: #32306b; + --point-color: #5b64ed; --pale-point-color: #f8f9ff; // hex #024D97; to rgb 2, 77, 151 --pnu-green: rgb(6, 186, 110); diff --git a/frontend/src/pages/oj/api.js b/frontend/src/pages/oj/api.js index 9f76595f0..1e70e681f 100644 --- a/frontend/src/pages/oj/api.js +++ b/frontend/src/pages/oj/api.js @@ -126,6 +126,9 @@ export default { getHomeBonusProblem() { return ajax("problem/bonus", "get") }, + getWeeklyTopProblems() { + return ajax("problem/weekly_top", "get") + }, getMostDifficultProblem() { return ajax("problem/most_difficult_problem", "get") }, diff --git a/frontend/src/pages/oj/components/LogoButton.vue b/frontend/src/pages/oj/components/LogoButton.vue index 8af59b40e..63c579c89 100644 --- a/frontend/src/pages/oj/components/LogoButton.vue +++ b/frontend/src/pages/oj/components/LogoButton.vue @@ -2,7 +2,7 @@