diff --git a/src/main/java/com/mr/domain/backingTrack/entity/BackingTrack.java b/src/main/java/com/mr/domain/backingTrack/entity/BackingTrack.java new file mode 100644 index 00000000..d8bf5a73 --- /dev/null +++ b/src/main/java/com/mr/domain/backingTrack/entity/BackingTrack.java @@ -0,0 +1,160 @@ +package com.mr.domain.backingTrack.entity; + +import com.mr.domain.backingTrack.entity.enums.AccessLevel; +import com.mr.domain.backingTrack.entity.enums.Level; +import com.mr.domain.backingTrack.entity.enums.ScaleType; +import com.mr.global.entity.BaseTimeDeletedEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table( + name = "backing_track", + indexes = { + @Index(name = "idx_bt_genre_play", + columnList = "genre, play_count"), + } +) +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class BackingTrack extends BaseTimeDeletedEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "backing_track_id") + private Long id; + + // 유저 아이디 + @Column(name = "user_id", nullable = false) + private Long userId; + + // 학원 아이디 + @Column(name = "academy_id") + private Long academyId; + + // 트랙 이름 + @Column(name = "title", nullable = false, length = 50) + private String title; + + // 장르 + @Column(name = "genre", nullable = false, length = 50) + private String genre; + + // key + @Column(name = "key_signature", nullable = false, length = 20) + private String keySignature; + + // 조성 + @Enumerated(EnumType.STRING) + @Column(name = "scale_type", nullable = false) + private ScaleType scaleType; + + // 박자 + @Column(name = "time_signature", nullable = false, length = 10) + private String timeSignature; + + // bpm + @Column(name = "bpm", nullable = false) + private Integer bpm; + + // 재생 시간 + @Column(name = "playtime_sec", nullable = false) + private Integer playtimeSec; + + // 오디오 파일 + @Column(name = "audio_file_url", length = 255) + private String audioFileUrl; + + @Column(name = "midi_file_url", columnDefinition = "JSON") + private String midiFileUrl; + + // 재생 수 + @Column(name = "play_count", nullable = false) + private Integer playCount; + + // 공개 범위 + @Enumerated(EnumType.STRING) + @Column(name = "access_level", nullable = false) + private AccessLevel accessLevel; + + // 난이도 + @Enumerated(EnumType.STRING) + @Column(name = "level", nullable = false) + private Level level; + + @Builder(access = lombok.AccessLevel.PRIVATE) + private BackingTrack(Long userId, Long academyId, String title, String genre, + String keySignature, ScaleType scaleType, String timeSignature, + Integer bpm, Integer playtimeSec, String audioFileUrl, + String midiFileUrl, Integer playCount, + AccessLevel accessLevel, Level level) { + this.userId = userId; + this.academyId = academyId; + this.title = title; + this.genre = genre; + this.keySignature = keySignature; + this.scaleType = scaleType; + this.timeSignature = timeSignature; + this.bpm = bpm; + this.playtimeSec = playtimeSec; + this.audioFileUrl = audioFileUrl; + this.midiFileUrl = midiFileUrl; + this.playCount = playCount != null ? playCount : 0; + this.accessLevel = accessLevel != null ? accessLevel : AccessLevel.PRIVATE; + this.level = level != null ? level : Level.BASIC; + } + + public static BackingTrack create(Long userId, Long academyId, String title, String genre, + String keySignature, ScaleType scaleType, String timeSignature, + Integer bpm, Integer playtimeSec, String audioFileUrl, + String midiFileUrl, AccessLevel accessLevel, Level level) { + return BackingTrack.builder() + .userId(userId) + .academyId(academyId) + .title(title) + .genre(genre) + .keySignature(keySignature) + .scaleType(scaleType) + .timeSignature(timeSignature) + .bpm(bpm) + .playtimeSec(playtimeSec) + .audioFileUrl(audioFileUrl) + .midiFileUrl(midiFileUrl) + .playCount(0) + .accessLevel(accessLevel) + .level(level) + .build(); + } + + public void updateTrackInfo(String title, String genre, String keySignature, + ScaleType scaleType, String timeSignature, Integer bpm, + Integer playtimeSec, AccessLevel accessLevel, Level level) { + this.title = title; + this.genre = genre; + this.keySignature = keySignature; + this.scaleType = scaleType; + this.timeSignature = timeSignature; + this.bpm = bpm; + this.playtimeSec = playtimeSec; + if (accessLevel != null) this.accessLevel = accessLevel; + if (level != null) this.level = level; + } + + // 공개 범위 수정 + public void changeAccessLevel(AccessLevel accessLevel) { + if (accessLevel != null) { + this.accessLevel = accessLevel; + } + } +} diff --git a/src/main/java/com/mr/domain/backingTrack/entity/ChordProgression.java b/src/main/java/com/mr/domain/backingTrack/entity/ChordProgression.java new file mode 100644 index 00000000..402ca24d --- /dev/null +++ b/src/main/java/com/mr/domain/backingTrack/entity/ChordProgression.java @@ -0,0 +1,77 @@ +package com.mr.domain.backingTrack.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table( + name = "chord_progression", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_chord_progression_track_measure_seq", + columnNames = {"backing_track_id", "measure_no", "sequence_no"} + ) + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChordProgression{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "chord_progression_id") + private Long chordProgressionId; + + // 백킹트랙 아이디 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "backing_track_id", nullable = false) + private BackingTrack backingTrack; + + // 코드 순서 + @Column(name = "sequence_no", nullable = false) + private Integer sequenceNo; + + // 마디 번호 + @Column(name = "measure_no", nullable = false) + private Integer measureNo; + + // 코드명 + @Column(name = "chord_name", nullable = false, length = 30) + private String chordName; + + @Builder(access = AccessLevel.PRIVATE) + private ChordProgression(BackingTrack backingTrack, Integer sequenceNo, Integer measureNo, String chordName) { + this.backingTrack = backingTrack; + this.sequenceNo = sequenceNo; + this.measureNo = measureNo; + this.chordName = chordName; + } + + public static ChordProgression create(BackingTrack backingTrack, Integer sequenceNo, Integer measureNo, String chordName) { + return ChordProgression.builder() + .backingTrack(backingTrack) + .sequenceNo(sequenceNo) + .measureNo(measureNo) + .chordName(chordName) + .build(); + } + + public void updateChordInfo(Integer sequenceNo, Integer measureNo, String chordName) { + this.sequenceNo = sequenceNo; + this.measureNo = measureNo; + this.chordName = chordName; + } +} diff --git a/src/main/java/com/mr/domain/backingTrack/entity/enums/AccessLevel.java b/src/main/java/com/mr/domain/backingTrack/entity/enums/AccessLevel.java new file mode 100644 index 00000000..59ddd78d --- /dev/null +++ b/src/main/java/com/mr/domain/backingTrack/entity/enums/AccessLevel.java @@ -0,0 +1,7 @@ +package com.mr.domain.backingTrack.entity.enums; + +public enum AccessLevel { + PRIVATE, + ACADEMY, + PUBLIC +} diff --git a/src/main/java/com/mr/domain/backingTrack/entity/enums/Level.java b/src/main/java/com/mr/domain/backingTrack/entity/enums/Level.java new file mode 100644 index 00000000..03d3e0b9 --- /dev/null +++ b/src/main/java/com/mr/domain/backingTrack/entity/enums/Level.java @@ -0,0 +1,7 @@ +package com.mr.domain.backingTrack.entity.enums; + +public enum Level { + BASIC, + MED, + ADVANCED +} diff --git a/src/main/java/com/mr/domain/backingTrack/entity/enums/ScaleType.java b/src/main/java/com/mr/domain/backingTrack/entity/enums/ScaleType.java new file mode 100644 index 00000000..b76f35db --- /dev/null +++ b/src/main/java/com/mr/domain/backingTrack/entity/enums/ScaleType.java @@ -0,0 +1,6 @@ +package com.mr.domain.backingTrack.entity.enums; + +public enum ScaleType { + MAJOR, + MINOR +} diff --git a/src/main/java/com/mr/domain/learning/entity/ChordExample.java b/src/main/java/com/mr/domain/learning/entity/ChordExample.java new file mode 100644 index 00000000..015c41db --- /dev/null +++ b/src/main/java/com/mr/domain/learning/entity/ChordExample.java @@ -0,0 +1,86 @@ +package com.mr.domain.learning.entity; + +import com.mr.global.entity.BaseCreatedEntity; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Table( + name = "chord_example" +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChordExample extends BaseCreatedEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "chord_example_id") + private Long id; + + // 학습 단계 id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "learning_step_id", nullable = false) + private LearningStep learningStep; + + // 코드명 + @Column(name = "chord_name", nullable = false, length = 100) + private String chordName; + + // 미디 노트 번호 목록 (chord_example_note 보조 테이블에 매핑) + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable( + name = "chord_example_note", + joinColumns = @JoinColumn(name = "chord_example_id") + ) + @Column(name = "note_number", nullable = false) + private List noteNumbers; + + // 설명 + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @Builder(access = AccessLevel.PRIVATE) + private ChordExample(LearningStep learningStep, String chordName, + List noteNumbers, String description) { + this.learningStep = learningStep; + this.chordName = chordName; + this.noteNumbers = noteNumbers != null ? noteNumbers : new ArrayList<>(); + this.description = description; + } + + public static ChordExample create(LearningStep learningStep, String chordName, + List noteNumbers, String description) { + return ChordExample.builder() + .learningStep(learningStep) + .chordName(chordName) + .noteNumbers(noteNumbers) + .description(description) + .build(); + } + + + public void updateChordExample(String chordName, List noteNumbers, String description) { + this.chordName = chordName; + if (noteNumbers != null) { + this.noteNumbers.clear(); + this.noteNumbers.addAll(noteNumbers); + } + this.description = description; + } +} diff --git a/src/main/java/com/mr/domain/learning/entity/Learning.java b/src/main/java/com/mr/domain/learning/entity/Learning.java new file mode 100644 index 00000000..f9015633 --- /dev/null +++ b/src/main/java/com/mr/domain/learning/entity/Learning.java @@ -0,0 +1,126 @@ +package com.mr.domain.learning.entity; + +import com.mr.domain.learning.entity.enums.LearningCategory; +import com.mr.domain.learning.entity.enums.LearningDifficulty; +import com.mr.global.entity.BaseTimeDeletedEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table( + name = "learning", + indexes = { + @Index(name = "idx_learning_active", + columnList = "is_active") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Learning extends BaseTimeDeletedEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "learning_id") + private Long id; + + //제목 + @Column(name = "title", nullable = false, length = 100) + private String title; + + // 카테고리 + @Enumerated(EnumType.STRING) + @Column(name = "category", nullable = false, length = 50) + private LearningCategory category; + + // 난이도 + @Enumerated(EnumType.STRING) + @Column(name = "difficulty", nullable = false, length = 50) + private LearningDifficulty difficulty; + + // 핵심 요약 + @Column(name = "summary", length = 255) + private String summary; + + // 이론 설명 + @Column(name = "content", nullable = false, columnDefinition = "TEXT") + private String content; + + // 엽습 팁 + @Column(name = "practice_tip", columnDefinition = "TEXT") + private String practiceTip; + + // 소요 시간 + @Column(name = "estimated_minutes") + private int estimatedMinutes = 0; + + // 악기 + @Column(name = "instrument_type", nullable = false) + private String instrumentType; + + // 활성 여부 + @Column(name = "is_active", nullable = false) + private Boolean isActive = true; + + @Builder(access = AccessLevel.PRIVATE) + private Learning(String title, LearningCategory category, LearningDifficulty difficulty, + String summary, String content, String practiceTip, + Integer estimatedMinutes, String instrumentType, Boolean isActive) { + this.title = title; + this.category = category != null ? category : LearningCategory.THEORY; + this.difficulty = difficulty != null ? difficulty : LearningDifficulty.BEGINNER; // Default: BEGINNER + this.summary = summary; + this.content = content; + this.practiceTip = practiceTip; + this.estimatedMinutes = estimatedMinutes != null ? estimatedMinutes : 0; + this.instrumentType = instrumentType; + this.isActive = isActive != null ? isActive : true; + } + + public static Learning create(String title, LearningCategory category, LearningDifficulty difficulty, + String summary, String content, String practiceTip, + Integer estimatedMinutes, String instrumentType, Boolean isActive) { + return Learning.builder() + .title(title) + .category(category) + .difficulty(difficulty) + .summary(summary) + .content(content) + .practiceTip(practiceTip) + .estimatedMinutes(estimatedMinutes) + .instrumentType(instrumentType) + .isActive(isActive) + .build(); + } + + public void activate() { + this.isActive = true; + } + + public void deactivate() { + this.isActive = false; + } + + public void updateContent(String title, LearningCategory category, LearningDifficulty difficulty, + String summary, String content, String practiceTip, + Integer estimatedMinutes, String instrumentType) { + this.title = title; + this.category = category; + this.difficulty = difficulty; + this.summary = summary; + this.content = content; + this.practiceTip = practiceTip; + if (estimatedMinutes != null) this.estimatedMinutes = estimatedMinutes; + if (instrumentType != null) this.instrumentType = instrumentType; + } +} diff --git a/src/main/java/com/mr/domain/learning/entity/LearningStep.java b/src/main/java/com/mr/domain/learning/entity/LearningStep.java new file mode 100644 index 00000000..150b3ec6 --- /dev/null +++ b/src/main/java/com/mr/domain/learning/entity/LearningStep.java @@ -0,0 +1,104 @@ +package com.mr.domain.learning.entity; + +import com.mr.global.entity.BaseCreatedEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Builder; + +@Entity +@Getter +@Table( + name = "learning_step", + indexes = { + @Index(name = "idx_learning_step_course_seq", + columnList = "learning_id, step_no", + unique = true) + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LearningStep extends BaseCreatedEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "learning_step_id") + private Long id; + + // 학습 아이디 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "learning_id", nullable = false) + private Learning learning; + + // 순서 + @Column(name = "step_no", nullable = false) + private Integer stepNo; + + // 제목 + @Column(name = "title", nullable = false, length = 100) + private String title; + + // 핵심 요약 + @Column(name = "summary", length = 255) + private String summary; + + // 설명 / 이론 설명 + @Column(name = "content", columnDefinition = "TEXT") + private String content; + + // 내용 연습 팁 + @Column(name = "practice_tip", columnDefinition = "TEXT") + private String practiceTip; + + // 소요 시간 + @Column(name = "estimated_minutes", nullable = false) + private Integer estimatedMinutes = 10; + + @Builder(access = AccessLevel.PRIVATE) + private LearningStep(Learning learning, Integer stepNo, String title, + String summary, String content, String practiceTip, + Integer estimatedMinutes) { + this.learning = learning; + this.stepNo = stepNo; + this.title = title; + this.summary = summary; + this.content = content; + this.practiceTip = practiceTip; + this.estimatedMinutes = estimatedMinutes != null ? estimatedMinutes : 10; + } + + public static LearningStep create(Learning learning, Integer stepNo, String title, + String summary, String content, String practiceTip, + Integer estimatedMinutes) { + return LearningStep.builder() + .learning(learning) + .stepNo(stepNo) + .title(title) + .summary(summary) + .content(content) + .practiceTip(practiceTip) + .estimatedMinutes(estimatedMinutes) + .build(); + } + + public void updateStepInfo(Integer stepNo, String title, String summary, + String content, String practiceTip, Integer estimatedMinutes) { + this.stepNo = stepNo; + this.title = title; + this.summary = summary; + this.content = content; + this.practiceTip = practiceTip; + if (estimatedMinutes != null) { + this.estimatedMinutes = estimatedMinutes; + } + } +} diff --git a/src/main/java/com/mr/domain/learning/entity/PlayingExample.java b/src/main/java/com/mr/domain/learning/entity/PlayingExample.java new file mode 100644 index 00000000..bbf462f7 --- /dev/null +++ b/src/main/java/com/mr/domain/learning/entity/PlayingExample.java @@ -0,0 +1,107 @@ +package com.mr.domain.learning.entity; + +import com.mr.global.entity.BaseCreatedEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; +import lombok.Builder; + +@Entity +@Getter +@Table( + name = "playing_example" +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PlayingExample extends BaseCreatedEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "learning_example_id") + private Long id; + + // 학습 단계 엔티티와 일대일(1:1) 매핑 (외래 키 관리자) + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "learning_step_id", nullable = false, unique = true) + private LearningStep learningStep; + + // 제목 + @Column(name = "title", nullable = false, length = 100) + private String title; + + // 미디 파일 데이터 + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "midi_data", nullable = false, columnDefinition = "JSON") + private String midiData; + + // 오디오 파일 + @Column(name = "audio_file_url", nullable = false, length = 255) + private String audioFileUrl; + + // bpm + @Column(name = "bpm") + private Integer bpm; + + // key + @Column(name = "key_signature", length = 20) + private String keySignature; + + // 설명 + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + // 재생 시간 (초 단위 저장) + @Column(name = "playing_seconds") + private Long playingSeconds; + + @Builder(access = AccessLevel.PRIVATE) + private PlayingExample(LearningStep learningStep, String title, String midiData, + String audioFileUrl, Integer bpm, String keySignature, + String description, Long playingSeconds) { + this.learningStep = learningStep; + this.title = title; + this.midiData = midiData; + this.audioFileUrl = audioFileUrl; + this.bpm = bpm; + this.keySignature = keySignature; + this.description = description; + this.playingSeconds = playingSeconds; + } + + public static PlayingExample create(LearningStep learningStep, String title, String midiData, + String audioFileUrl, Integer bpm, String keySignature, + String description, Long playingSeconds) { + return PlayingExample.builder() + .learningStep(learningStep) + .title(title) + .midiData(midiData) + .audioFileUrl(audioFileUrl) + .bpm(bpm) + .keySignature(keySignature) + .description(description) + .playingSeconds(playingSeconds) + .build(); + } + + public void updatePlayingExample(String title, String midiData, String audioFileUrl, + Integer bpm, String keySignature, String description, + Long playingSeconds) { + this.title = title; + this.midiData = midiData; + this.audioFileUrl = audioFileUrl; + this.bpm = bpm; + this.keySignature = keySignature; + this.description = description; + this.playingSeconds = playingSeconds; + } +} diff --git a/src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java b/src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java new file mode 100644 index 00000000..4778c11b --- /dev/null +++ b/src/main/java/com/mr/domain/learning/entity/UserLearningProgress.java @@ -0,0 +1,86 @@ +package com.mr.domain.learning.entity; + +import com.mr.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Table( + name = "user_learning_progress", + indexes = { + // 특정 유저가 이 학습 단계를 진행했는지 확인/조회할 때 + @Index(name = "idx_user_learning_step", + columnList = "user_id, learning_step_id", + unique = true), + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserLearningProgress extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_learning_progress_id") + private Long id; + + // 유저 아이디 + @Column(name = "user_id", nullable = false) + private Long userId; + + // 학습 아이디 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "learning_id", nullable = false) + private Learning learning; + + // 학습 단계 아이디 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "learning_step_id", nullable = false) + private LearningStep learningStep; + + // 점수 + @Column(name = "score") + private Integer score; + + // 마지막 학습일 + @Column(name = "last_studied_at") + private LocalDateTime lastStudiedAt; + + @Builder(access = AccessLevel.PRIVATE) + private UserLearningProgress(Long userId, Learning learning, LearningStep learningStep, + Integer score, LocalDateTime lastStudiedAt) { + this.userId = userId; + this.learning = learning; + this.learningStep = learningStep; + this.score = score; + this.lastStudiedAt = lastStudiedAt != null ? lastStudiedAt : LocalDateTime.now(); + } + + public static UserLearningProgress create(Long userId, Learning learning, LearningStep learningStep) { + return UserLearningProgress.builder() + .userId(userId) + .learning(learning) + .learningStep(learningStep) + .lastStudiedAt(LocalDateTime.now()) + .build(); + } + + // 학습 시간 및 점수 업데이트 + public void updateProgress(Integer score, LocalDateTime lastStudiedAt) { + this.score = score; + this.lastStudiedAt = lastStudiedAt != null ? lastStudiedAt : LocalDateTime.now(); + } +} diff --git a/src/main/java/com/mr/domain/learning/entity/enums/LearningCategory.java b/src/main/java/com/mr/domain/learning/entity/enums/LearningCategory.java new file mode 100644 index 00000000..a8bd5872 --- /dev/null +++ b/src/main/java/com/mr/domain/learning/entity/enums/LearningCategory.java @@ -0,0 +1,6 @@ +package com.mr.domain.learning.entity.enums; + +public enum LearningCategory { + THEORY, + ACCOMPANIMENT +} diff --git a/src/main/java/com/mr/domain/learning/entity/enums/LearningDifficulty.java b/src/main/java/com/mr/domain/learning/entity/enums/LearningDifficulty.java new file mode 100644 index 00000000..826f0c23 --- /dev/null +++ b/src/main/java/com/mr/domain/learning/entity/enums/LearningDifficulty.java @@ -0,0 +1,7 @@ +package com.mr.domain.learning.entity.enums; + +public enum LearningDifficulty { + BEGINNER, + INTERMEDIATE, + ADVANCED +} diff --git a/src/main/java/com/mr/domain/notification/entity/Notification.java b/src/main/java/com/mr/domain/notification/entity/Notification.java new file mode 100644 index 00000000..585822fb --- /dev/null +++ b/src/main/java/com/mr/domain/notification/entity/Notification.java @@ -0,0 +1,75 @@ +package com.mr.domain.notification.entity; + +import com.mr.global.entity.BaseTimeDeletedEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "notification", + indexes = { + // 1. 특정 유저의 알림 목록을 최신순(생성일순)으로 정렬해 보여줄 때 + @Index(name = "idx_notification_user_created", + columnList = "user_id, created_at"), + + // 2. 유저가 읽지 않은 알림 개수를 셀 때 + @Index(name = "idx_notification_user_read", + columnList = "user_id, is_read") + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notification extends BaseTimeDeletedEntity{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_id") + private Long id; + + // 유저 아이디 + @Column(name = "user_id", nullable = false) + private Long userId; + + // 제목 + @Column(name = "title", nullable = false, length = 100) + private String title; + + // 내용 + @Column(name = "content", nullable = false, columnDefinition = "TEXT") + private String content; + + // 읽음 여부 + @Column(name = "is_read", nullable = false) + private boolean isRead = false; + + + @Builder(access = AccessLevel.PRIVATE) + private Notification(Long userId, String title, String content) { + this.userId = userId; + this.title = title; + this.content = content; + this.isRead = false; + } + + public static Notification create(Long userId, String title, String content) { + return Notification.builder() + .userId(userId) + .title(title) + .content(content) + .build(); + } + + // 알림 읽음 처리 + public void markAsRead() { + this.isRead = true; + } +} \ No newline at end of file diff --git a/src/main/java/com/mr/domain/subscriptions/entity/Subscription.java b/src/main/java/com/mr/domain/subscriptions/entity/Subscription.java new file mode 100644 index 00000000..e655fe74 --- /dev/null +++ b/src/main/java/com/mr/domain/subscriptions/entity/Subscription.java @@ -0,0 +1,92 @@ +package com.mr.domain.subscriptions.entity; + +import com.mr.domain.subscriptions.exception.SubscriptionErrorStatus; +import com.mr.global.apipayload.exception.GeneralException; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Builder; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Table( + name="subscriptions" +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "subscription_id") + private Long id; + + // 유저 아이디 + @Column(name = "user_id", nullable = false) + private Long userId; + + // 구독 등급 + @Column(name = "tier", nullable = false, length = 20) + private String tier; + + // 구독 시작일 + @Column(name = "start_date", nullable = false) + private LocalDateTime startDate; + + // 구독 종료일 + @Column(name = "end_date", nullable = false) + private LocalDateTime endDate; + + @Builder(access = AccessLevel.PRIVATE) + private Subscription(Long userId, String tier, LocalDateTime startDate, LocalDateTime endDate) { + this.userId = userId; + this.tier = tier; + this.startDate = startDate; + this.endDate = endDate; + } + + // 정적 팩토리 메서드 (구독 생성) + public static Subscription create(Long userId, String tier, LocalDateTime startDate, LocalDateTime endDate) { + validateDates(startDate, endDate); + return Subscription.builder() + .userId(userId) + .tier(tier) + .startDate(startDate) + .endDate(endDate) + .build(); + } + + private static void validateDates(LocalDateTime startDate, LocalDateTime endDate) { + if (startDate != null && endDate != null && endDate.isBefore(startDate)) { + throw new GeneralException(SubscriptionErrorStatus.INVALID_SUBSCRIPTION_DATE); + } + } + + // 현재 구독 유효 여부 확인 (현재 시간이 시작일과 종료일 사이인지) + public boolean isActive() { + LocalDateTime now = LocalDateTime.now(); + return !now.isBefore(this.startDate) && !now.isAfter(this.endDate); + } + + // 구독 기간 연장 + public void extendSubscription(LocalDateTime newEndDate) { + if (newEndDate == null || !newEndDate.isAfter(this.endDate)) { + throw new GeneralException(SubscriptionErrorStatus.INVALID_SUBSCRIPTION_EXTENSION_DATE); + } + this.endDate = newEndDate; + } + + // 구독 티어 변경 + public void changeTier(String newTier) { + if (newTier != null && !newTier.isBlank()) { + this.tier = newTier; + } + } +} diff --git a/src/main/java/com/mr/domain/subscriptions/exception/SubscriptionErrorStatus.java b/src/main/java/com/mr/domain/subscriptions/exception/SubscriptionErrorStatus.java new file mode 100644 index 00000000..f3a6b9e8 --- /dev/null +++ b/src/main/java/com/mr/domain/subscriptions/exception/SubscriptionErrorStatus.java @@ -0,0 +1,18 @@ +package com.mr.domain.subscriptions.exception; + +import com.mr.global.apipayload.code.BaseCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum SubscriptionErrorStatus implements BaseCode { + + INVALID_SUBSCRIPTION_DATE(HttpStatus.BAD_REQUEST, "SUBSCRIPTION_400_01", "구독 종료일은 시작일보다 이전일 수 없습니다."), + INVALID_SUBSCRIPTION_EXTENSION_DATE(HttpStatus.BAD_REQUEST, "SUBSCRIPTION_400_02", "연장할 종료일은 기존 종료일보다 이후여야 합니다."); + + private final HttpStatus status; + private final String code; + private final String message; +}