diff --git a/apps/api/src/answer-evaluation/answer-evaluation.constants.ts b/apps/api/src/answer-evaluation/answer-evaluation.constants.ts new file mode 100644 index 0000000..c4fbb8a --- /dev/null +++ b/apps/api/src/answer-evaluation/answer-evaluation.constants.ts @@ -0,0 +1,22 @@ +const AccuracyEval = { + PERFECT: 'PERFECT', + MINOR_ERROR: 'MINOR_ERROR', + WRONG: 'WRONG', +} as const; +type AccuracyEval = (typeof AccuracyEval)[keyof typeof AccuracyEval]; + +const LogicEval = { + CLEAR: 'CLEAR', + WEAK: 'WEAK', + NONE: 'NONE', +} as const; +type LogicEval = (typeof LogicEval)[keyof typeof LogicEval]; + +const DepthEval = { + DEEP: 'DEEP', + BASIC: 'BASIC', + NONE: 'NONE', +} as const; +type DepthEval = (typeof DepthEval)[keyof typeof DepthEval]; + +export { AccuracyEval, LogicEval, DepthEval }; diff --git a/apps/api/src/answer-evaluation/answer-evaluation.entity.ts b/apps/api/src/answer-evaluation/answer-evaluation.entity.ts new file mode 100644 index 0000000..2ffe4f2 --- /dev/null +++ b/apps/api/src/answer-evaluation/answer-evaluation.entity.ts @@ -0,0 +1,56 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + OneToOne, + JoinColumn, +} from 'typeorm'; +import { + AccuracyEval, + LogicEval, + DepthEval, +} from './answer-evaluation.constants'; +import { AnswerSubmission } from 'src/answer-submission/answer-submission.entity'; + +@Entity('answer_evaluations') +class AnswerEvaluation { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'submission_id', type: 'int' }) + submissionId: number; + + @Column({ name: 'feedback_message', type: 'text' }) + feedbackMessage: string; + + @Column({ name: 'detail_analysis', type: 'jsonb' }) + detailAnalysis: any; + + @Column({ name: 'score_details', type: 'jsonb' }) + scoreDetails: any; + + @Column({ name: 'accuracy_eval', type: 'enum', enum: AccuracyEval }) + accuracyEval: AccuracyEval; + + @Column({ name: 'logic_eval', type: 'enum', enum: LogicEval }) + logicEval: LogicEval; + + @Column({ name: 'depth_eval', type: 'enum', enum: DepthEval }) + depthEval: DepthEval; + + @Column({ name: 'has_application', type: 'boolean', default: false }) + hasApplication: boolean; + + @Column({ name: 'is_complete_sentence', type: 'boolean', default: false }) + isCompleteSentence: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @OneToOne(() => AnswerSubmission) + @JoinColumn({ name: 'submission_id' }) + submission: AnswerSubmission; +} + +export { AnswerEvaluation }; diff --git a/apps/api/src/answer-submission/answer-submission.constants.ts b/apps/api/src/answer-submission/answer-submission.constants.ts new file mode 100644 index 0000000..0809530 --- /dev/null +++ b/apps/api/src/answer-submission/answer-submission.constants.ts @@ -0,0 +1,20 @@ +const QuizMode = { + DAILY: 'DAILY', + INTERVIEW: 'INTERVIEW', +} as const; +type QuizMode = (typeof QuizMode)[keyof typeof QuizMode]; + +const InputType = { + VOICE: 'VOICE', + TEXT: 'TEXT', +} as const; +type InputType = (typeof InputType)[keyof typeof InputType]; + +const ProcessStatus = { + PENDING: 'PENDING', + DONE: 'DONE', + FAILED: 'FAILED', +} as const; +type ProcessStatus = (typeof ProcessStatus)[keyof typeof ProcessStatus]; + +export { QuizMode, InputType, ProcessStatus }; diff --git a/apps/api/src/answer-submission/answer-submission.entity.ts b/apps/api/src/answer-submission/answer-submission.entity.ts new file mode 100644 index 0000000..2ab5576 --- /dev/null +++ b/apps/api/src/answer-submission/answer-submission.entity.ts @@ -0,0 +1,75 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, + OneToOne, +} from 'typeorm'; +import { + QuizMode, + InputType, + ProcessStatus, +} from './answer-submission.constants'; +import { Question } from 'src/question/question.entity'; +import { AudioAsset } from 'src/audio-asset/audio-asset.entity'; + +@Entity('answer_submissions') +class AnswerSubmission { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'quiz_mode', type: 'enum', enum: QuizMode }) + quizMode: QuizMode; + + @Column({ name: 'input_type', type: 'enum', enum: InputType }) + inputType: InputType; + + @Column({ name: 'raw_answer', type: 'text' }) + rawAnswer: string; + + @Column({ name: 'taken_time', type: 'int' }) + takenTime: number; + + @Column({ type: 'int', default: 0 }) + score: number; + + @CreateDateColumn({ name: 'submitted_at', type: 'timestamp' }) + submittedAt: Date; + + @Column({ + name: 'stt_status', + type: 'enum', + enum: ProcessStatus, + default: ProcessStatus.PENDING, + }) + sttStatus: ProcessStatus; + + @Column({ + name: 'evaluation_status', + type: 'enum', + enum: ProcessStatus, + default: ProcessStatus.PENDING, + }) + evaluationStatus: ProcessStatus; + + @Column({ name: 'user_id', type: 'int' }) + userId: number; + + @Column({ name: 'question_id', type: 'int' }) + questionId: number; + + @Column({ name: 'audio_asset_id', type: 'int' }) + audioAssetId: number; + + @ManyToOne(() => Question) + @JoinColumn({ name: 'question_id' }) + question: Question; + + @OneToOne(() => AudioAsset) + @JoinColumn({ name: 'audio_asset_id' }) + audioAsset: AudioAsset; +} + +export { AnswerSubmission }; diff --git a/apps/api/src/audio-asset/audio-asset.entity.ts b/apps/api/src/audio-asset/audio-asset.entity.ts new file mode 100644 index 0000000..45e27dc --- /dev/null +++ b/apps/api/src/audio-asset/audio-asset.entity.ts @@ -0,0 +1,27 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +@Entity('audio_assets') +class AudioAsset { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'storage_url', type: 'text' }) + storageUrl: string; + + // BIGINT는 JS에서 범위 문제로 string으로 반환 + @Column({ name: 'byte_size', type: 'bigint' }) + byteSize: string; + + @Column({ name: 'duration_ms', type: 'int' }) + durationMs: number; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; +} + +export { AudioAsset }; diff --git a/apps/api/src/category/category.entity.ts b/apps/api/src/category/category.entity.ts new file mode 100644 index 0000000..22a8815 --- /dev/null +++ b/apps/api/src/category/category.entity.ts @@ -0,0 +1,18 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('categories') +class Category { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'varchar', length: 255 }) + name: string; + + @Column({ name: 'parent_id', type: 'int', nullable: true }) + parentId: number | null; + + @Column({ type: 'int', default: 1 }) + depth: number; +} + +export { Category }; diff --git a/apps/api/src/question-solution/question-solution.entity.ts b/apps/api/src/question-solution/question-solution.entity.ts new file mode 100644 index 0000000..aae8c05 --- /dev/null +++ b/apps/api/src/question-solution/question-solution.entity.ts @@ -0,0 +1,45 @@ +import { Question } from 'src/question/question.entity'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + OneToOne, + JoinColumn, +} from 'typeorm'; + +@Entity('question_solutions') +class QuestionSolution { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'question_id', type: 'int' }) + questionId: number; + + @Column({ name: 'reference_source', type: 'varchar', length: 255 }) + referenceSource: string; + + @Column({ name: 'standard_definition', type: 'text' }) + standardDefinition: string; + + @Column({ name: 'technical_mechanism', type: 'jsonb' }) + technicalMechanism: any; + + @Column({ name: 'key_terminology', type: 'jsonb' }) + keyTerminology: any; + + @Column({ name: 'practical_application', type: 'text' }) + practicalApplication: string; + + @Column({ name: 'common_misconceptions', type: 'text' }) + commonMisconceptions: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @OneToOne(() => Question) + @JoinColumn({ name: 'question_id' }) + question: Question; +} + +export { QuestionSolution }; diff --git a/apps/api/src/question/question.entity.ts b/apps/api/src/question/question.entity.ts new file mode 100644 index 0000000..17ab982 --- /dev/null +++ b/apps/api/src/question/question.entity.ts @@ -0,0 +1,38 @@ +import { Category } from 'src/category/category.entity'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + JoinColumn, + OneToOne, +} from 'typeorm'; + +@Entity('questions') +class Question { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'varchar', length: 255 }) + title: string; + + @Column({ type: 'text' }) + content: string; + + @Column({ name: 'tts_url', type: 'varchar', length: 255, nullable: true }) + ttsUrl: string; + + @Column({ name: 'avg_score', type: 'float', default: 0 }) + avgScore: number; + + @Column({ name: 'avg_importance', type: 'float', default: 0 }) + avgImportance: number; + + @Column({ name: 'category_id', type: 'int', nullable: true }) + categoryId: number; + + @OneToOne(() => Category) + @JoinColumn({ name: 'category_id' }) + category: Category; +} + +export { Question };