diff --git a/src/app.module.ts b/src/app.module.ts index 986ac43..c9505ec 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { EmailOptions } from "./common/config/email-config"; import { AccessTokenGuard } from "./common/guards/bearer-token.guard"; import { TypeOrmModuleOptions } from "./common/typeorm"; +import { ArticlesModule } from "./modules/articles.module"; import { AuthModule } from "./modules/auth.module"; import { MembersModule } from "./modules/members.module"; @@ -20,6 +21,7 @@ import { MembersModule } from "./modules/members.module"; MembersModule, AuthModule, MailerModule.forRootAsync(EmailOptions), + ArticlesModule, ], controllers: [], providers: [ diff --git a/src/controllers/articles.controller.ts b/src/controllers/articles.controller.ts new file mode 100644 index 0000000..246e7f4 --- /dev/null +++ b/src/controllers/articles.controller.ts @@ -0,0 +1,18 @@ +import { Body, Controller, Post } from "@nestjs/common"; + +import { CurrentMemberDecorator } from "@APP/common/decorators/current-member.decorator"; +import { CreateArticleDto } from "@APP/dtos/create-article.dto"; +import { ArticlesService } from "@APP/services/articles.service"; + +@Controller("articles") +export class ArticlesController { + constructor(private readonly articlesService: ArticlesService) {} + + @Post() + async postArticle( + @Body() body: CreateArticleDto, + @CurrentMemberDecorator("id") currentMemberId: number, + ) { + return this.articlesService.createArticle(currentMemberId, body); + } +} diff --git a/src/dtos/create-article.dto.ts b/src/dtos/create-article.dto.ts new file mode 100644 index 0000000..6e462d3 --- /dev/null +++ b/src/dtos/create-article.dto.ts @@ -0,0 +1,13 @@ +import { PickType } from "@nestjs/swagger"; + +import { ArticleEntity } from "@APP/entities/article.entity"; + +export class CreateArticleDto extends PickType(ArticleEntity, [ + "title", + "content", + "startTime", + "endTime", + "categoryId", + "districtId", + "regionId", +]) {} diff --git a/src/entities/article.entity.ts b/src/entities/article.entity.ts index f676694..f99571d 100644 --- a/src/entities/article.entity.ts +++ b/src/entities/article.entity.ts @@ -1,3 +1,5 @@ +import { Type } from "class-transformer"; +import { IsDate, IsNotEmpty, IsNumber, IsString } from "class-validator"; import { Column, CreateDateColumn, @@ -20,18 +22,30 @@ export class ArticleEntity { @PrimaryGeneratedColumn({ type: "int" }) id!: number; + @IsNotEmpty() + @IsString() @Column({ type: "varchar", length: 100 }) title!: string; + @IsNotEmpty() + @IsString() @Column({ type: "text" }) content!: string; + @IsNotEmpty() + @Type(() => Date) + @IsDate() @Column({ type: "datetime" }) startTime!: Date; + @IsNotEmpty() + @Type(() => Date) + @IsDate() @Column({ type: "datetime" }) endTime!: Date; + @IsNotEmpty() + @IsNumber() @Column({ type: "int" }) memberId!: number; @@ -39,6 +53,8 @@ export class ArticleEntity { @JoinColumn({ name: "memberId", referencedColumnName: "id" }) member!: MemberEntity; + @IsNotEmpty() + @IsNumber() @Column({ type: "int" }) categoryId!: number; @@ -46,6 +62,8 @@ export class ArticleEntity { @JoinColumn({ name: "categoryId", referencedColumnName: "id" }) category!: CategoryEntity; + @IsNotEmpty() + @IsNumber() @Column({ type: "int" }) regionId!: number; @@ -53,6 +71,8 @@ export class ArticleEntity { @JoinColumn({ name: "regionId", referencedColumnName: "id" }) region!: RegionEntity; + @IsNotEmpty() + @IsNumber() @Column({ type: "int" }) districtId!: number; diff --git a/src/modules/articles.module.ts b/src/modules/articles.module.ts new file mode 100644 index 0000000..1cb3abf --- /dev/null +++ b/src/modules/articles.module.ts @@ -0,0 +1,36 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { ArticlesController } from "@APP/controllers/articles.controller"; +import { ArticleLikeEntity } from "@APP/entities/article-like.entity"; +import { ArticleEntity } from "@APP/entities/article.entity"; +import { CategoryEntity } from "@APP/entities/category.entity"; +import { DistrictEntity } from "@APP/entities/district.entity"; +import { RegionEntity } from "@APP/entities/region.entity"; +import { ArticlesRepository } from "@APP/repositories/articles.repository"; +import { CategoriesRepository } from "@APP/repositories/categories.repository"; +import { DistrictsRepository } from "@APP/repositories/districts.repository"; +import { RegionsRepository } from "@APP/repositories/regions.repository"; +import { ArticlesService } from "@APP/services/articles.service"; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + ArticleEntity, + ArticleLikeEntity, + CategoryEntity, + DistrictEntity, + RegionEntity, + ]), + ], + controllers: [ArticlesController], + providers: [ + ArticlesService, + ArticlesRepository, + CategoriesRepository, + DistrictsRepository, + RegionsRepository, + ], + exports: [], +}) +export class ArticlesModule {} diff --git a/src/services/articles.service.ts b/src/services/articles.service.ts new file mode 100644 index 0000000..083cfd3 --- /dev/null +++ b/src/services/articles.service.ts @@ -0,0 +1,59 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; + +import { CreateArticleDto } from "@APP/dtos/create-article.dto"; +import { ArticlesRepository } from "@APP/repositories/articles.repository"; +import { CategoriesRepository } from "@APP/repositories/categories.repository"; +import { DistrictsRepository } from "@APP/repositories/districts.repository"; +import { RegionsRepository } from "@APP/repositories/regions.repository"; + +@Injectable() +export class ArticlesService { + constructor( + private readonly articlesRepository: ArticlesRepository, + private readonly categoriesRepository: CategoriesRepository, + private readonly districtsRepository: DistrictsRepository, + private readonly regionsRepository: RegionsRepository, + ) {} + + async createArticle(currentMemberId: number, body: CreateArticleDto) { + const [category, district, region] = await Promise.all([ + this.categoriesRepository.exists({ + where: { + id: body.categoryId, + }, + }), + this.districtsRepository.exists({ + where: { + id: body.districtId, + }, + }), + this.regionsRepository.exists({ + where: { + id: body.regionId, + }, + }), + ]); + + if (!category) { + throw new NotFoundException("카테고리를 찾을 수 없습니다."); + } + + if (!district || !region) { + throw new NotFoundException("지역을 찾을 수 없습니다."); + } + + return this.articlesRepository.save( + this.createArticleEntity(currentMemberId, body), + ); + } + + private createArticleEntity( + currentMemberId: number, + body: CreateArticleDto, + ) { + return this.articlesRepository.create({ + memberId: currentMemberId, + ...body, + }); + } +}