From 88e2b13d675a4782baa5c4030aa50dded014d2bd Mon Sep 17 00:00:00 2001 From: wngjs8114 Date: Sun, 9 Aug 2026 14:54:20 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20#90=20feasibility=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=20=EA=B3=BC=EB=AA=A9=EB=B3=84=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0(subject=5Fresults)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- planner/views.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/planner/views.py b/planner/views.py index b3939d4..8194cab 100644 --- a/planner/views.py +++ b/planner/views.py @@ -75,6 +75,30 @@ def _calculate_feasibility_for_period(exam_period): result = calculate_feasibility(required_min, required_max, available) return result, tasks +def _build_subject_results(exam_period, tasks): + """ + 과목별 카드 표시용 데이터. 가용시간은 시험기간 전체가 공유하는 구조라 + 과목별 possible/risky/impossible 판정은 여기서 만들지 않는다. + """ + subject_results = [] + + for exam in exam_period.exams.all().order_by('exam_date'): + subject_tasks = [task for task in tasks if task.exam_id == exam.id] + + subject_results.append({ + 'exam_id': exam.id, + 'subject_name': exam.subject_name, + 'exam_date': exam.exam_date, + 'task_count': len(subject_tasks), + 'required_min_minutes': sum( + task.estimated_min_minutes for task in subject_tasks + ), + 'required_recommended_minutes': sum( + task.estimated_max_minutes for task in subject_tasks + ), + }) + + return subject_results def _validate_task_readiness(exam_period): """ @@ -111,9 +135,12 @@ def feasibility(request, period_id): is_ready, readiness_error = _validate_task_readiness(exam_period) result, tasks = _calculate_feasibility_for_period(exam_period) + subject_results = _build_subject_results(exam_period, tasks) + context = { 'exam_period': exam_period, 'result': result, + 'subject_results': subject_results, 'total_min_minutes': result['required_min_minutes'], 'total_max_minutes': result['required_recommended_minutes'], 'total_available_minutes': result['available_minutes'], From 88e665e6eb427dfabb83adeb5b734b19b63c82c1 Mon Sep 17 00:00:00 2001 From: wngjs8114 Date: Sun, 9 Aug 2026 16:15:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20#90=20=5Fbuild=5Fsubject=5Fresults?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80,=20dev=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- planner/tests.py | 113 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/planner/tests.py b/planner/tests.py index bb65930..a639800 100644 --- a/planner/tests.py +++ b/planner/tests.py @@ -1853,6 +1853,119 @@ def test_plan_complete_redirects_when_no_plan_yet(self): response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id}) ) +class FeasibilitySubjectResultsTests(TestCase): + """ + #90 feasibility() subject_results context 테스트. + """ + + def setUp(self): + from django.contrib.auth import get_user_model + from exams.models import Exam, ExamPeriod, StudyTask + + User = get_user_model() + self.user = User.objects.create_user( + username="subject_results_tester", + email="subject_results@example.com", + password="pass1234", + ) + self.other_user = User.objects.create_user( + username="subject_results_other", + email="subject_results_other@example.com", + password="pass1234", + ) + self.today = django_timezone.localdate() + self.exam_period = ExamPeriod.objects.create( + user=self.user, + title="과목별 데이터 테스트 시험기간", + start_date=self.today, + end_date=self.today + timedelta(days=10), + status="active", + ) + self.client.login(username="subject_results@example.com", password="pass1234") + + def _get(self): + return self.client.get( + reverse("planner:feasibility", kwargs={"period_id": self.exam_period.id}) + ) + + def _subject_results_by_name(self, response): + return {s["subject_name"]: s for s in response.context["subject_results"]} + + # ── 1. 확정된 작업만 집계되는지 (미확정 제외) ── + def test_only_confirmed_tasks_are_counted(self): + from exams.models import Exam, StudyTask + + exam = Exam.objects.create( + exam_period=self.exam_period, subject_name="데이터통신", + exam_date=self.today + timedelta(days=5), + ) + StudyTask.objects.create( + exam=exam, title="확정 작업", importance="high", depth="core", + task_type="concept", difficulty="normal", order=1, + estimated_min_minutes=20, estimated_max_minutes=40, is_confirmed=True, + ) + StudyTask.objects.create( + exam=exam, title="미확정 작업", importance="high", depth="core", + task_type="concept", difficulty="normal", order=2, + estimated_min_minutes=30, estimated_max_minutes=50, is_confirmed=False, + ) + + response = self._get() + results = self._subject_results_by_name(response) + + self.assertEqual(results["데이터통신"]["task_count"], 1) + self.assertEqual(results["데이터통신"]["required_min_minutes"], 20) + self.assertEqual(results["데이터통신"]["required_recommended_minutes"], 40) + + # ── 2. 작업이 아예 없는 과목도 0/0/0으로 나오는지 ── + def test_subject_with_no_tasks_shows_zero(self): + from exams.models import Exam + + Exam.objects.create( + exam_period=self.exam_period, subject_name="운영체제", + exam_date=self.today + timedelta(days=8), + ) + + response = self._get() + results = self._subject_results_by_name(response) + + self.assertEqual(results["운영체제"]["task_count"], 0) + self.assertEqual(results["운영체제"]["required_min_minutes"], 0) + self.assertEqual(results["운영체제"]["required_recommended_minutes"], 0) + + # ── 3. exam_date 순으로 정렬되는지 ── + def test_sorted_by_exam_date(self): + from exams.models import Exam + + Exam.objects.create( + exam_period=self.exam_period, subject_name="나중 시험", + exam_date=self.today + timedelta(days=9), + ) + Exam.objects.create( + exam_period=self.exam_period, subject_name="먼저 시험", + exam_date=self.today + timedelta(days=2), + ) + + response = self._get() + names = [s["subject_name"] for s in response.context["subject_results"]] + + self.assertEqual(names, ["먼저 시험", "나중 시험"]) + + # ── 4. 다른 사용자는 접근 자체가 404 (subject_results 노출 안 됨) ── + def test_other_user_cannot_access(self): + from exams.models import Exam + + Exam.objects.create( + exam_period=self.exam_period, subject_name="비공개 과목", + exam_date=self.today + timedelta(days=5), + ) + self.client.logout() + self.client.login(username="subject_results_other@example.com", password="pass1234") + + response = self._get() + + self.assertEqual(response.status_code, 404) + class DashboardViewTests(TestCase): def setUp(self): from django.contrib.auth import get_user_model