Content 모듈
Content 모듈은 학원 운영에 필요한 다양한 콘텐츠를 관리합니다. 공지사항, Q&A 게시판, 상담 예약, 리뷰, 일정 관리, 교재 관리 등의 기능을 제공합니다.
모듈 개요
- 배포: lumie-backend 모놀리스의
modules/content - 데이터베이스: PostgreSQL (멀티테넌트 RLS)
- 주요 의존성:
TenantService(internal-api)
주요 기능
공지사항 관리
- 일반 공지사항 및 자산 공지사항 구분
- 중요 공지사항 설정
- 학원별 공지사항 타겟팅
- 공지사항 CRUD 및 목록 조회
Q&A 게시판
- 질문 등록 및 답변 관리
- 댓글 시스템 (학생/관리자)
- 미답변 질문 필터링
- 제목, 내용, 학생 정보 기반 키워드 검색
- 사용자별 질문 조회
상담 예약 시스템
- 일정 생성 및 관리
- 예약 상태 관리 (대기/확정/취소/완료)
- 관리자별 일정 조회
- 예약 가능한 일정 필터링
리뷰 관리
- 리뷰 조회 및 삭제
- 리뷰 팝업 설정 관리
교재 관리
- 교재 정보 CRUD
- 과목별 교재 분류
- 교재 상태 관리 (활성/비활성)
API 엔드포인트
공지사항 API
공지사항 목록 조회
GET /v1/announcements?isAsset=false&page=0&size=20
중요 공지사항 조회
GET /v1/announcements/important
공지사항 생성
POST /v1/announcements
Content-Type: application/json
{
"authorId": 1,
"announcementTitle": "새학기 개강 안내",
"announcementContent": "2024년 새학기가 시작됩니다...",
"isItAssetAnnouncement": false,
"isItImportantAnnouncement": true,
"academyIds": [1, 2, 3]
}
공지사항 수정
PUT /v1/announcements/{id}
Content-Type: application/json
{
"announcementTitle": "수정된 제목",
"announcementContent": "수정된 내용",
"isItAssetAnnouncement": false,
"isItImportantAnnouncement": false,
"academyIds": [1, 2]
}
Q&A API
Q&A 목록 조회
GET /v1/qna?keyword=수학&page=0&size=20
keyword는 선택값이며 Q&A 제목, 내용, 학생 정보 검색에 사용됩니다. isAnswered=true|false와 함께 전달하면 답변 상태와 검색어를 동시에 적용합니다.
미답변 Q&A 조회
GET /v1/qna/unanswered?page=0&size=20
사용자별 Q&A 조회
GET /v1/qna/user/{userId}?page=0&size=20
Q&A 생성
POST /v1/qna
Content-Type: application/json
{
"qnaUserId": 123,
"qnaTitle": "수업 시간 문의",
"qnaContent": "다음 주 수업 시간이 변경되나요?"
}
댓글 추가
POST /v1/qna/{qnaId}/comments
Content-Type: application/json
{
"adminId": 1,
"commentContent": "네, 다음 주 화요일 수업은 오후 3시로 변경됩니다."
}
예약 관리 API
일정 생성
POST /v1/schedules
Content-Type: application/json
{
"date": "2024-03-15",
"timeSlotId": 3
}
날짜 범위별 일정 조회
GET /v1/schedules/date-range?startDate=2024-03-01&endDate=2024-03-31
예약 가능한 일정 조회
GET /v1/schedules/available?startDate=2024-03-01&endDate=2024-03-31
예약 생성
POST /v1/reservations
Content-Type: application/json
{
"scheduleId": 1,
"studentId": 123,
"adminId": 1,
"consultationContent": "진로 상담"
}
예약 상태 변경
PUT /v1/reservations/{id}/status
Content-Type: application/json
{
"status": "CONFIRMED"
}
교재 관리 API
교재 목록 조회
GET /v1/textbooks?subject=수학&page=0&size=20
활성 교재 목록
GET /v1/textbooks/active
교재 생성
POST /v1/textbooks
Content-Type: application/json
{
"academyId": 1,
"name": "고등수학 기본서",
"description": "고등학교 1학년 수학 기본서",
"author": "김수학",
"publisher": "수학출판사",
"isbn": "978-89-123-4567-8",
"subject": "수학",
"gradeLevel": "고1",
"price": 25000,
"coverImagePath": "/images/textbook1.jpg"
}
리뷰 관리 API
리뷰 목록 조회
GET /v1/reviews?page=0&size=20
리뷰 팝업 설정 조회
GET /v1/reviews/popup-setting
리뷰 팝업 설정 변경
PUT /v1/reviews/popup-setting
Content-Type: application/json
{
"isReviewPopupOn": true
}
도메인 모델
Announcement 엔티티
@Entity
public class Announcement extends BaseEntity {
private Long authorId; // 작성자 ID
private String announcementTitle; // 제목
private String announcementContent; // 내용
private Boolean isItAssetAnnouncement; // 자산 공지사항 여부
private Boolean isItImportantAnnouncement; // 중요 공지사항 여부
private Set<Long> academyIds; // 대상 학원 ID 목록
public static Announcement create(Long authorId, String title, String content,
Boolean isAsset, Boolean isImportant, Set<Long> academyIds);
public void update(String title, String content, Boolean isAsset, Boolean isImportant);
public void markImportant();
public void unmarkImportant();
}
QnaBoard 엔티티
@Entity
public class QnaBoard extends BaseEntity {
private Long qnaUserId; // 질문자 ID
private String qnaTitle; // 제목
private String qnaContent; // 내용
private Boolean isItAnswered; // 답변 완료 여부
private List<QnaComment> comments; // 댓글 목록
public static QnaBoard create(Long userId, String title, String content);
public void update(String title, String content);
public void markAsAnswered();
public void addComment(QnaComment comment);
}
QnaComment 엔티티
@Entity
public class QnaComment extends BaseEntity {
private QnaBoard qnaBoard; // 소속 Q&A
private Long studentId; // 학생 ID (학생 댓글인 경우)
private Long adminId; // 관리자 ID (관리자 댓글인 경우)
private String commentContent; // 댓글 내용
public static QnaComment createByStudent(QnaBoard qnaBoard, Long studentId, String content);
public static QnaComment createByAdmin(QnaBoard qnaBoard, Long adminId, String content);
public boolean isAdminComment();
public boolean isStudentComment();
}
Schedule 엔티티
@Entity
public class Schedule extends BaseEntity {
private Long adminId; // 담당 관리자 ID
private LocalDate date; // 일정 날짜
private Integer timeSlotId; // 시간대 ID
private Boolean isAvailable; // 예약 가능 여부
private List<Reservation> reservations; // 예약 목록
public static Schedule create(Long adminId, LocalDate date, Integer timeSlotId);
public void update(LocalDate date, Integer timeSlotId);
public void open();
public void close();
public boolean canAcceptReservation();
public boolean hasReservation();
public int getConfirmedCount();
}
Reservation 엔티티
@Entity
public class Reservation extends BaseEntity {
private Schedule schedule; // 예약 일정
private Long studentId; // 학생 ID
private Long adminId; // 담당 관리자 ID
private String consultationContent; // 상담 내용
private ReservationStatus status; // 예약 상태
public static Reservation create(Schedule schedule, Long studentId,
Long adminId, String content);
public void confirm();
public void cancel();
public void complete();
public boolean isPending();
public boolean isConfirmed();
public boolean isCancelled();
public boolean isCompleted();
}
Textbook 엔티티
@Entity
public class Textbook extends BaseEntity {
private Long academyId; // 소속 학원 ID
private String name; // 교재명
private String description; // 설명
private String author; // 저자
private String publisher; // 출판사
private String isbn; // ISBN
private String subject; // 과목
private String gradeLevel; // 학년
private Integer price; // 가격
private String coverImagePath; // 표지 이미지 경로
private TextbookStatus status; // 상태 (ACTIVE/INACTIVE)
public static Textbook create(Long academyId, String name, String description, ...);
public void update(String name, String description, ...);
public void activate();
public void deactivate();
public boolean isActive();
}
비즈니스 로직
예약 상태 전이 규칙
private void validateStatusTransition(ReservationStatus current, ReservationStatus target) {
Map<ReservationStatus, Set<ReservationStatus>> allowedTransitions = Map.of(
PENDING, Set.of(CONFIRMED, CANCELLED),
CONFIRMED, Set.of(COMPLETED, CANCELLED),
CANCELLED, Set.of(), // 취소된 예약은 상태 변경 불가
COMPLETED, Set.of() // 완료된 예약은 상태 변경 불가
);
if (!allowedTransitions.get(current).contains(target)) {
throw new ContentException(ContentErrorCode.INVALID_STATUS_TRANSITION,
String.format("Cannot transition from %s to %s", current, target));
}
}