diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..f10855c --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,31 @@ +services: + mysql: + image: mysql:8.4 + container_name: todayeng-mysql + restart: unless-stopped + ports: + - "3306:3306" + environment: + MYSQL_DATABASE: today_eng + MYSQL_USER: todayeng + MYSQL_PASSWORD: todayeng1234 + MYSQL_ROOT_PASSWORD: root1234 + TZ: Asia/Seoul + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_0900_ai_ci + volumes: + - todayeng-mysql-data:/var/lib/mysql + healthcheck: + test: + [ + "CMD-SHELL", + "mysqladmin ping -h localhost -uroot -proot1234 --silent" + ] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + +volumes: + todayeng-mysql-data: \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/Diary.java b/src/main/java/com/example/todayEng/domain/diary/entity/Diary.java new file mode 100644 index 0000000..e13102f --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/Diary.java @@ -0,0 +1,99 @@ +package com.example.todayEng.domain.diary.entity; + +import com.example.todayEng.domain.diary.entity.enums.DiaryStatus; +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.Table; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +@Getter +@Entity +@Table(name = "diary") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Diary { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "diary_id") + private Long id; + + /* + * User 엔티티 구현 후 수정 + * + * @ManyToOne(fetch = FetchType.LAZY) + * @JoinColumn(name = "user_id", nullable = false) + * private User user; + */ + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "diary_date", nullable = false) + private LocalDate diaryDate; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + private DiaryStatus status; + + @Column(name = "memo", length = 500) + private String memo; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "completed_at") + private LocalDateTime completedAt; + + @Builder(access = AccessLevel.PRIVATE) + private Diary( + Long userId, + LocalDate diaryDate, + String memo + ) { + this.userId = userId; + this.diaryDate = diaryDate; + this.memo = memo; + this.status = DiaryStatus.IN_PROGRESS; + } + + public static Diary create( + Long userId, + LocalDate diaryDate, + String memo + ) { + return Diary.builder() + .userId(userId) + .diaryDate(diaryDate) + .memo(memo) + .build(); + } + + public void updateMemo(String memo) { + this.memo = memo; + } + + public void complete() { + if (this.status == DiaryStatus.COMPLETED) { + return; + } + + this.status = DiaryStatus.COMPLETED; + this.completedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/DiaryAnswer.java b/src/main/java/com/example/todayEng/domain/diary/entity/DiaryAnswer.java new file mode 100644 index 0000000..3230296 --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/DiaryAnswer.java @@ -0,0 +1,116 @@ +package com.example.todayEng.domain.diary.entity; + +import com.example.todayEng.domain.diary.entity.enums.CorrectionStatus; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +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 java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.type.SqlTypes; + +@Getter +@Entity +@Table(name = "diary_answer") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DiaryAnswer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "answer_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn( + name = "question_id", + nullable = false, + unique = true + ) + private DiaryQuestion question; + + @Column(name = "original_text", nullable = false, columnDefinition = "TEXT") + private String originalText; + + @Column(name = "corrected_text", columnDefinition = "TEXT") + private String correctedText; + + @Column(name = "correction_reason", columnDefinition = "TEXT") + private String correctionReason; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "alternative_expression", columnDefinition = "json") + private JsonNode alternativeExpression; + + @Enumerated(EnumType.STRING) + @Column(name = "correction_status", nullable = false, length = 20) + private CorrectionStatus correctionStatus; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Builder(access = AccessLevel.PRIVATE) + private DiaryAnswer( + DiaryQuestion question, + String originalText + ) { + this.question = question; + this.originalText = originalText; + this.correctionStatus = CorrectionStatus.PENDING; + } + + public static DiaryAnswer create( + DiaryQuestion question, + String originalText + ) { + return DiaryAnswer.builder() + .question(question) + .originalText(originalText) + .build(); + } + + public void updateOriginalText(String originalText) { + this.originalText = originalText; + this.correctedText = null; + this.correctionReason = null; + this.alternativeExpression = null; + this.correctionStatus = CorrectionStatus.PENDING; + } + + public void startCorrection() { + this.correctionStatus = CorrectionStatus.PENDING; + } + + public void completeCorrection( + String correctedText, + String correctionReason, + JsonNode alternativeExpression + ) { + this.correctedText = correctedText; + this.correctionReason = correctionReason; + this.alternativeExpression = alternativeExpression; + this.correctionStatus = CorrectionStatus.SUCCEEDED; + } + + public void failCorrection() { + this.correctionStatus = CorrectionStatus.FAILED; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/DiaryContext.java b/src/main/java/com/example/todayEng/domain/diary/entity/DiaryContext.java new file mode 100644 index 0000000..b251d33 --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/DiaryContext.java @@ -0,0 +1,96 @@ +package com.example.todayEng.domain.diary.entity; + +import com.example.todayEng.domain.diary.entity.enums.DiaryContextType; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +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 java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Getter +@Entity +@Table(name = "diary_context") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DiaryContext { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "context_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "diary_id", nullable = false) + private Diary diary; + + @Enumerated(EnumType.STRING) + @Column(name = "context_type", nullable = false, length = 30) + private DiaryContextType contextType; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "context_data", columnDefinition = "json") + private JsonNode contextData; + + @Column(name = "is_success", nullable = false) + private boolean success; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Builder(access = AccessLevel.PRIVATE) + private DiaryContext( + Diary diary, + DiaryContextType contextType, + JsonNode contextData, + boolean success + ) { + this.diary = diary; + this.contextType = contextType; + this.contextData = contextData; + this.success = success; + } + + public static DiaryContext success( + Diary diary, + DiaryContextType contextType, + JsonNode contextData + ) { + return DiaryContext.builder() + .diary(diary) + .contextType(contextType) + .contextData(contextData) + .success(true) + .build(); + } + + public static DiaryContext failure( + Diary diary, + DiaryContextType contextType + ) { + return DiaryContext.builder() + .diary(diary) + .contextType(contextType) + .success(false) + .build(); + } + + public void updateContextData(JsonNode contextData) { + this.contextData = contextData; + this.success = true; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/DiaryQuestion.java b/src/main/java/com/example/todayEng/domain/diary/entity/DiaryQuestion.java new file mode 100644 index 0000000..c1737e3 --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/DiaryQuestion.java @@ -0,0 +1,161 @@ +package com.example.todayEng.domain.diary.entity; + +import com.example.todayEng.domain.diary.entity.enums.QuestionGenerationType; +import com.example.todayEng.domain.diary.entity.enums.QuestionType; +import com.example.todayEng.global.error.ErrorCode; +import com.example.todayEng.global.error.exception.BaseException; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +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 java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +@Getter +@Entity +@Table(name = "diary_question") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DiaryQuestion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "question_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "diary_id", nullable = false) + private Diary diary; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_question_id") + private DiaryQuestion parentQuestion; + + @Enumerated(EnumType.STRING) + @Column(name = "question_type", nullable = false, length = 20) + private QuestionType questionType; + + @Column(name = "question_order", nullable = false) + private Integer questionOrder; + + @Column(name = "question_text", nullable = false, columnDefinition = "TEXT") + private String questionText; + + @Enumerated(EnumType.STRING) + @Column(name = "generation_type", nullable = false, length = 20) + private QuestionGenerationType generationType; + + @Column( + name = "korean_translation", + nullable = false, + columnDefinition = "TEXT" + ) + private String koreanTranslation; + + @Column(name = "keyword", length = 50) + private String keyword; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Builder(access = AccessLevel.PRIVATE) + private DiaryQuestion( + Diary diary, + DiaryQuestion parentQuestion, + QuestionType questionType, + Integer questionOrder, + String questionText, + QuestionGenerationType generationType, + String koreanTranslation, + String keyword + ) { + validateParentQuestion(questionType, parentQuestion); + validateKeyword(questionType, keyword); + + this.diary = diary; + this.parentQuestion = parentQuestion; + this.questionType = questionType; + this.questionOrder = questionOrder; + this.questionText = questionText; + this.generationType = generationType; + this.koreanTranslation = koreanTranslation; + this.keyword = keyword; + } + + public static DiaryQuestion createMainQuestion( + Diary diary, + Integer questionOrder, + String questionText, + QuestionGenerationType generationType, + String koreanTranslation, + String keyword + ) { + return DiaryQuestion.builder() + .diary(diary) + .questionType(QuestionType.MAIN) + .questionOrder(questionOrder) + .questionText(questionText) + .generationType(generationType) + .koreanTranslation(koreanTranslation) + .keyword(keyword) + .build(); + } + + public static DiaryQuestion createFollowUpQuestion( + Diary diary, + DiaryQuestion parentQuestion, + Integer questionOrder, + String questionText, + QuestionGenerationType generationType, + String koreanTranslation + ) { + return DiaryQuestion.builder() + .diary(diary) + .parentQuestion(parentQuestion) + .questionType(QuestionType.FOLLOW_UP) + .questionOrder(questionOrder) + .questionText(questionText) + .generationType(generationType) + .koreanTranslation(koreanTranslation) + .build(); + } + + private static void validateParentQuestion( + QuestionType questionType, + DiaryQuestion parentQuestion + ) { + if (questionType == QuestionType.MAIN && parentQuestion != null) { + throw new BaseException(ErrorCode.MAIN_QUESTION_CANNOT_HAVE_PARENT); + } + + if (questionType == QuestionType.FOLLOW_UP + && parentQuestion == null) { + throw new BaseException(ErrorCode.FOLLOW_UP_QUESTION_REQUIRES_PARENT); + } + + if (parentQuestion != null + && parentQuestion.questionType != QuestionType.MAIN) { + throw new BaseException(ErrorCode.FOLLOW_UP_PARENT_MUST_BE_MAIN_QUESTION); + } + } + + private static void validateKeyword( + QuestionType questionType, + String keyword + ) { + if (questionType == QuestionType.FOLLOW_UP && keyword != null) { + throw new BaseException(ErrorCode.FOLLOW_UP_QUESTION_CANNOT_HAVE_KEYWORD); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/enums/CorrectionStatus.java b/src/main/java/com/example/todayEng/domain/diary/entity/enums/CorrectionStatus.java new file mode 100644 index 0000000..fe56e72 --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/enums/CorrectionStatus.java @@ -0,0 +1,7 @@ +package com.example.todayEng.domain.diary.entity.enums; + +public enum CorrectionStatus { + PENDING, + SUCCEEDED, + FAILED +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/enums/DiaryContextType.java b/src/main/java/com/example/todayEng/domain/diary/entity/enums/DiaryContextType.java new file mode 100644 index 0000000..ea77146 --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/enums/DiaryContextType.java @@ -0,0 +1,9 @@ +package com.example.todayEng.domain.diary.entity.enums; + +public enum DiaryContextType { + CALENDAR, + WEATHER, + PHOTO, + USER_MEMO, + DIARY_MEMORY +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/enums/DiaryStatus.java b/src/main/java/com/example/todayEng/domain/diary/entity/enums/DiaryStatus.java new file mode 100644 index 0000000..ad6d3c1 --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/enums/DiaryStatus.java @@ -0,0 +1,6 @@ +package com.example.todayEng.domain.diary.entity.enums; + +public enum DiaryStatus { + IN_PROGRESS, + COMPLETED +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/enums/QuestionGenerationType.java b/src/main/java/com/example/todayEng/domain/diary/entity/enums/QuestionGenerationType.java new file mode 100644 index 0000000..f2478f5 --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/enums/QuestionGenerationType.java @@ -0,0 +1,6 @@ +package com.example.todayEng.domain.diary.entity.enums; + +public enum QuestionGenerationType { + AI, + DEFAULT +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/domain/diary/entity/enums/QuestionType.java b/src/main/java/com/example/todayEng/domain/diary/entity/enums/QuestionType.java new file mode 100644 index 0000000..0952e5e --- /dev/null +++ b/src/main/java/com/example/todayEng/domain/diary/entity/enums/QuestionType.java @@ -0,0 +1,6 @@ +package com.example.todayEng.domain.diary.entity.enums; + +public enum QuestionType { + MAIN, + FOLLOW_UP +} \ No newline at end of file diff --git a/src/main/java/com/example/todayEng/global/error/ErrorCode.java b/src/main/java/com/example/todayEng/global/error/ErrorCode.java index 06891c5..f94cdf0 100644 --- a/src/main/java/com/example/todayEng/global/error/ErrorCode.java +++ b/src/main/java/com/example/todayEng/global/error/ErrorCode.java @@ -43,7 +43,15 @@ public enum ErrorCode { FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "E001", "S3 파일 업로드에 실패했습니다."), FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "E002", "파일 업로드 용량을 초과했습니다."), INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "E003", "허용되지 않는 파일 확장자입니다."), - EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E004", "외부 연동 API 호출에 실패했습니다."); + EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E004", "외부 연동 API 호출에 실패했습니다."), + + // Diary (D) + MAIN_QUESTION_CANNOT_HAVE_PARENT(HttpStatus.BAD_REQUEST, "D001", "메인 질문에는 상위 질문을 지정할 수 없습니다."), + FOLLOW_UP_QUESTION_REQUIRES_PARENT(HttpStatus.BAD_REQUEST, "D002", "후속 질문에는 메인 질문이 필요합니다."), + FOLLOW_UP_PARENT_MUST_BE_MAIN_QUESTION(HttpStatus.BAD_REQUEST,"D003","후속 질문의 상위 질문은 메인 질문이어야 합니다."), + FOLLOW_UP_QUESTION_CANNOT_HAVE_KEYWORD(HttpStatus.BAD_REQUEST, "D004", "후속 질문에는 키워드를 지정할 수 없습니다.") + + ; private final HttpStatus status; private final String code;