feat: initial english learning backend
This commit is contained in:
commit
ad897dc1e1
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.js.map
|
||||||
|
.env
|
||||||
5
nest-cli.json
Normal file
5
nest-cli.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src"
|
||||||
|
}
|
||||||
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "english-back",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"build": "nest build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/typeorm": "^10.0.0",
|
||||||
|
"@nestjs/schedule": "^4.0.0",
|
||||||
|
"typeorm": "^0.3.0",
|
||||||
|
"mysql2": "^3.0.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rxjs": "^7.8.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"@nestjs/schematics": "^10.0.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"@types/node": "^20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/app.module.ts
Normal file
36
src/app.module.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { WordsModule } from './words/words.module';
|
||||||
|
import { QuizModule } from './quiz/quiz.module';
|
||||||
|
import { QuestModule } from './quest/quest.module';
|
||||||
|
import { PhrasesModule } from './phrases/phrases.module';
|
||||||
|
import { GrammarModule } from './grammar/grammar.module';
|
||||||
|
import { Word } from './words/entities/word.entity';
|
||||||
|
import { Phrase } from './phrases/entities/phrase.entity';
|
||||||
|
import { GrammarLesson } from './grammar/entities/grammar.entity';
|
||||||
|
import { DailyQuest } from './quest/entities/quest.entity';
|
||||||
|
import { QuizResult } from './quiz/entities/quiz-result.entity';
|
||||||
|
import { Streak } from './quest/entities/streak.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'mysql',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3306,
|
||||||
|
username: 'kakao',
|
||||||
|
password: '486251daKWON@',
|
||||||
|
database: 'english_study',
|
||||||
|
entities: [Word, Phrase, GrammarLesson, DailyQuest, QuizResult, Streak],
|
||||||
|
synchronize: true,
|
||||||
|
}),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
WordsModule,
|
||||||
|
QuizModule,
|
||||||
|
QuestModule,
|
||||||
|
PhrasesModule,
|
||||||
|
GrammarModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
22
src/grammar/entities/grammar.entity.ts
Normal file
22
src/grammar/entities/grammar.entity.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('grammar_lessons')
|
||||||
|
export class GrammarLesson {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
explanation: string;
|
||||||
|
|
||||||
|
@Column({ type: 'json' })
|
||||||
|
examples: { en: string; ko: string }[];
|
||||||
|
|
||||||
|
@Column({ default: 'beginner' })
|
||||||
|
level: string;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
order_num: number;
|
||||||
|
}
|
||||||
9
src/grammar/grammar.controller.ts
Normal file
9
src/grammar/grammar.controller.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Controller, Get, Param } from '@nestjs/common';
|
||||||
|
import { GrammarService } from './grammar.service';
|
||||||
|
|
||||||
|
@Controller('grammar')
|
||||||
|
export class GrammarController {
|
||||||
|
constructor(private readonly service: GrammarService) {}
|
||||||
|
@Get() findAll() { return this.service.findAll(); }
|
||||||
|
@Get(':id') findOne(@Param('id') id: string) { return this.service.findOne(+id); }
|
||||||
|
}
|
||||||
12
src/grammar/grammar.module.ts
Normal file
12
src/grammar/grammar.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { GrammarLesson } from './entities/grammar.entity';
|
||||||
|
import { GrammarController } from './grammar.controller';
|
||||||
|
import { GrammarService } from './grammar.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([GrammarLesson])],
|
||||||
|
controllers: [GrammarController],
|
||||||
|
providers: [GrammarService],
|
||||||
|
})
|
||||||
|
export class GrammarModule {}
|
||||||
11
src/grammar/grammar.service.ts
Normal file
11
src/grammar/grammar.service.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { GrammarLesson } from './entities/grammar.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GrammarService {
|
||||||
|
constructor(@InjectRepository(GrammarLesson) private repo: Repository<GrammarLesson>) {}
|
||||||
|
findAll() { return this.repo.find({ order: { order_num: 'ASC' } }); }
|
||||||
|
findOne(id: number) { return this.repo.findOne({ where: { id } }); }
|
||||||
|
}
|
||||||
11
src/main.ts
Normal file
11
src/main.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.enableCors({ origin: '*' });
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
await app.listen(3011);
|
||||||
|
console.log('English Learning API running on port 3011');
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
19
src/phrases/entities/phrase.entity.ts
Normal file
19
src/phrases/entities/phrase.entity.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('phrases')
|
||||||
|
export class Phrase {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
english: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
korean: string;
|
||||||
|
|
||||||
|
@Column({ default: 'greeting' })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
situation: string;
|
||||||
|
}
|
||||||
10
src/phrases/phrases.controller.ts
Normal file
10
src/phrases/phrases.controller.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { PhrasesService } from './phrases.service';
|
||||||
|
|
||||||
|
@Controller('phrases')
|
||||||
|
export class PhrasesController {
|
||||||
|
constructor(private readonly service: PhrasesService) {}
|
||||||
|
@Get() findAll(@Query('category') category?: string) { return this.service.findAll(category); }
|
||||||
|
@Get('categories') getCategories() { return this.service.getCategories(); }
|
||||||
|
@Get('daily') getDaily() { return this.service.getDailyPhrases(); }
|
||||||
|
}
|
||||||
13
src/phrases/phrases.module.ts
Normal file
13
src/phrases/phrases.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Phrase } from './entities/phrase.entity';
|
||||||
|
import { PhrasesController } from './phrases.controller';
|
||||||
|
import { PhrasesService } from './phrases.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Phrase])],
|
||||||
|
controllers: [PhrasesController],
|
||||||
|
providers: [PhrasesService],
|
||||||
|
exports: [PhrasesService],
|
||||||
|
})
|
||||||
|
export class PhrasesModule {}
|
||||||
20
src/phrases/phrases.service.ts
Normal file
20
src/phrases/phrases.service.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Phrase } from './entities/phrase.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PhrasesService {
|
||||||
|
constructor(@InjectRepository(Phrase) private repo: Repository<Phrase>) {}
|
||||||
|
findAll(category?: string) {
|
||||||
|
if (category) return this.repo.find({ where: { category } });
|
||||||
|
return this.repo.find();
|
||||||
|
}
|
||||||
|
getCategories() {
|
||||||
|
return this.repo.createQueryBuilder('p').select('DISTINCT p.category', 'category').getRawMany();
|
||||||
|
}
|
||||||
|
async getDailyPhrases(): Promise<Phrase[]> {
|
||||||
|
const all = await this.repo.find();
|
||||||
|
return all.sort(() => Math.random() - 0.5).slice(0, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/quest/entities/quest.entity.ts
Normal file
25
src/quest/entities/quest.entity.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('daily_quests')
|
||||||
|
export class DailyQuest {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
session_id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'date' })
|
||||||
|
quest_date: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
word_done: boolean;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
phrase_done: boolean;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
quiz_done: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
quiz_score: number;
|
||||||
|
}
|
||||||
22
src/quest/entities/streak.entity.ts
Normal file
22
src/quest/entities/streak.entity.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('streaks')
|
||||||
|
export class Streak {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
session_id: string;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
current_streak: number;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
max_streak: number;
|
||||||
|
|
||||||
|
@Column({ type: 'date', nullable: true })
|
||||||
|
last_date: string;
|
||||||
|
|
||||||
|
@Column({ type: 'json', nullable: true })
|
||||||
|
history: string[];
|
||||||
|
}
|
||||||
27
src/quest/quest.controller.ts
Normal file
27
src/quest/quest.controller.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
|
||||||
|
import { QuestService } from './quest.service';
|
||||||
|
|
||||||
|
@Controller('quest')
|
||||||
|
export class QuestController {
|
||||||
|
constructor(private readonly service: QuestService) {}
|
||||||
|
|
||||||
|
@Get('today')
|
||||||
|
getToday(@Query('session') session: string) {
|
||||||
|
return this.service.getOrCreateQuest(session || 'default');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('complete')
|
||||||
|
complete(@Body() body: { session_id: string; type: 'word' | 'phrase' | 'quiz'; score?: number }) {
|
||||||
|
return this.service.completeQuest(body.session_id, body.type, body.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('streak')
|
||||||
|
getStreak(@Query('session') session: string) {
|
||||||
|
return this.service.getStreak(session || 'default');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('history')
|
||||||
|
getHistory(@Query('session') session: string) {
|
||||||
|
return this.service.getHistory(session || 'default');
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/quest/quest.module.ts
Normal file
14
src/quest/quest.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { DailyQuest } from './entities/quest.entity';
|
||||||
|
import { Streak } from './entities/streak.entity';
|
||||||
|
import { QuestController } from './quest.controller';
|
||||||
|
import { QuestService } from './quest.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([DailyQuest, Streak])],
|
||||||
|
controllers: [QuestController],
|
||||||
|
providers: [QuestService],
|
||||||
|
exports: [QuestService],
|
||||||
|
})
|
||||||
|
export class QuestModule {}
|
||||||
72
src/quest/quest.service.ts
Normal file
72
src/quest/quest.service.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { DailyQuest } from './entities/quest.entity';
|
||||||
|
import { Streak } from './entities/streak.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QuestService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(DailyQuest) private questRepo: Repository<DailyQuest>,
|
||||||
|
@InjectRepository(Streak) private streakRepo: Repository<Streak>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getOrCreateQuest(session_id: string): Promise<DailyQuest> {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
let quest = await this.questRepo.findOne({ where: { session_id, quest_date: today } });
|
||||||
|
if (!quest) {
|
||||||
|
quest = this.questRepo.create({ session_id, quest_date: today });
|
||||||
|
await this.questRepo.save(quest);
|
||||||
|
}
|
||||||
|
return quest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeQuest(session_id: string, type: 'word' | 'phrase' | 'quiz', score?: number): Promise<DailyQuest> {
|
||||||
|
const quest = await this.getOrCreateQuest(session_id);
|
||||||
|
if (type === 'word') quest.word_done = true;
|
||||||
|
if (type === 'phrase') quest.phrase_done = true;
|
||||||
|
if (type === 'quiz') { quest.quiz_done = true; if (score !== undefined) quest.quiz_score = score; }
|
||||||
|
await this.questRepo.save(quest);
|
||||||
|
|
||||||
|
if (quest.word_done && quest.phrase_done && quest.quiz_done) {
|
||||||
|
await this.updateStreak(session_id);
|
||||||
|
}
|
||||||
|
return quest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStreak(session_id: string): Promise<Streak> {
|
||||||
|
let streak = await this.streakRepo.findOne({ where: { session_id } });
|
||||||
|
if (!streak) {
|
||||||
|
streak = this.streakRepo.create({ session_id, history: [] });
|
||||||
|
await this.streakRepo.save(streak);
|
||||||
|
}
|
||||||
|
return streak;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStreak(session_id: string): Promise<Streak> {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
let streak = await this.getStreak(session_id);
|
||||||
|
if (streak.last_date === today) return streak;
|
||||||
|
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
const yest = yesterday.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
if (streak.last_date === yest) {
|
||||||
|
streak.current_streak += 1;
|
||||||
|
} else {
|
||||||
|
streak.current_streak = 1;
|
||||||
|
}
|
||||||
|
streak.max_streak = Math.max(streak.max_streak, streak.current_streak);
|
||||||
|
streak.last_date = today;
|
||||||
|
if (!streak.history) streak.history = [];
|
||||||
|
if (!streak.history.includes(today)) streak.history.push(today);
|
||||||
|
return this.streakRepo.save(streak);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHistory(session_id: string) {
|
||||||
|
const quests = await this.questRepo.find({ where: { session_id } });
|
||||||
|
const streak = await this.getStreak(session_id);
|
||||||
|
return { quests, streak };
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/quiz/entities/quiz-result.entity.ts
Normal file
19
src/quiz/entities/quiz-result.entity.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('quiz_results')
|
||||||
|
export class QuizResult {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
session_id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
score: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
11
src/quiz/quiz.controller.ts
Normal file
11
src/quiz/quiz.controller.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
|
||||||
|
import { QuizService } from './quiz.service';
|
||||||
|
|
||||||
|
@Controller('quiz')
|
||||||
|
export class QuizController {
|
||||||
|
constructor(private readonly service: QuizService) {}
|
||||||
|
@Get('generate') generate(@Query('count') count?: string) { return this.service.generateQuiz(count ? +count : 5); }
|
||||||
|
@Post('result') saveResult(@Body() body: { session_id: string; score: number; total: number }) {
|
||||||
|
return this.service.saveResult(body.session_id, body.score, body.total);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/quiz/quiz.module.ts
Normal file
14
src/quiz/quiz.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { QuizResult } from './entities/quiz-result.entity';
|
||||||
|
import { QuizController } from './quiz.controller';
|
||||||
|
import { QuizService } from './quiz.service';
|
||||||
|
import { WordsModule } from '../words/words.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([QuizResult]), WordsModule],
|
||||||
|
controllers: [QuizController],
|
||||||
|
providers: [QuizService],
|
||||||
|
exports: [QuizService],
|
||||||
|
})
|
||||||
|
export class QuizModule {}
|
||||||
35
src/quiz/quiz.service.ts
Normal file
35
src/quiz/quiz.service.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { QuizResult } from './entities/quiz-result.entity';
|
||||||
|
import { WordsService } from '../words/words.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QuizService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(QuizResult) private repo: Repository<QuizResult>,
|
||||||
|
private wordsService: WordsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async generateQuiz(count = 5) {
|
||||||
|
const words = await this.wordsService.getQuizWords(count + 12);
|
||||||
|
const selected = words.slice(0, count);
|
||||||
|
const pool = words.slice(count);
|
||||||
|
|
||||||
|
return selected.map((word) => {
|
||||||
|
const wrong = pool.sort(() => Math.random() - 0.5).slice(0, 3);
|
||||||
|
const choices = [word.korean, ...wrong.map((w) => w.korean)].sort(() => Math.random() - 0.5);
|
||||||
|
return {
|
||||||
|
id: word.id,
|
||||||
|
english: word.english,
|
||||||
|
pronunciation: word.pronunciation,
|
||||||
|
answer: word.korean,
|
||||||
|
choices,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveResult(session_id: string, score: number, total: number) {
|
||||||
|
return this.repo.save({ session_id, score, total });
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/words/entities/word.entity.ts
Normal file
34
src/words/entities/word.entity.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('words')
|
||||||
|
export class Word {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
english: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
korean: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
pronunciation: string;
|
||||||
|
|
||||||
|
@Column({ default: 'general' })
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
@Column({ default: 'beginner' })
|
||||||
|
difficulty: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'text' })
|
||||||
|
example_en: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'text' })
|
||||||
|
example_ko: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
is_daily: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'date', nullable: true })
|
||||||
|
daily_date: string;
|
||||||
|
}
|
||||||
27
src/words/words.controller.ts
Normal file
27
src/words/words.controller.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { WordsService } from './words.service';
|
||||||
|
|
||||||
|
@Controller('words')
|
||||||
|
export class WordsController {
|
||||||
|
constructor(private readonly service: WordsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll(@Query('category') category?: string) {
|
||||||
|
return this.service.findAll(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('daily')
|
||||||
|
getDaily() {
|
||||||
|
return this.service.getDailyWords();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('quiz')
|
||||||
|
getQuizWords(@Query('count') count?: string) {
|
||||||
|
return this.service.getQuizWords(count ? +count : 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('categories')
|
||||||
|
getCategories() {
|
||||||
|
return this.service.getCategories();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/words/words.module.ts
Normal file
13
src/words/words.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Word } from './entities/word.entity';
|
||||||
|
import { WordsController } from './words.controller';
|
||||||
|
import { WordsService } from './words.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Word])],
|
||||||
|
controllers: [WordsController],
|
||||||
|
providers: [WordsService],
|
||||||
|
exports: [WordsService],
|
||||||
|
})
|
||||||
|
export class WordsModule {}
|
||||||
44
src/words/words.service.ts
Normal file
44
src/words/words.service.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Word } from './entities/word.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WordsService {
|
||||||
|
constructor(@InjectRepository(Word) private repo: Repository<Word>) {}
|
||||||
|
|
||||||
|
findAll(category?: string) {
|
||||||
|
if (category) return this.repo.find({ where: { category } });
|
||||||
|
return this.repo.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
findByCategory(category: string) {
|
||||||
|
return this.repo.find({ where: { category } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDailyWords(): Promise<Word[]> {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const existing = await this.repo.find({ where: { daily_date: today, is_daily: true } });
|
||||||
|
if (existing.length >= 5) return existing;
|
||||||
|
|
||||||
|
const all = await this.repo.find();
|
||||||
|
const shuffled = all.sort(() => Math.random() - 0.5).slice(0, 5);
|
||||||
|
for (const w of shuffled) {
|
||||||
|
w.is_daily = true;
|
||||||
|
w.daily_date = today;
|
||||||
|
}
|
||||||
|
return this.repo.save(shuffled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getQuizWords(count = 10): Promise<Word[]> {
|
||||||
|
const all = await this.repo.find();
|
||||||
|
return all.sort(() => Math.random() - 0.5).slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategories() {
|
||||||
|
return this.repo
|
||||||
|
.createQueryBuilder('w')
|
||||||
|
.select('DISTINCT w.category', 'category')
|
||||||
|
.getRawMany();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user