From 1b559f5da2403bfa4f1c76eab842a29a269c109e Mon Sep 17 00:00:00 2001 From: AWESOMEDEV Date: Mon, 20 Jul 2026 14:01:05 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A0=84=EB=A9=B4=20=EA=B2=B0=ED=95=A8?= =?UTF-8?q?=20=EC=8A=A4=EC=9C=95=20=E2=80=94=204=EA=B4=80=EC=A0=90=20?= =?UTF-8?q?=EA=B0=90=EC=82=AC=20=ED=99=95=EC=A0=95=20=EA=B2=B0=ED=95=A8=20?= =?UTF-8?q?=EC=9D=BC=EA=B4=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4개 전문 감사관(프론트·백엔드·일관성·UX) 병렬 검증 결과 반영. 버그/깨짐(HIGH·MED): - HtmlCssPage: 존재하지 않는 /learn/computer → /learn/hardware(깨진 링크, 대시보드로 튕김) - MentorAnalyticsService: 체크리스트 활동(Progress.checkedAt)이 '마지막 활동'에 누락돼 체크리스트만 한 학생이 '관심 필요'로 오분류되던 버그 수정(+회귀 테스트) - 퀴즈 핫스팟 평균 정수절삭 → Math.round로 통일 접근성/UX(MED): - CodeEditor: 코드 입력창에 aria-label 추가(WCAG 4.1.2, 스크린리더 이름 부여) - 멘토 제출물 표: tr role=button → 셀 안 button으로(표 의미구조 보존 + 키보드) - 대시보드 체크박스 토글 실패 시 무피드백 → try/catch + 에러 안내 콘텐츠/문서 드리프트: - 초급 미니프로젝트 JS 예제 var→let(let/const 가르치며 var 쓰던 자기모순) - README 강좌 수(71→83+48), BACKUP.md 복원검증 카운트(퀴즈655·문서19·코딩25) - 브랜드/코스수 주석 정리(수습 플랫폼→학습 플랫폼, 72개→비수치화), 퀴즈 결과 '점'→'%' 테스트 백엔드 51→52, 프론트 30 유지. 운영 배포·스모크 11/11. Co-Authored-By: Claude Opus 4.8 --- README.md | 4 +-- .../mirim/service/MentorAnalyticsService.java | 13 ++++++-- .../service/MentorAnalyticsServiceTest.java | 17 ++++++++++ deploy/BACKUP.md | 2 +- frontend/src/App.jsx | 6 ++-- frontend/src/components/CodeEditor.jsx | 3 +- frontend/src/components/CourseShell.jsx | 2 +- frontend/src/pages/CodingProblemPage.jsx | 2 +- frontend/src/pages/DashboardPage.jsx | 32 ++++++++++++------ frontend/src/pages/HtmlCssPage.jsx | 2 +- frontend/src/pages/MentorPage.jsx | 33 +++++++++---------- frontend/src/pages/QuizPage.jsx | 2 +- .../pages/levels/Lv1FirstMiniProjectPage.jsx | 14 ++++---- frontend/src/styles/global.css | 24 +++++++++++++- 14 files changed, 107 insertions(+), 49 deletions(-) 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