Skip to content

Commit

Permalink
feat: 멀티 이미지 등록, 스키마 수정, swagger 추가, 저장장소 Docker volume으로 변경
Browse files Browse the repository at this point in the history
  • Loading branch information
zerosial committed Jan 23, 2024
1 parent 92de959 commit 4ec0b89
Show file tree
Hide file tree
Showing 13 changed files with 2,059 additions and 993 deletions.
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ services:
- postgres
env_file:
- .env
volumes:
- nest-uploads:/app/uploads # 여기에 볼륨을 추가
networks:
- pinemarket_api

Expand All @@ -30,6 +32,8 @@ services:
volumes:
postgres:
name: pinemarket-db
nest-uploads: # 새 볼륨 정의
name: pinemarket-nest-uploads

networks:
pinemarket_api:
Expand Down
3 changes: 2 additions & 1 deletion prisma/dbml/schema.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ Table Post {
published Boolean [not null]
title String [not null]
content String [not null]
imgUrl String
imgUrls String[] [not null]
location String
price String [not null]
author User
authorId String
}
Expand Down
9 changes: 9 additions & 0 deletions prisma/migrations/20240122084938_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `imgUrl` on the `Post` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Post" DROP COLUMN "imgUrl",
ADD COLUMN "imgUrls" TEXT[];
8 changes: 8 additions & 0 deletions prisma/migrations/20240122093608_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `price` to the `Post` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Post" ADD COLUMN "price" TEXT NOT NULL;
3 changes: 2 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ model Post {
published Boolean
title String
content String
imgUrl String?
imgUrls String[] // 이미지 URL 배열
location String?
price String
author User? @relation(fields: [authorId], references: [id])
authorId String?
}
Expand Down
2,936 changes: 1,970 additions & 966 deletions prisma/seed.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
import config from './common/configs/config';
import { MulterModule } from '@nestjs/platform-express';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [config] }),
Expand All @@ -24,6 +25,11 @@ import config from './common/configs/config';
},
}),

// Docker 외부 이미지 저장소 volume
MulterModule.register({
dest: '/app/uploads',
}),

// vercel 배포를 위한 graphql 비활성화
/* GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
Expand Down
13 changes: 5 additions & 8 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import { AuthService } from './auth.service';
import { LoginInput } from './dto/login.input';
import { SignupInput } from './dto/signup.input';
import { AuthGuard } from '@nestjs/passport';

Check warning on line 15 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / build (16)

'AuthGuard' is defined but never used

Check warning on line 15 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / build (18)

'AuthGuard' is defined but never used
import { ApiBearerAuth } from '@nestjs/swagger';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';

Check warning on line 16 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / build (16)

'ApiBearerAuth' is defined but never used

Check warning on line 16 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / build (18)

'ApiBearerAuth' is defined but never used
import { Request, Response } from 'express';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('signup')
@ApiTags('Auth API')
async signup(@Body() signupData: SignupInput, @Res() res: Response) {
signupData.email = signupData.email.toLowerCase();
const { accessToken, refreshToken } = await this.authService.createUser(
Expand All @@ -39,6 +40,7 @@ export class AuthController {
}

@Post('login')
@ApiTags('Auth API')
async login(@Body() loginData: LoginInput, @Res() res: Response) {
const { accessToken, refreshToken } = await this.authService.login(
loginData.email.toLowerCase(),
Expand All @@ -57,6 +59,7 @@ export class AuthController {
}

@Post('refresh-token')
@ApiTags('Auth API')
async refreshToken(@Req() req: Request, @Res() res: Response) {
console.log('req.cookie', req.cookies);
const refreshToken = req.cookies['refreshToken'];
Expand All @@ -79,6 +82,7 @@ export class AuthController {
}

@Post('logout')
@ApiTags('Auth API')
async logout(@Res() res: Response) {
res.cookie('refreshToken', '', {
httpOnly: true,
Expand All @@ -89,11 +93,4 @@ export class AuthController {
});
return res.sendStatus(200);
}

@ApiBearerAuth('access-token')
@UseGuards(AuthGuard('jwt'))
@Get('profile')
async getUser(@Req() req) {
return this.authService.validateUser(req.user.id);
}
}
2 changes: 1 addition & 1 deletion src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export default async () => {
["./users/dto/response-user-dto"]: await import("./users/dto/response-user-dto"),
["./users/models/user.model"]: await import("./users/models/user.model")
};
return { "@nestjs/swagger/plugin": { "models": [[import("./posts/dto/pagenation-query.dto"), { "PaginationQueryDto": { page: { required: false, type: () => Number, default: 1, minimum: 1 }, limit: { required: false, type: () => Number, default: 10, minimum: 1 }, query: { required: false, type: () => String }, orderBy: { required: false, type: () => String }, direction: { required: false, type: () => Object, default: "asc" } } }]], "controllers": [[import("./app.controller"), { "AppController": { "getHello": { type: String }, "getHelloName": { type: String } } }], [import("./auth/auth.controller"), { "AuthController": { "signup": {}, "login": {}, "refreshToken": {}, "getUser": { type: Object } } }], [import("./users/users.controller"), { "UserController": { "getProfile": { type: t["./users/dto/response-user-dto"].UserResponseDto }, "updateUser": { type: Object }, "changePassword": { type: Object } } }], [import("./posts/posts.controller"), { "PostsController": { "createPost": { type: Object }, "getPublishedPosts": {}, "getUserPosts": { type: [Object] }, "getPost": { type: Object } } }]] }, "@nestjs/graphql/plugin": { "models": [[import("./auth/dto/signup.input"), { "SignupInput": { email: {}, password: {}, username: { nullable: true } } }], [import("./auth/models/token.model"), { "Token": { accessToken: {}, refreshToken: {} } }], [import("./common/models/base.model"), { "BaseModel": { id: {}, createdAt: {}, updatedAt: {} } }], [import("./posts/models/post.model"), { "Post": { title: {}, content: { nullable: true }, published: {}, imgUrl: { nullable: true }, author: { nullable: true } } }], [import("./users/models/user.model"), { "User": { email: {}, username: { nullable: true }, role: {}, posts: { nullable: true } } }], [import("./auth/models/auth.model"), { "Auth": { user: { type: () => t["./users/models/user.model"].User } } }], [import("./auth/dto/login.input"), { "LoginInput": { email: {}, password: {} } }], [import("./auth/dto/refresh-token.input"), { "RefreshTokenInput": { token: {} } }], [import("./users/dto/change-password.input"), { "ChangePasswordInput": { oldPassword: {}, newPassword: {} } }], [import("./users/dto/update-user.input"), { "UpdateUserInput": { username: { nullable: true } } }], [import("./common/pagination/pagination.args"), { "PaginationArgs": { skip: { nullable: true, type: () => Number }, after: { nullable: true, type: () => String }, before: { nullable: true, type: () => String }, first: { nullable: true, type: () => Number }, last: { nullable: true, type: () => Number } } }], [import("./posts/args/post-id.args"), { "PostIdArgs": { postId: { type: () => String } } }], [import("./posts/args/user-id.args"), { "UserIdArgs": { userId: { type: () => String } } }], [import("./common/pagination/page-info.model"), { "PageInfo": { endCursor: { nullable: true }, hasNextPage: {}, hasPreviousPage: {}, startCursor: { nullable: true } } }], [import("./posts/models/post-connection.model"), { "PostConnection": {} }], [import("./posts/dto/post-order.input"), { "PostOrder": { field: {} } }], [import("./posts/dto/createPost.input"), { "CreatePostInput": { content: {}, title: {} } }]] } };
return { "@nestjs/swagger/plugin": { "models": [[import("./posts/dto/pagenation-query.dto"), { "PaginationQueryDto": { page: { required: false, type: () => Number, default: 1, minimum: 1 }, limit: { required: false, type: () => Number, default: 10, minimum: 1 }, query: { required: false, type: () => String }, orderBy: { required: false, type: () => String }, direction: { required: false, type: () => Object, default: "asc" } } }]], "controllers": [[import("./app.controller"), { "AppController": { "getHello": { type: String }, "getHelloName": { type: String } } }], [import("./auth/auth.controller"), { "AuthController": { "signup": {}, "login": {}, "refreshToken": {}, "logout": {}, "getUser": { type: Object } } }], [import("./users/users.controller"), { "UserController": { "getProfile": { type: t["./users/dto/response-user-dto"].UserResponseDto }, "updateUser": { type: Object }, "changePassword": { type: Object } } }], [import("./posts/posts.controller"), { "PostsController": { "createPost": { type: Object }, "getPublishedPosts": {}, "getUserPosts": { type: [Object] }, "getPost": { type: Object } } }]] }, "@nestjs/graphql/plugin": { "models": [[import("./auth/dto/signup.input"), { "SignupInput": { email: {}, password: {}, username: { nullable: true } } }], [import("./auth/models/token.model"), { "Token": { accessToken: {}, refreshToken: {} } }], [import("./common/models/base.model"), { "BaseModel": { id: {}, createdAt: {}, updatedAt: {} } }], [import("./posts/models/post.model"), { "Post": { title: {}, content: { nullable: true }, published: {}, imgUrl: { nullable: true }, author: { nullable: true } } }], [import("./users/models/user.model"), { "User": { email: {}, username: { nullable: true }, role: {}, posts: { nullable: true } } }], [import("./auth/models/auth.model"), { "Auth": { user: { type: () => t["./users/models/user.model"].User } } }], [import("./auth/dto/login.input"), { "LoginInput": { email: {}, password: {} } }], [import("./auth/dto/refresh-token.input"), { "RefreshTokenInput": { token: {} } }], [import("./users/dto/change-password.input"), { "ChangePasswordInput": { oldPassword: {}, newPassword: {} } }], [import("./users/dto/update-user.input"), { "UpdateUserInput": { username: { nullable: true } } }], [import("./common/pagination/pagination.args"), { "PaginationArgs": { skip: { nullable: true, type: () => Number }, after: { nullable: true, type: () => String }, before: { nullable: true, type: () => String }, first: { nullable: true, type: () => Number }, last: { nullable: true, type: () => Number } } }], [import("./posts/args/post-id.args"), { "PostIdArgs": { postId: { type: () => String } } }], [import("./posts/args/user-id.args"), { "UserIdArgs": { userId: { type: () => String } } }], [import("./common/pagination/page-info.model"), { "PageInfo": { endCursor: { nullable: true }, hasNextPage: {}, hasPreviousPage: {}, startCursor: { nullable: true } } }], [import("./posts/models/post-connection.model"), { "PostConnection": {} }], [import("./posts/dto/post-order.input"), { "PostOrder": { field: {} } }], [import("./posts/dto/createPost.input"), { "CreatePostInput": { content: {}, title: {} } }]] } };
};
5 changes: 5 additions & 0 deletions src/posts/dto/createPost.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ export class CreatePostInput {
@ApiProperty({ description: 'The title of the post' }) // Swagger 데코레이터
@IsNotEmpty()
title: string;

@Field() // GraphQL 데코레이터
@ApiProperty({ description: '가격' }) // Swagger 데코레이터
@IsNotEmpty()
price: string;
}
57 changes: 42 additions & 15 deletions src/posts/posts.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FileInterceptor } from '@nestjs/platform-express';
import { FilesInterceptor } from '@nestjs/platform-express';
import {
Controller,
Get,
Expand All @@ -8,12 +8,18 @@ import {
Query,
Req,
UseInterceptors,
UploadedFile,
UploadedFiles,
} from '@nestjs/common';
import { PrismaService } from 'nestjs-prisma';
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiBody, ApiConsumes } from '@nestjs/swagger';
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiParam,
ApiTags,
} from '@nestjs/swagger';
import { PaginationQueryDto } from './dto/pagenation-query.dto';
import { diskStorage } from 'multer';
import { extname } from 'path';
Expand All @@ -24,11 +30,13 @@ export const multerConfig = {
storage: diskStorage({
destination: './uploads', // 저장 위치
filename: (req, file, cb) => {
const date = new Date();
const formattedDate = date.toISOString().split('T')[0]; // YYYY-MM-DD 형식
const randomName = Array(32)
.fill(null)
.map(() => Math.round(Math.random() * 16).toString(16))
.join('');
cb(null, `${randomName}${extname(file.originalname)}`); // 파일명
cb(null, `${formattedDate}-${randomName}${extname(file.originalname)}`); // 파일명
},
}),
};
Expand All @@ -43,44 +51,53 @@ export class PostsController {
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard('jwt'))
@Post()
@UseInterceptors(FileInterceptor('file', multerConfig))
@UseInterceptors(FilesInterceptor('files', 5, multerConfig))
@ApiTags('POST API')
@ApiConsumes('multipart/form-data')
@ApiBody({
description: 'Create a post with an image',
description: 'Create a post with multiple images',
schema: {
type: 'object',
properties: {
title: { type: 'string' },
content: { type: 'string' },
file: {
type: 'string',
format: 'binary',
location: { type: 'string' },
price: { type: 'string' },
files: {
type: 'array',
items: {
type: 'string',
format: 'binary',
},
},
},
required: ['title', 'file'],
required: ['title', 'files'],
},
})
async createPost(
@Req() req: CustomRequest,
@UploadedFile() file: Express.Multer.File,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
// title과 content는 req.body에서 추출
const { title, content } = req.body;
const { title, content, location, price } = req.body;
const hostUrl = this.configService.get('MINIPC_DOMAIN');

const imgUrl = file ? `${hostUrl}/uploads/${file.filename}` : null;
const imgUrls = files.map((file) => `${hostUrl}/uploads/${file.filename}`);
return this.prisma.post.create({
data: {
published: true,
title: title,
content: content,
imgUrl: imgUrl,
location: location,
price: price,
imgUrls: imgUrls,
authorId: req.user.id,
},
});
}

@Get('published')
@Get('list')
@ApiTags('POST API')
async getPublishedPosts(@Query() paginationQuery: PaginationQueryDto) {
const page = paginationQuery.page;
const limit = paginationQuery.limit;
Expand Down Expand Up @@ -109,13 +126,23 @@ export class PostsController {
}

@Get(':userId/posts')
@ApiTags('POST API')
@ApiParam({
name: 'userId',
description: '회원 id 입니다 (list에서는 authorId로 표시됩니다.)',
})
async getUserPosts(@Param('userId') userId: string) {
return this.prisma.user
.findUnique({ where: { id: userId } })
.posts({ where: { published: true } });
}

@Get(':id')
@ApiTags('POST API')
@ApiParam({
name: 'id',
description: '게시글의 고유 id로 해당 게시글을 조회할 수 있습니다.',
})
async getPost(@Param('id') id: string) {
// 게시글과 함께 저자 정보를 가져옵니다
return this.prisma.post.findUnique({
Expand Down
1 change: 1 addition & 0 deletions src/posts/posts.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class PostsResolver {
published: true,
title: data.title,
content: data.content,
price: data.price,
authorId: user.id,
},
});
Expand Down
5 changes: 4 additions & 1 deletion src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UsersService } from './users.service';
import { UpdateUserInput } from './dto/update-user.input';
import { ChangePasswordInput } from './dto/change-password.input';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { UserResponseDto } from './dto/response-user-dto';

@Controller('users')
Expand All @@ -13,20 +13,23 @@ export class UserController {
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard('jwt'))
@Get('me')
@ApiTags('USER API')
async getProfile(@Req() req): Promise<UserResponseDto> {
return this.usersService.getProfile(req.user);
}

@ApiBearerAuth('access-token')
@UseGuards(AuthGuard('jwt'))
@Put('update')
@ApiTags('USER API')
async updateUser(@Req() req, @Body() newUserData: UpdateUserInput) {
return this.usersService.updateUser(req.user.id, newUserData);
}

@ApiBearerAuth('access-token')
@UseGuards(AuthGuard('jwt'))
@Put('change-password')
@ApiTags('USER API')
async changePassword(
@Req() req,
@Body() changePasswordData: ChangePasswordInput,
Expand Down

0 comments on commit 4ec0b89

Please sign in to comment.