diff --git a/package-lock.json b/package-lock.json index 823384a..9997a80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nestjs/core": "^10.4.8", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.4.8", + "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^8.0.7", "@nestjs/typeorm": "^10.0.2", "bcrypt": "^5.1.1", @@ -2533,6 +2534,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@nestjs/serve-static": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/serve-static/-/serve-static-4.0.2.tgz", + "integrity": "sha512-cT0vdWN5ar7jDI2NKbhf4LcwJzU4vS5sVpMkVrHuyLcltbrz6JdGi1TfIMMatP2pNiq5Ie/uUdPSFDVaZX/URQ==", + "license": "MIT", + "dependencies": { + "path-to-regexp": "0.2.5" + }, + "peerDependencies": { + "@fastify/static": "^6.5.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "express": "^4.18.1", + "fastify": "^4.7.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "express": { + "optional": true + }, + "fastify": { + "optional": true + } + } + }, + "node_modules/@nestjs/serve-static/node_modules/path-to-regexp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz", + "integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q==", + "license": "MIT" + }, "node_modules/@nestjs/swagger": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.0.7.tgz", diff --git a/package.json b/package.json index 5cbd7fc..607b9ba 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@nestjs/core": "^10.4.8", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.4.8", + "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^8.0.7", "@nestjs/typeorm": "^10.0.2", "bcrypt": "^5.1.1", diff --git a/src/app.module.ts b/src/app.module.ts index 4cdc9d4..c80c956 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,7 +2,9 @@ import { MailerModule } from "@nestjs-modules/mailer"; import { ClassSerializerInterceptor, Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core"; +import { ServeStaticModule } from "@nestjs/serve-static"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { join } from "path"; import { EmailOptions } from "./common/config/email-config"; import { AccessTokenGuard } from "./common/guards/bearer-token.guard"; @@ -14,6 +16,10 @@ import { ParticipationsModule } from "./modules/participations.module"; @Module({ imports: [ + ServeStaticModule.forRoot({ + rootPath: join(__dirname, "..", "uploads"), + serveRoot: "/public", + }), ConfigModule.forRoot({ envFilePath: [`${__dirname}/../.${process.env["NODE_ENV"]}.env`], isGlobal: true, diff --git a/src/controllers/articles.controller.ts b/src/controllers/articles.controller.ts index 33571df..32cdbb3 100644 --- a/src/controllers/articles.controller.ts +++ b/src/controllers/articles.controller.ts @@ -8,7 +8,10 @@ import { Patch, Post, Put, + UploadedFile, + UseInterceptors, } from "@nestjs/common"; +import { FileInterceptor } from "@nestjs/platform-express"; import { CurrentMemberDecorator } from "@APP/common/decorators/current-member.decorator"; import { CreateArticleDto } from "@APP/dtos/create-article.dto"; @@ -34,6 +37,14 @@ export class ArticlesController { return this.articlesService.createArticle(currentMemberId, body); } + @Patch("upload-image") + @UseInterceptors(FileInterceptor("image")) + async patchUploadImage(@UploadedFile() file: Express.Multer.File) { + return { + filename: file.filename, + }; + } + @Patch(":articleId") async patchArticle( @Param("articleId", new ParseIntPipe()) articleId: number, diff --git a/src/entities/article.entity.ts b/src/entities/article.entity.ts index be4e951..1781b6f 100644 --- a/src/entities/article.entity.ts +++ b/src/entities/article.entity.ts @@ -1,4 +1,4 @@ -import { Type } from "class-transformer"; +import { Transform, Type } from "class-transformer"; import { IsDate, IsNotEmpty, IsNumber, IsString } from "class-validator"; import { Column, @@ -45,6 +45,17 @@ export class ArticleEntity { @Column({ type: "datetime" }) endTime!: Date; + @Transform(({ value }) => { + if (!value) return null; + + return new URL( + `/public/articles/${value}`, + process.env["API_BASE_URL"], + ).toString(); + }) + @Column({ type: "varchar", length: 2048, nullable: true }) + articleImage?: string; + @IsNotEmpty() @IsNumber() @Column({ type: "int" }) diff --git a/src/entities/member.entity.ts b/src/entities/member.entity.ts index 06e4285..8fbd930 100644 --- a/src/entities/member.entity.ts +++ b/src/entities/member.entity.ts @@ -1,4 +1,4 @@ -import { Exclude } from "class-transformer"; +import { Exclude, Transform } from "class-transformer"; import { IsEmail, IsNotEmpty, IsString, Length } from "class-validator"; import { Column, @@ -43,6 +43,14 @@ export class MemberEntity { @Column({ type: "varchar", length: 100, nullable: false, unique: true }) email!: string; + @Transform(({ value }) => { + if (!value) return null; + + return new URL( + `/public/members/${value}`, + process.env["API_BASE_URL"], + ).toString(); + }) @Column({ type: "varchar", length: 2048, nullable: true }) profileImage?: string; diff --git a/src/modules/articles.module.ts b/src/modules/articles.module.ts index e4afd5b..ca7e48e 100644 --- a/src/modules/articles.module.ts +++ b/src/modules/articles.module.ts @@ -1,5 +1,8 @@ -import { Module } from "@nestjs/common"; +import { BadRequestException, Module } from "@nestjs/common"; +import { MulterModule } from "@nestjs/platform-express"; import { TypeOrmModule } from "@nestjs/typeorm"; +import multer from "multer"; +import { extname } from "path"; import { ArticlesController } from "@APP/controllers/articles.controller"; import { ArticleLikeEntity } from "@APP/entities/article-like.entity"; @@ -24,6 +27,33 @@ import { ArticlesService } from "@APP/services/articles.service"; RegionEntity, ArticleLikeEntity, ]), + MulterModule.register({ + limits: { + fileSize: 10000000, + }, + fileFilter: (_req, file, cb) => { + const ext = extname(file.originalname); + if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png") { + return cb( + new BadRequestException( + "jpg/jpeg/png 파일만 업로드 가능합니다", + ), + false, + ); + } + + return cb(null, true); + }, + + storage: multer.diskStorage({ + destination: function (_req, _file, cb) { + cb(null, "uploads/articles/"); + }, + filename: function (_req, file, cb) { + cb(null, `${Date.now()}-${file.originalname}`); + }, + }), + }), ], controllers: [ArticlesController], providers: [ diff --git a/src/modules/members.module.ts b/src/modules/members.module.ts index 8b96db2..528f596 100644 --- a/src/modules/members.module.ts +++ b/src/modules/members.module.ts @@ -34,7 +34,7 @@ import { MembersService } from "@APP/services/members.service"; storage: multer.diskStorage({ destination: function (_req, _file, cb) { - cb(null, "uploads/"); + cb(null, "uploads/members/"); }, filename: function (_req, file, cb) { cb(null, `${Date.now()}-${file.originalname}`);