Skip to content

Commit 4d5df5e

Browse files
authored
Merge pull request #55 from junotb/refactor/2026-02-15-2
refactor: LessonFeedback 제거 및 ScheduleFeedback·lesson 구조로 전환
2 parents d838140 + 242d65a commit 4d5df5e

122 files changed

Lines changed: 1203 additions & 1625 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# 1:1 Course Registration System (LMS)
2+
3+
1:1 과외·튜터링 기반 Learning Management System (LMS). 높은 동시성을 고려한 수강 신청 및 수업 관리 Monorepo입니다.
4+
5+
## 프로젝트 배경
6+
7+
이전 시스템에서는 튜터 가용성을 **사전 생성된 Time Block(행) INSERT** 방식으로 관리했습니다. 이 접근은 다음 위험을 내포했으며, 현 프로젝트에서 이를 해결하고자 합니다.
8+
9+
- **스토리지 급증**: 튜터 수 × 시간슬롯 × 기간에 비례해 행 수가 선형 증가 (예: 1,000명 × 16개월 ≈ 2,300만 행)
10+
- **동기화 위험**: 근무조건·휴식·오버타임·수업·공휴일 등 진실 소스가 분산되어 있어, 변경 시 재동기화가 누락되면 불일치 발생
11+
- **오래된 데이터**: 강사가 근무시간을 10~17시로 변경했는데, 가용 슬롯은 9~18시 기준으로 노출 → 잘못된 예약 또는 기회 손실
12+
- **가용 기간 계속 넓히기 부담**: 16개월치 가용 슬롯을 앞으로 밀어가며 유지하는 배치에 의존, 실패 시 공백
13+
- **동시성·재계산 비용**: 대량 UPDATE와 예약 트랜잭션 경합, 재배치 시 DB 부하 급증
14+
15+
본 프로젝트는 **TeacherAvailability(가용 시간)·TeacherTimeOff(휴무)·Schedule(수업)** 기반으로 가용성을 조회 시점에 계산하는 모델을 채택하여, 사전 INSERT·동기화·가용 기간 유지 배치 의존을 줄이고 일관성을 확보합니다.
16+
17+
## 프로젝트 성과
18+
19+
| 구분 | 이전 시스템 | 이번 프로젝트 |
20+
|------|-------------|---------------|
21+
| **스토리지** | 튜터 수 × 슬롯 × 기간에 비례해 수백만 행 증가 | 가용 시간·휴무·수업만 저장, 행 수 최소화 |
22+
| **동기화** | 근무조건·휴식·수업 등 다수 소스 재동기화 필요 | 조회 시점 계산으로 재동기화 불필요 |
23+
| **오래된 데이터** | 강사 근무시간 변경 후 재배치 미실행 시 가용 슬롯이 구버전 유지 | 조회 시점에 TeacherAvailability 기준 계산으로 항상 최신 가용성 노출 |
24+
| **가용 기간 유지** | 16개월치를 앞으로 밀어가는 배치 유지, 실패 시 공백 | 기간 제한 없이 필요 시점 계산 |
25+
| **동시성** | 대량 UPDATE와 예약 트랜잭션 경합 | Redis 분산 락으로 수강 신청 경합만 관리 |
26+
| **운영** | 재배치·일별 확장 배치 의존 | 관련 배치 제거로 운영 부담 감소 |
27+
28+
## 프로젝트 구조
29+
30+
```
31+
next-java-lms/
32+
├── api/ # Spring Boot 백엔드 (Java 21)
33+
├── web/ # Next.js 프론트엔드 (React 19)
34+
└── docs/ # 프로젝트 문서
35+
```
36+
37+
## Tech Stack
38+
39+
| 구분 | 기술 |
40+
|------|------|
41+
| Backend | Spring Boot 3.3.3, Java 21, JPA, PostgreSQL |
42+
| Frontend | Next.js 15.5, React 19, TypeScript |
43+
| 인증 | Better-Auth (Web), Session (API) |
44+
| 동시성 | Redis (Redisson Distributed Lock) |
45+
| API 문서 | SpringDoc OpenAPI (Swagger) |
46+
47+
## 사전 요구사항
48+
49+
- **Java 21** (JDK)
50+
- **Node.js 20+** (LTS 권장)
51+
- **PostgreSQL** 14+
52+
- **Redis** 7+
53+
- **Gradle** (Wrapper 포함)
54+
55+
## 빠른 시작
56+
57+
### 1. 백엔드 실행
58+
59+
```bash
60+
cd api
61+
./gradlew bootRun
62+
```
63+
64+
- API: `http://localhost:8080`
65+
- Swagger UI: `http://localhost:8080/swagger-ui.html`
66+
67+
### 2. 프론트엔드 실행
68+
69+
```bash
70+
cd web
71+
npm install
72+
npm run dev
73+
```
74+
75+
- Web: `http://localhost:3000`
76+
77+
### 3. 환경 변수
78+
79+
- `api/`: `.env` 또는 시스템 환경 변수 (`DB_URL`, `REDIS_URL`, `CORS_ALLOWED_ORIGINS` 등)
80+
- `web/`: `.env.local` (Better-Auth, API URL 등)
81+
82+
## 주요 기능
83+
84+
- **관리자**: 강좌·사용자·수업 일정 관리
85+
- **강사**: 대시보드, 수업 예정 조회, 가용 시간·휴무 설정
86+
- **학생**: 수강 신청, 대시보드, 수업방 진입
87+
- **수업 피드백**: 녹화본 업로드, AI 피드백 (VTT, Google Gemini)
88+

api/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# API (Backend)
2+
3+
Spring Boot 기반 REST API. 1:1 수강 신청, 수업 관리, 수업 피드백 등을 처리합니다.
4+
5+
## Tech Stack
6+
7+
| 항목 | 기술 |
8+
|------|------|
9+
| Framework | Spring Boot 3.3.3 |
10+
| Language | Java 21 |
11+
| Build | Gradle (Kotlin DSL) |
12+
| DB | PostgreSQL (Production), H2 (Test) |
13+
| ORM | JPA / Hibernate |
14+
| Security | Spring Security + 커스텀 인증 필터 |
15+
| Concurrency | Redisson (Distributed Lock) |
16+
| API Docs | SpringDoc OpenAPI (Swagger) |
17+
| Utils | Lombok, Commons Lang3 |
18+
19+
## 프로젝트 구조
20+
21+
```
22+
api/src/main/java/org/junotb/api/
23+
├── auth/ # 세션·인증
24+
├── common/ # 예외 처리, Security Filter
25+
├── config/ # Security, Web, Redis, OpenAPI
26+
├── course/ # 강좌
27+
├── dashboard/ # 강사/학생 대시보드
28+
├── lesson/ # 수업방(세션) API
29+
├── registration/ # 수강 신청
30+
├── schedule/ # 수업 일정
31+
├── schedulefeedback/ # 수업 피드백 (녹화·AI)
32+
├── teacher/ # 강사 설정·가용 시간
33+
├── user/ # 사용자 관리
34+
└── LmsApplication.java
35+
```
36+
37+
## 실행 방법
38+
39+
```bash
40+
# Gradle Wrapper 사용
41+
./gradlew bootRun
42+
43+
# 특정 포트 지정
44+
PORT=8081 ./gradlew bootRun
45+
```
46+
47+
## 환경 변수
48+
49+
| 변수 | 설명 | 기본값 |
50+
|------|------|--------|
51+
| `PORT` | 서버 포트 | 8080 |
52+
| `DB_URL` | PostgreSQL 연결 URL | jdbc:postgresql://localhost:5432/java_lms_db |
53+
| `DB_USERNAME` | DB 사용자 | postgres |
54+
| `DB_PASSWORD` | DB 비밀번호 | password |
55+
| `REDIS_URL` | Redis URL | redis://localhost:6379 |
56+
| `CORS_ALLOWED_ORIGINS` | CORS 허용 Origin | http://localhost:3000 |
57+
| `GOOGLE_GEMINI_API_KEY` | Gemini API 키 (AI 피드백) | - |
58+
| `FFMPEG_PATH` | FFmpeg 실행 경로 | ffmpeg |
59+
| `VIDEO_PROCESSOR_ENABLED` | 비디오 처리 활성화 | true |
60+
61+
## API 문서
62+
63+
- Swagger UI: `http://localhost:8080/swagger-ui.html`
64+
- OpenAPI JSON: `http://localhost:8080/v3/api-docs`
65+
66+
## 테스트
67+
68+
```bash
69+
./gradlew test
70+
```
71+
72+
## 아키텍처
73+
74+
- **Layered**: Controller → Service → Repository
75+
- **DTO**: Request/Response 분리, Entity 직접 반환 금지
76+
- **Concurrency**: 수강 신청 시 Redis 분산 락 또는 DB 비관적 락

api/src/main/java/org/junotb/api/config/SecurityConfig.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurat
3535
.requestMatchers("/api/auth/refresh").authenticated() // 세션 갱신은 인증 필요
3636
.requestMatchers(
3737
"/api/auth/**", // 기타 인증 관련 엔드포인트는 허용
38-
"/api/dev/**",
3938
"/swagger-ui/**",
4039
"/v3/api-docs/**",
4140
"/swagger-ui.html",

api/src/main/java/org/junotb/api/global/dev/DevController.java

Lines changed: 0 additions & 52 deletions
This file was deleted.

api/src/main/java/org/junotb/api/lesson/LessonController.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import io.swagger.v3.oas.annotations.tags.Tag;
55
import lombok.RequiredArgsConstructor;
66
import org.junotb.api.lesson.dto.LessonAccessResponse;
7-
import org.junotb.api.lessonfeedback.LessonFeedbackService;
8-
import org.junotb.api.lessonfeedback.dto.LessonFeedbackResponse;
9-
import org.junotb.api.lessonfeedback.dto.VideoUploadResponse;
7+
import org.junotb.api.schedulefeedback.ScheduleFeedbackService;
8+
import org.junotb.api.schedulefeedback.dto.ScheduleFeedbackResponse;
9+
import org.junotb.api.schedulefeedback.dto.VideoUploadResponse;
1010
import org.springframework.http.HttpStatus;
1111
import org.springframework.http.ResponseEntity;
1212
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -22,7 +22,7 @@
2222
public class LessonController {
2323

2424
private final LessonService lessonService;
25-
private final LessonFeedbackService lessonFeedbackService;
25+
private final ScheduleFeedbackService scheduleFeedbackService;
2626

2727
@GetMapping("/{scheduleId}/access")
2828
@Operation(summary = "입장 권한 확인", description = "해당 스케줄(수업)에 대한 입장 가능 여부와 역할을 반환합니다.")
@@ -51,17 +51,17 @@ public ResponseEntity<VideoUploadResponse> uploadVideo(
5151
@AuthenticationPrincipal String teacherId,
5252
@RequestParam("file") MultipartFile file
5353
) throws IOException {
54-
VideoUploadResponse response = lessonFeedbackService.uploadVideo(scheduleId, teacherId, file);
54+
VideoUploadResponse response = scheduleFeedbackService.uploadVideo(scheduleId, teacherId, file);
5555
return ResponseEntity.status(HttpStatus.CREATED).body(response);
5656
}
5757

5858
@GetMapping("/{scheduleId}/feedback")
5959
@Operation(summary = "수업 피드백 조회", description = "해당 수업의 강사 또는 수강생이 vttContent, feedbackContent 조회.")
60-
public ResponseEntity<LessonFeedbackResponse> getFeedback(
60+
public ResponseEntity<ScheduleFeedbackResponse> getFeedback(
6161
@PathVariable Long scheduleId,
6262
@AuthenticationPrincipal String userId
6363
) {
64-
LessonFeedbackResponse response = lessonFeedbackService.getFeedback(scheduleId, userId);
64+
ScheduleFeedbackResponse response = scheduleFeedbackService.getFeedback(scheduleId, userId);
6565
return ResponseEntity.ok(response);
6666
}
6767
}

api/src/main/java/org/junotb/api/lesson/LessonService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ private LessonAccessResponse buildAccessResponse(boolean allowed, String role, S
9090
String meetLink = schedule.getMeetLink() != null ? schedule.getMeetLink() : null;
9191
var c = schedule.getCourse();
9292
if (c == null) {
93-
throw new IllegalStateException("수업은 반드시 강의(Course)와 연결되어 있어야 합니다.");
93+
throw new IllegalStateException("수업은 반드시 강좌(Course)와 연결되어 있어야 합니다.");
9494
}
9595
CourseInLesson course = new CourseInLesson(c.getTitle(), c.getDescription());
9696
return new LessonAccessResponse(allowed, role != null ? role : "", scheduleResponse, meetLink, course);

api/src/main/java/org/junotb/api/lesson/dto/CourseInLesson.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package org.junotb.api.lesson.dto;
22

33
/**
4-
* 수업방에서 표시하는 강의 요약 정보.
4+
* 수업방에서 표시하는 강좌 요약 정보.
55
*
6-
* @param title 강의 제목
7-
* @param description 강의 설명 (nullable)
6+
* @param title 강좌 제목
7+
* @param description 강좌 설명 (nullable)
88
*/
99
public record CourseInLesson(
1010
String title,

api/src/main/java/org/junotb/api/lesson/dto/LessonAccessResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @param role TEACHER | STUDENT
1010
* @param schedule 스케줄 정보
1111
* @param meetLink Google Meet 링크 (강사가 등록한 경우). 수업방에서 링크 전달용.
12-
* @param course 강의 정보 (제목, 설명). 수업방 상단 표시용.
12+
* @param course 강좌 정보 (제목, 설명). 수업방 상단 표시용.
1313
*/
1414
public record LessonAccessResponse(
1515
boolean allowed,

api/src/main/java/org/junotb/api/lessonfeedback/LessonFeedbackRepository.java

Lines changed: 0 additions & 12 deletions
This file was deleted.

api/src/main/java/org/junotb/api/lessonfeedback/dto/LessonFeedbackResponse.java

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)