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+
+
+
// 알고리즘 문제
+
+
+
+
+
+
+
+
+
+
+
features
+
주요 기능
+
+
+
01
+
⚡
+
코딩테스트 & 대회
+
+ 실전과 동일한 환경에서 코딩테스트를 준비하세요. 정기 대회 참가로
+ 실력을 검증하고 경쟁하며 성장할 수 있습니다.
+
+
+
+
02
+
🎯
+
개인 맞춤 문제 추천
+
+ 푼 문제의 난이도와 부족한 영역을 분석해 매주 최적의 문제를
+ 추천합니다. 보너스 점수 문제와 주간 챌린지도 제공됩니다.
+
+
+
+
03
+
🏆
+
랭킹 & 티어 시스템
+
+ 총점 기반 전체 랭킹, 오늘의 급상승 랭킹, 학과별 순위까지. 새싹부터
+ 다이아몬드까지 6단계 티어로 성장을 시각화합니다.
+
+
+
+
04
+
💻
+
통합 코드 에디터
+
+ 웹에서 바로 코딩, 실행, 제출까지. 다크모드와 코드 하이라이팅을
+ 지원하는 쾌적한 온라인 개발 환경을 제공합니다.
+
+
+
+
+
+
+
+
+
+
ranking system
+
티어 등급
+
+
+
+
+
+
+
+
tech stack
+
개발 스택
+
+
+
// 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 @@
+
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 @@

@@ -49,11 +49,12 @@ export default {
.pnuName {
font-size: 14.5px;
font-weight: normal;
+ color: #555560;
}
.systemTitle {
font-size: 19px;
font-weight: bold;
- color: #32306b;
+ color: #5b64ed;
}
}
}
diff --git a/frontend/src/pages/oj/components/NavBar.vue b/frontend/src/pages/oj/components/NavBar.vue
index 03232a0fa..ce30d0167 100644
--- a/frontend/src/pages/oj/components/NavBar.vue
+++ b/frontend/src/pages/oj/components/NavBar.vue
@@ -8,14 +8,15 @@
:active-name="activeMenu"
>
-