diff --git a/BE/package-lock.json b/BE/package-lock.json index 6688b8e..f59ca3c 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -16,6 +16,8 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.15", "@nestjs/typeorm": "^10.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "multer-s3": "^3.0.1", "mysql2": "^3.6.3", "nest-winston": "^1.9.4", @@ -3656,6 +3658,11 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, + "node_modules/@types/validator": { + "version": "13.11.7", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.7.tgz", + "integrity": "sha512-q0JomTsJ2I5Mv7dhHhQLGjMvX0JJm5dyZ1DXQySIUzU1UlwzB8bt+R6+LODUbz0UDIOvEzGc28tk27gBJw2N8Q==" + }, "node_modules/@types/yargs": { "version": "17.0.31", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", @@ -4757,6 +4764,21 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "dependencies": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -7929,6 +7951,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.49", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz", + "integrity": "sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10777,6 +10804,14 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/BE/package.json b/BE/package.json index d75da50..effb79c 100644 --- a/BE/package.json +++ b/BE/package.json @@ -27,6 +27,8 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.15", "@nestjs/typeorm": "^10.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "multer-s3": "^3.0.1", "mysql2": "^3.6.3", "nest-winston": "^1.9.4", diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index b94320b..45c36d2 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -1,4 +1,4 @@ -import { Module, Logger } from '@nestjs/common'; +import { Module, Logger, ValidationPipe } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WinstonModule } from 'nest-winston'; @@ -9,6 +9,7 @@ import { AppService } from './app.service'; import { winstonOptions, dailyOption } from './config/winston.config'; import { MysqlConfigProvider } from './config/mysql.config'; import { PostModule } from './post/post.module'; +import { APP_PIPE } from '@nestjs/core'; @Module({ imports: [ @@ -25,6 +26,13 @@ import { PostModule } from './post/post.module'; PostModule, ], controllers: [AppController], - providers: [AppService, Logger], + providers: [ + AppService, + Logger, + { + provide: APP_PIPE, + useClass: ValidationPipe, + }, + ], }) export class AppModule {} diff --git a/BE/src/entities/post.entity.ts b/BE/src/entities/post.entity.ts index 64ff151..2995c2c 100644 --- a/BE/src/entities/post.entity.ts +++ b/BE/src/entities/post.entity.ts @@ -34,7 +34,7 @@ export class PostEntity { user: UserEntity; @Column({ type: 'tinyint', nullable: false }) - status: number; + status: boolean; @Column({ type: 'datetime', nullable: false }) start_date: Date; @@ -43,7 +43,7 @@ export class PostEntity { end_date: Date; @Column({ type: 'tinyint', nullable: false }) - is_request: number; + is_request: boolean; @CreateDateColumn({ type: 'timestamp', diff --git a/BE/src/main.ts b/BE/src/main.ts index 0515f92..45ab391 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -4,6 +4,7 @@ import { setupSwagger } from './config/swagger.config'; import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston'; import { dailyOption, winstonOptions } from './config/winston.config'; import * as winstonDaily from 'winston-daily-rotate-file'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -16,6 +17,7 @@ async function bootstrap() { }), }); app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); + app.useGlobalPipes(new ValidationPipe()); setupSwagger(app); await app.listen(3000); } diff --git a/BE/src/post/createPost.dto.ts b/BE/src/post/createPost.dto.ts new file mode 100644 index 0000000..d8c06dd --- /dev/null +++ b/BE/src/post/createPost.dto.ts @@ -0,0 +1,22 @@ +import { IsBoolean, IsNumber, IsString, ValidateIf } from 'class-validator'; + +export class CreatePostDto { + @IsString() + title: string; + + @IsString() + contents: string; + + @IsNumber() + @ValidateIf((object) => object.is_request === false) + price: number; + + @IsBoolean() + is_request: boolean; + + @IsString() + start_date: string; + + @IsString() + end_date: string; +} diff --git a/BE/src/post/post.controller.ts b/BE/src/post/post.controller.ts index 1c1476b..9dfd506 100644 --- a/BE/src/post/post.controller.ts +++ b/BE/src/post/post.controller.ts @@ -1,6 +1,18 @@ -import { Controller, Get, HttpException, Param } from '@nestjs/common'; +import { + Controller, + Get, + HttpException, + Param, + Post, + UploadedFiles, + UseInterceptors, + ValidationPipe, +} from '@nestjs/common'; import { PostService } from './post.service'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import { CreatePostDto } from './createPost.dto'; +import { MultiPartBody } from '../utils/multiPartBody.decorator'; @Controller('posts') @ApiTags('posts') @@ -13,6 +25,24 @@ export class PostController { return posts; } + @Post() + @UseInterceptors(FilesInterceptor('image', 12)) + async postsCreate( + @UploadedFiles() files: Array, + @MultiPartBody( + 'profile_info', + new ValidationPipe({ validateCustomDecorators: true }), + ) + createPostDto: CreatePostDto, + ) { + const userId: string = 'qwe'; + let imageLocation: Array = []; + if (createPostDto.is_request === false && files !== undefined) { + imageLocation = await this.postService.uploadImages(files); + } + await this.postService.createPost(imageLocation, createPostDto, userId); + } + @Get('/:id') @ApiOperation({ summary: 'search for post', description: '게시글 상세 조회' }) async postDetails(@Param('id') id: number) { diff --git a/BE/src/post/post.module.ts b/BE/src/post/post.module.ts index ab5094e..0ca8682 100644 --- a/BE/src/post/post.module.ts +++ b/BE/src/post/post.module.ts @@ -3,10 +3,15 @@ import { PostController } from './post.controller'; import { PostService } from './post.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { PostEntity } from '../entities/post.entity'; +import { S3Handler } from '../utils/S3Handler'; +import { UserEntity } from '../entities/user.entity'; +import { PostImageEntity } from '../entities/postImage.entity'; @Module({ - imports: [TypeOrmModule.forFeature([PostEntity])], + imports: [ + TypeOrmModule.forFeature([PostEntity, UserEntity, PostImageEntity]), + ], controllers: [PostController], - providers: [PostService], + providers: [PostService, S3Handler], }) export class PostModule {} diff --git a/BE/src/post/post.service.ts b/BE/src/post/post.service.ts index 7dea156..cbbedc9 100644 --- a/BE/src/post/post.service.ts +++ b/BE/src/post/post.service.ts @@ -2,12 +2,20 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { PostEntity } from '../entities/post.entity'; import { Repository } from 'typeorm'; +import { S3Handler } from '../utils/S3Handler'; +import { UserEntity } from '../entities/user.entity'; +import { PostImageEntity } from '../entities/postImage.entity'; @Injectable() export class PostService { constructor( @InjectRepository(PostEntity) private postRepository: Repository, + @InjectRepository(UserEntity) + private userRepository: Repository, + @InjectRepository(PostImageEntity) + private postImageRepository: Repository, + private s3Handler: S3Handler, ) {} async getPosts() { const res = await this.postRepository.find(); @@ -50,4 +58,41 @@ export class PostService { return null; } } + + async uploadImages(files: Express.Multer.File[]): Promise { + const fileLocation: Array = []; + for (const file of files) { + fileLocation.push(await this.s3Handler.uploadFile(file)); + } + return fileLocation; + } + + async createPost(imageLocations, createPostDto, userHash) { + const post = new PostEntity(); + const user = await this.userRepository.findOne({ + where: { user_hash: userHash }, + }); + post.title = createPostDto.title; + post.contents = createPostDto.contents; + post.price = createPostDto.price; + post.is_request = createPostDto.is_request; + post.start_date = createPostDto.start_date; + post.end_date = createPostDto.end_date; + post.status = true; + post.user_id = user.id; + // 이미지 추가 + const res = await this.postRepository.save(post); + if (res.is_request === false) { + await this.createImages(imageLocations, res.id); + } + } + + async createImages(imageLocations: Array, postId: number) { + for (const imageLocation of imageLocations) { + const postImageEntity = new PostImageEntity(); + postImageEntity.image_url = imageLocation; + postImageEntity.post_id = postId; + await this.postImageRepository.save(postImageEntity); + } + } } diff --git a/BE/src/utils/multiPartBody.decorator.ts b/BE/src/utils/multiPartBody.decorator.ts new file mode 100644 index 0000000..d99391e --- /dev/null +++ b/BE/src/utils/multiPartBody.decorator.ts @@ -0,0 +1,10 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const MultiPartBody = createParamDecorator( + (data: string, ctx: ExecutionContext) => { + const request: Request = ctx.switchToHttp().getRequest(); + const body = request.body; + + return data ? JSON.parse(body?.[data]) : body; + }, +);