diff --git a/README.md b/README.md index edf88a6..d327cc1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 미림 앱 (mirim-app) — 어썸데브 수습 학습 플랫폼 +# 미림 앱 (mirim-app) — 어썸데브 학습 플랫폼 > **이 저장소는 여러분(수습생)의 것입니다.** > 미림마이스터고 수습생 4명이 직접 보고, 배우고, 고치는 학습용 웹 앱입니다. @@ -8,7 +8,7 @@ ## 무엇을 하는 앱인가? - 수습 기간(**8주**, 9월~10월) 동안 필요한 **학습 문서**(가이드·커리큘럼·수업자료·과제·실습가이드)를 한곳에서 봅니다. -- **학습 센터**에서 컴퓨터 기초부터 디자인까지 **강좌 71개**를 순서대로 배우고, 각 코스 끝에서 **퀴즈**로 확인합니다. +- **학습 센터**에서 수습 트랙 **강좌 83개**(컴퓨터 기초~앱 개발)와 수준별 과정(초급·중급·고급·특급) **48개**를 순서대로 배우고, 각 코스 끝에서 **퀴즈**로 확인합니다. - **코딩 문제 25개**(JavaScript·SQL·Java)를 브라우저에서 직접 풀고 자동 채점받습니다. - 주차별 **체크리스트**로 오늘 할 일을 확인하고, **과제를 제출**하면 멘토가 **피드백**을 남깁니다. - 멘토는 학생별 진도·제출물·인적사항을 한 화면에서 관리합니다. diff --git a/backend/src/main/java/dev/awesomedev/mirim/service/MentorAnalyticsService.java b/backend/src/main/java/dev/awesomedev/mirim/service/MentorAnalyticsService.java index fe58014..bf13fb4 100644 --- a/backend/src/main/java/dev/awesomedev/mirim/service/MentorAnalyticsService.java +++ b/backend/src/main/java/dev/awesomedev/mirim/service/MentorAnalyticsService.java @@ -2,6 +2,7 @@ package dev.awesomedev.mirim.service; import dev.awesomedev.mirim.domain.CodingSubmission; import dev.awesomedev.mirim.domain.CourseProgress; +import dev.awesomedev.mirim.domain.Progress; import dev.awesomedev.mirim.domain.QuizResult; import dev.awesomedev.mirim.domain.StudentProfile; import dev.awesomedev.mirim.domain.Submission; @@ -169,8 +170,13 @@ public class MentorAnalyticsService { last = later(last, cp.getCompletedAt()); } - // 체크리스트 완료 수 - int checklistDone = progressRepository.findByUser(u).size(); + // 체크리스트 완료 수 + 체크한 시각도 "활동"으로 반영한다. + // (안 그러면 최근에 체크리스트만 열심히 한 학생이 '관심 필요'로 잘못 잡힌다.) + List progresses = progressRepository.findByUser(u); + for (Progress pr : progresses) { + last = later(last, pr.getCheckedAt()); + } + int checklistDone = progresses.size(); // 과제 — 리뷰 대기(SUBMITTED) 집계 + 활동 시각 for (Submission s : submissionRepository.findByUserOrderBySubmittedAtDesc(u)) { @@ -189,7 +195,8 @@ public class MentorAnalyticsService { // 어려운 지점 = 평균이 낮은 순 top 5 (응시/시도가 있는 것만) List hardestQuizzes = quizAgg.entrySet().stream() - .map(e -> new QuizHotspot(e.getKey(), e.getValue()[0] / e.getValue()[1], e.getValue()[1])) + .map(e -> new QuizHotspot(e.getKey(), + (int) Math.round((double) e.getValue()[0] / e.getValue()[1]), e.getValue()[1])) .sorted(Comparator.comparingInt(QuizHotspot::avgScorePct)) .limit(5) .toList(); diff --git a/backend/src/test/java/dev/awesomedev/mirim/service/MentorAnalyticsServiceTest.java b/backend/src/test/java/dev/awesomedev/mirim/service/MentorAnalyticsServiceTest.java index 4bad96f..ba29bac 100644 --- a/backend/src/test/java/dev/awesomedev/mirim/service/MentorAnalyticsServiceTest.java +++ b/backend/src/test/java/dev/awesomedev/mirim/service/MentorAnalyticsServiceTest.java @@ -2,6 +2,7 @@ package dev.awesomedev.mirim.service; import dev.awesomedev.mirim.domain.CodingProblem; import dev.awesomedev.mirim.domain.CodingSubmission; +import dev.awesomedev.mirim.domain.Progress; import dev.awesomedev.mirim.domain.QuizResult; import dev.awesomedev.mirim.domain.StudentProfile; import dev.awesomedev.mirim.domain.User; @@ -116,6 +117,22 @@ class MentorAnalyticsServiceTest { assertEquals(0, row.quizAvgPct()); } + @Test + @DisplayName("체크리스트 체크만 한 학생도 그 시각이 '마지막 활동'에 반영된다") + void checklist_counts_as_activity() { + User a = student("가", "DEV"); + StudentProfile pa = profile(a, "2026"); + Progress pr = mock(Progress.class); + when(pr.getCheckedAt()).thenReturn(NOW.minus(Duration.ofDays(2))); + emptyActivity(); + when(profileRepo.findAllWithUser()).thenReturn(List.of(pa)); + when(progressRepo.findByUser(a)).thenReturn(List.of(pr)); + + var row = service.analytics(null).students().get(0); + assertEquals(2L, row.daysSinceActive()); // 체크리스트 체크(2일 전)가 활동으로 잡힘 + assertEquals(1, row.checklistDone()); + } + @Test @DisplayName("어려운 코딩 문제는 통과율 낮은 순으로 정렬된다") void coding_hotspots_sorted_by_pass_rate() { diff --git a/deploy/BACKUP.md b/deploy/BACKUP.md index 8ec14c0..4b4b56d 100644 --- a/deploy/BACKUP.md +++ b/deploy/BACKUP.md @@ -116,7 +116,7 @@ sudo rm -rf ~/gtest | 대상 | 결과 | |---|---| -| PostgreSQL | 임시 DB에 복원 → 운영 원본과 **완전 일치** (사용자 5 / 퀴즈 355 / 문서 17 / 체크리스트 46 / 코딩 20) ✅ | +| PostgreSQL | 임시 DB에 복원 → 운영 원본과 **완전 일치** (퀴즈 655 / 문서 19 / 체크리스트 46 / 코딩 25) ✅ | | Gitea | 백업 속 저장소에서 clone 성공 → HEAD·커밋 20개·파일 269개가 운영과 **완전 일치** ✅ | ## 무엇이 백업되고, 무엇이 안 되나 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 12e43d5..0fc5060 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,7 @@ // /login 이외의 모든 페이지는 로그인해야 볼 수 있고, /mentor는 MENTOR 역할만 접근 가능하다. // // 학습 포인트: 코스 라우트는 여기 일일이 쓰지 않는다 — courseCatalog.jsx의 데이터를 .map으로 돌려 -// 자동 생성한다. 코스가 100개가 되어도 이 파일은 그대로다. +// 자동 생성한다. 코스가 수백 개로 늘어도 이 파일은 그대로다. import { Suspense } from 'react'; import { Routes, Route, NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from './AuthContext'; @@ -179,7 +179,7 @@ export default function App() { } /> {/* 학습 센터: 허브 + 코스들(카탈로그에서 자동 생성) */} } /> - {/* 학습 포인트: 코스를 CourseShell로 감싸면 72개 코스 전부에 + {/* 학습 포인트: 코스를 CourseShell로 감싸면 모든 코스 라우트에 하단 "완료" 바가 자동으로 붙는다 — 공통 기능은 감싸는 컴포넌트로. */} {ALL_COURSES.map((course) => ( ))} {/* 학습 포인트: :slug는 "주소의 이 자리에 오는 값을 변수로 받겠다"는 뜻. - 72개 퀴즈 라우트를 하나로 처리한다 — QuizPage가 useParams()로 꺼내 쓴다. */} + 수많은 퀴즈 라우트를 하나로 처리한다 — QuizPage가 useParams()로 꺼내 쓴다. */} } /> } /> } /> diff --git a/frontend/src/components/CodeEditor.jsx b/frontend/src/components/CodeEditor.jsx index 9705e7c..6c70f63 100644 --- a/frontend/src/components/CodeEditor.jsx +++ b/frontend/src/components/CodeEditor.jsx @@ -11,7 +11,7 @@ import { useRef } from 'react'; * 라이브러리를 고를 때는 "이게 해 주는 일" 말고 "내가 진짜 필요한 일"을 먼저 적어 보자. * 나중에 자동완성·문법 하이라이팅이 정말 필요해지면 그때 바꾸면 된다. */ -export default function CodeEditor({ value, onChange, readOnly = false, minRows = 12 }) { +export default function CodeEditor({ value, onChange, readOnly = false, minRows = 12, ariaLabel = '코드 작성 입력란' }) { const textareaRef = useRef(null); // 줄번호는 줄 수만큼 만들어 준다. 최소 minRows줄은 보이게 해서 창이 쪼그라들지 않게 한다. @@ -62,6 +62,7 @@ export default function CodeEditor({ value, onChange, readOnly = false, minRows