본문으로 건너뛰기

콘텐츠 서비스

이 페이지는 lumie-backend/modules/content를 다룹니다.

책임

content 모듈은 다음을 소유합니다.

  • 공지사항, 공지사항의 class target, 공지 읽음 추적
  • Q&A 게시판과 댓글 스레드
  • 상담 일정과 예약
  • 리뷰와 리뷰 팝업 토글
  • student나 staff를 제거하기 전에 다른 모듈이 사용하는 삭제 가능성 검사

소스 경로

Path역할
lumie-backend/modules/content/src/main/java/com/lumie/content/adapter/in/webannouncement, Q&A, reservation, schedule, review용 공개 HTTP controller
lumie-backend/modules/content/src/main/java/com/lumie/content/adapter/in/internal/ContentServiceAdapter.javainternal monolith API 구현
lumie-backend/modules/content/src/main/java/com/lumie/content/application/serviceannouncement, Q&A, reservation, schedule, review, 삭제 가능성 service
lumie-backend/modules/content/src/main/java/com/lumie/content/domain/entitycontent aggregate와 보조 table
lumie-backend/libs/internal-api/src/main/java/com/lumie/content/api/ContentService.javaannouncement 생성과 readiness check를 위한 internal 계약
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sqlannouncements, 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.sqlqna_boards.is_it_answeredis_answered로 rename하고 version 추가
lumie-backend/app/src/main/resources/db/migration/public/V24__add_version_to_domain_mutable_entities.sqlannouncement, reservation, schedule, review, toggle에 version 추가
lumie-backend/app/src/main/resources/db/migration/public/V43__qna_boards_add_category_name.sqlQ&A category 데이터의 legacy import 지원
lumie-backend/app/src/main/resources/db/migration/public/V55__create_announcement_class_targets.sqlclass target visibility row 생성
lumie-backend/app/src/main/resources/db/migration/public/V56__announcement_class_targets_tenant_safe_fk.sqlannouncement target FK를 tenant-safe하게 변경
lumie-backend/app/src/main/resources/db/migration/public/V62__add_read_receipt_tables.sqlannouncement_reads 생성
lumie-backend/app/src/main/resources/db/migration/public/V65__repair_read_receipt_and_file_download_rls.sqlannouncement read에 대한 tenant-safe RLS와 FK 동작 복구

공개 인터페이스

SurfaceEntrypoints
AnnouncementsGET/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&AGET/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}
ReservationsGET/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}
SchedulesGET/POST /v1/schedules, GET /v1/schedules/range, GET /v1/schedules/available, GET /v1/schedules/user/{userId}, GET/PATCH/DELETE /v1/schedules/{id}
ReviewsPOST /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합니다.

집계와 테이블

AggregateTable참고
Announcementannouncements기본 공지 레코드
AnnouncementClassTargetannouncement_class_targets선택적 class-target visibility filter
AnnouncementReadannouncement_reads사용자별 읽음 영수증, idempotent insert
QnaBoardqna_boardsis_answered, author_last_read_at를 가진 질문 스레드
QnaCommentqna_comments관리자 또는 학생 댓글
Schedulecounseling_schedulesstaff 상담 slot
Reservationcounseling_reservationsschedule에 연결된 student 예약
Reviewreviews공개 review 텍스트
ReviewPopupSettingtoggles고정 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 또는 CANCELLED
  • CONFIRMED -> 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존재 이유
ClassServiceannouncement class target과 student class visibility 검증
StudentServiceQ&A 또는 announcement reader용 student 이름과 owner visibility 해석
StaffServicestaff author 이름 해석
FileServiceannouncement와 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 상태는 범용 toggles table 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로 끝납니다.
  • AnnouncementCommandServiceTestAnnouncementQueryServiceTest가 실패 없이 통과합니다.
  • Docusaurus가 backend/content-svc에 대해 MDX 또는 broken-link 오류 없이 완료됩니다.

관련 페이지