콘텐츠 서비스
이 페이지는 lumie-backend/modules/content를 다룹니다.
책임
content 모듈은 다음을 소유합니다.
- 공지사항, 공지사항의 class target, 공지 읽음 추적
- Q&A 게시판과 댓글 스레드
- 상담 일정과 예약
- 리뷰와 리뷰 팝업 토글
- student나 staff를 제거하기 전에 다른 모듈이 사용하는 삭제 가능성 검사
소스 경로
| Path | 역할 |
|---|---|
lumie-backend/modules/content/src/main/java/com/lumie/content/adapter/in/web | announcement, Q&A, reservation, schedule, review용 공개 HTTP controller |
lumie-backend/modules/content/src/main/java/com/lumie/content/adapter/in/internal/ContentServiceAdapter.java | internal monolith API 구현 |
lumie-backend/modules/content/src/main/java/com/lumie/content/application/service | announcement, Q&A, reservation, schedule, review, 삭제 가능성 service |
lumie-backend/modules/content/src/main/java/com/lumie/content/domain/entity | content aggregate와 보조 table |
lumie-backend/libs/internal-api/src/main/java/com/lumie/content/api/ContentService.java | announcement 생성과 readiness check를 위한 internal 계약 |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | announcements, qna_boards, qna_comments, counseling_schedules, counseling_reservations, reviews, toggles 등의 baseline table |
lumie-backend/app/src/main/resources/db/migration/public/V20__rename_qna_is_it_answered.sql | qna_boards.is_it_answered를 is_answered로 rename하고 version 추가 |
lumie-backend/app/src/main/resources/db/migration/public/V24__add_version_to_domain_mutable_entities.sql | announcement, reservation, schedule, review, toggle에 version 추가 |
lumie-backend/app/src/main/resources/db/migration/public/V43__qna_boards_add_category_name.sql | Q&A category 데이터의 legacy import 지원 |
lumie-backend/app/src/main/resources/db/migration/public/V55__create_announcement_class_targets.sql | class target visibility row 생성 |
lumie-backend/app/src/main/resources/db/migration/public/V56__announcement_class_targets_tenant_safe_fk.sql | announcement target FK를 tenant-safe하게 변경 |
lumie-backend/app/src/main/resources/db/migration/public/V62__add_read_receipt_tables.sql | announcement_reads 생성 |
lumie-backend/app/src/main/resources/db/migration/public/V65__repair_read_receipt_and_file_download_rls.sql | announcement read에 대한 tenant-safe RLS와 FK 동작 복구 |
공개 인터페이스
| Surface | Entrypoints |
|---|---|
| Announcements | GET/POST /v1/announcements, GET /v1/announcements/important, GET /v1/announcements/{id}, GET /v1/announcements/{id}/readers, PATCH /v1/announcements/{id}, DELETE /v1/announcements/{id} |
| Q&A | GET/POST /v1/qna, GET /v1/qna/unanswered, GET /v1/qna/statistics/unanswered-overdue, GET /v1/qna/user/{qnaUserId}, GET/PATCH/DELETE /v1/qna/{id}, POST /v1/qna/{id}/comments, DELETE /v1/qna/{qnaId}/comments/{commentId} |
| Reservations | GET/POST /v1/reservations, GET /v1/reservations/student/{studentId}, GET /v1/reservations/schedule/{scheduleId}, GET /v1/reservations/today, GET /v1/reservations/{id}, PATCH /v1/reservations/{id}/status, DELETE /v1/reservations/{id} |
| Schedules | GET/POST /v1/schedules, GET /v1/schedules/range, GET /v1/schedules/available, GET /v1/schedules/user/{userId}, GET/PATCH/DELETE /v1/schedules/{id} |
| Reviews | POST /v1/reviews, GET /v1/reviews, DELETE /v1/reviews/{id}, GET /v1/reviews/popup-setting, PUT /v1/reviews/popup-setting |
내부 인터페이스
ContentService는 다른 모듈에서 동기식으로 소비되며, 현재 다음을
노출합니다.
createAnnouncement(...)canUserViewAnnouncement(...)canUserViewQna(...)hasActiveOrUpcomingReservationsForStudent(...)hasUpcomingSchedulesForUser(...)
이 internal API는 generic CRUD가 아니라 운영적인 성격을 가집니다. 전체 reservation이나 schedule 수정이 아니라 guard check와 소규모 announcement 생성 helper만 export합니다.
집계와 테이블
| Aggregate | Table | 참고 |
|---|---|---|
Announcement | announcements | 기본 공지 레코드 |
AnnouncementClassTarget | announcement_class_targets | 선택적 class-target visibility filter |
AnnouncementRead | announcement_reads | 사용자별 읽음 영수증, idempotent insert |
QnaBoard | qna_boards | is_answered, author_last_read_at를 가진 질문 스레드 |
QnaComment | qna_comments | 관리자 또는 학생 댓글 |
Schedule | counseling_schedules | staff 상담 slot |
Reservation | counseling_reservations | schedule에 연결된 student 예약 |
Review | reviews | 공개 review 텍스트 |
ReviewPopupSetting | toggles | 고정 ID로 관리되는 review popup 스위치 |
ReviewPopupSetting은 특수 사례입니다. content 전용 설정 table이 아니라
범용 toggles table에 review popup 상태를 저장합니다.
런타임 흐름
공지 가시성과 읽음 추적
반 대상 계 약의 형태
private void validateClassTargets(List<Long> classIds) {
String tenantSlug = TenantContextHolder.getRequiredTenant();
for (Long classId : classIds) {
if (classService.getClass(tenantSlug, classId).isEmpty()) {
throw new BusinessException(ContentErrorCode.INVALID_CLASS_TARGET);
}
}
}
class-target 공지사항은 class 모듈을 통해 검증됩니다. content 모듈은 class aggregate 복사본이 아니라 class ID만 저장합니다.
핵심 동작
공지사항
- staff는 class target을 붙일 수 있으며, 학생은 자신이 등록된 class 중 하나를 target으로 삼은 공지나 target이 없는 공지만 볼 수 있습니다.
- 학생 읽기는
announcementReadRepository.insertIfAbsent(...)를 호출하므로, 반복 조회가 중복 row를 만들지 않습니다. - 공지 응답은 의도적으로 student 호출자에게
classIds를 숨기고, staff 측 호출자에게만 이를 노출합니다. - 파일 링크는
FileService.replaceFileLinks(...),deleteFileLinksByEntity(...),deleteFilesByEntity(...)로 관리됩니다.
Q&A
QnaBoard.addComment(...)는 admin comment가 추가되면 thread를 answered로 표시합니다.- 마지막 admin comment를 제거하면 thread는 다시 unanswered가 됩니다.
- 작성자가 thread를 읽으면
author_last_read_at이 갱신됩니다. - student 가시성은 작성자 본인에게만 허용됩니다. staff 가시성은 tenant 내부에서 제한되지 않습니다.
일정과 예약
- schedule uniqueness는
(user_id, date, start_time)입니다. - 예약은 schedule이 사용 가능하고 pending 또는 confirmed 예약이 없을 때만 허용됩니다.
- 예약 상태 전이는 다음으로 제한됩니다.
PENDING -> CONFIRMED또는CANCELLEDCONFIRMED -> COMPLETED또는CANCELLED- 이 모듈은 content 측의 활성/예정 의무 때문에 student나 staff 삭제가 막혀야 하는지를 판단하는 guard를 export합니다.
대표 계약 예시
이 예시는 CreateAnnouncementRequest, AnnouncementResponse,
AnnouncementController, AnnouncementCommandService,
AnnouncementQueryService와 일치합니다.
Staff가 반 대상 공지를 생성
POST /v1/announcements
{
"authorId": 900,
"announcementTitle": "보강 일정 안내",
"announcementContent": "이번 주 토요일 10시에 보강 수업이 있습니다.",
"isItAssetAnnouncement": false,
"isItImportantAnnouncement": true,
"textbookFileIds": [],
"classIds": [2, 3]
}
{
"id": 1,
"authorId": 900,
"authorName": "원장",
"announcementTitle": "보강 일정 안내",
"announcementContent": "이번 주 토요일 10시에 보강 수업이 있습니다.",
"isItAssetAnnouncement": false,
"isItImportantAnnouncement": true,
"createdAt": "2026-06-14T09:00:00Z",
"updatedAt": "2026-06-14T09:00:00Z",
"classIds": [2, 3]
}
학생이 같은 공지를 읽음
GET /v1/announcements/1
{
"id": 1,
"authorId": 900,
"authorName": "원장",
"announcementTitle": "보강 일정 안내",
"announcementContent": "이번 주 토요일 10시에 보강 수업이 있습니다.",
"isItAssetAnnouncement": false,
"isItImportantAnnouncement": true,
"createdAt": "2026-06-14T09:00:00Z",
"updatedAt": "2026-06-14T09:00:00Z",
"classIds": []
}
student 응답은 responseClassIds(...)를 통해 의도적으로 classIds를 비웁니다.
현재 학생이 target class 중 하나에 등록되어 있지 않으면,
getAnnouncement(...)는 별도 visibility 오류가 아니라
ANNOUNCEMENT_NOT_FOUND를 반환합니다.
의존성과 경계
| Dependency | 존재 이유 |
|---|---|
ClassService | announcement class target과 student class visibility 검증 |
StudentService | Q&A 또는 announcement reader용 student 이름과 owner visibility 해석 |
StaffService | staff author 이름 해석 |
FileService | announcement와 Q&A의 파일 링크 및 첨부 정리 관리 |
이 모듈은 student, staff, class 레코드를 소유하지 않습니다. foreign key와 유사한 ID만 저장하고, 이름이나 visibility는 query 시점에 internal API를 통해 해석합니다.
실패 모드
- 잘못되었거나 존재하지 않는 class target은
INVALID_CLASS_TARGET을 던집니다. - 숨겨진 공지사항은 별도 permission 오류가 아니라 의도적으로
ANNOUNCEMENT_NOT_FOUND로 surface됩니다. - reservation 쓰기는
SCHEDULE_NOT_FOUND,SCHEDULE_NOT_AVAILABLE,SCHEDULE_FULL,DUPLICATE_RESERVATION,INVALID_RESERVATION_STATUS로 실패할 수 있습니다. - Q&A 읽기/쓰기는 aggregate나 reply가 없을 때
QNA_NOT_FOUND,COMMENT_NOT_FOUND로 실패합니 다. - review popup 상태는 범용
togglestable row를 공유하므로, tenant에 그 row가 존재해야 이 경로가 동작합니다.
관측성
- 모듈은 application service에서 create, update, delete, query 활동을 로그합니다.
- 이 모듈에는 queue, retry, scheduled recovery loop가 없습니다.
QnaBoard,Schedule처럼 pagination에 민감한 entity는 fetch-join pagination 대신 batch-fetch 친화 패턴을 사용합니다.
검증
./gradlew :modules:content:test
./gradlew :app:test --tests '*Content*'
cd lumie-document/docusaurus && npm run build
예상 성공 신호:
- Gradle이
BUILD SUCCESSFUL로 끝납니다. AnnouncementCommandServiceTest와AnnouncementQueryServiceTest가 실패 없이 통과합니다.- Docusaurus가
backend/content-svc에 대해 MDX 또는 broken-link 오류 없이 완료됩니다.