diff --git a/database_setup/database.dbml b/database_setup/database.dbml index 38710fb..5b54f98 100644 --- a/database_setup/database.dbml +++ b/database_setup/database.dbml @@ -6,63 +6,69 @@ Project find_it { Enum item_category { ELECTRONICS [note: '전자기기'] WALLET [note: '지갑'] - BAG [note: '가방'] - CLOTHING [note: '의류'] - ACCESSORIES [note: '액세서리'] - DOCUMENT [note: '서류'] - CARD [note: '카드'] - OTHER [note: '기타'] + BAG [note: '가방'] + CLOTHING [note: '의류'] + ACCESSORIES [note: '액세서리'] + DOCUMENT [note: '서류'] + CARD [note: '카드'] + ETC [note: '기타'] } Enum post_status { - PENDING [note: '미해결'] + IN_PROGRESS [note: '진행 중'] RESOLVED [note: '해결됨'] + CANCELLED [note: '취소됨'] +} + +Enum post_type { + FOUND [note: '발견'] + LOST [note: '분실'] +} + +Enum comment_type { + COMMENT [note: '일반 댓글'] + REPLY [note: '대댓글'] } Table users { uuid uuid [pk, not null] name varchar [not null] - email varchar [not null, unique] created_at timestamp [not null, default: `now()`] updated_at timestamp [not null] Note: '사용자 정보' } -Table found_posts { - id uuid [pk, not null, default: `gen_random_uuid()`] - title varchar [not null] - description text [not null] +Table posts { + id serial [pk, not null] + type post_type [not null, note: '게시글 타입 (발견/분실)'] + title varchar(255) [not null, note: '게시글 제목'] + description text [not null, note: '게시글 설명'] images varchar[] [note: '이미지 URL 배열'] - location varchar [not null, note: '발견 장소'] - category item_category [not null] - status post_status [not null, default: 'PENDING'] - author_id uuid [ref: > users.uuid, not null] + location varchar [not null, note: '발견/분실 장소'] + category item_category [not null, default: 'ETC', note: '아이템 카테고리'] + status post_status [not null, note: '게시글 상태'] created_at timestamp [not null, default: `now()`] updated_at timestamp [not null] + deleted_at timestamp [note: '게시글 삭제 일자'] + author_id uuid [ref: > users.uuid, not null, note: '작성자 UUID'] indexes { author_id } - Note: '발견 게시글' + Note: '게시글 정보' } -Table lost_posts { - id uuid [pk, not null, default: `gen_random_uuid()`] - title varchar [not null] - description text [not null] - images varchar[] [note: '이미지 URL 배열'] - location varchar [not null, note: '분실 추정 장소'] - category item_category [not null] - status post_status [not null, default: 'PENDING'] - author_id uuid [ref: > users.uuid, not null] +Table comments { + id serial [pk, not null] + content text [not null, note: '댓글 내용'] + type comment_type [not null, note: '댓글 타입 (댓글/대댓글)'] created_at timestamp [not null, default: `now()`] - updated_at timestamp [not null] + is_deleted boolean [not null, default: false, note: '댓글 삭제 여부'] + post_id int [ref: > posts.id, not null, note: '게시글 ID'] + author_id uuid [ref: > users.uuid, not null, note: '작성자 UUID'] + parent_id int [ref: > comments.id, note: '상위 댓글 ID'] - indexes { - author_id - } - - Note: '분실 게시글' -} \ No newline at end of file + Note: '댓글 정보' +} diff --git a/src/idp/idp.service.ts b/src/idp/idp.service.ts index 2dc2984..99e37f2 100644 --- a/src/idp/idp.service.ts +++ b/src/idp/idp.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Injectable, InternalServerErrorException, UnauthorizedException, @@ -52,7 +53,10 @@ export class IdpService { if (error instanceof AxiosError && error.response?.status === 401) { throw new UnauthorizedException(); } - throw new InternalServerErrorException(); + if (error instanceof AxiosError && error.response?.status === 400) { + throw new BadRequestException(error.response.data); + } + throw new InternalServerErrorException(error.response.data); }), ), ); diff --git a/src/image/image.service.ts b/src/image/image.service.ts index cfc3ed2..51a0a88 100644 --- a/src/image/image.service.ts +++ b/src/image/image.service.ts @@ -10,6 +10,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, + NotFoundException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -139,7 +140,11 @@ export class ImageService { }, }); await this.s3Client.send(command).catch((error) => { - throw new InternalServerErrorException(error); + if (error.Code === 'AccessDenied') { + throw new NotFoundException('Image key is invalid'); + } + + throw new InternalServerErrorException(); }); } diff --git a/src/post/dto/req/createPost.dto.ts b/src/post/dto/req/createPost.dto.ts index 2da3c97..8cff453 100644 --- a/src/post/dto/req/createPost.dto.ts +++ b/src/post/dto/req/createPost.dto.ts @@ -34,7 +34,7 @@ export class CreatePostDto { @ApiProperty({ type: [String], description: 'Array of image keys/filenames for the post', - example: '[thisisanimagekey.jpg]', + example: ['thisisanimagekey.jpg'], }) @IsString({ each: true }) @IsOptional() diff --git a/src/post/dto/req/myPostFilter.dto.ts b/src/post/dto/req/myPostFilter.dto.ts new file mode 100644 index 0000000..5588b68 --- /dev/null +++ b/src/post/dto/req/myPostFilter.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PostType } from '@prisma/client'; +import { IsEnum, IsOptional } from 'class-validator'; + +export class MyPostFilterDto { + @ApiProperty({ + enum: PostType, + enumName: 'PostType', + description: 'Type of post (FOUND or LOST)', + example: PostType.FOUND, + required: false, + }) + @IsEnum(PostType) + @IsOptional() + type?: PostType; +} diff --git a/src/post/dto/req/postFilter.dto.ts b/src/post/dto/req/postFilter.dto.ts index ebce11d..b0b015f 100644 --- a/src/post/dto/req/postFilter.dto.ts +++ b/src/post/dto/req/postFilter.dto.ts @@ -37,7 +37,7 @@ export class PostFilterDto { @ApiProperty({ type: Number, description: 'Pagination cursor for next set of results', - example: 17, + example: 0, }) @Transform(({ value }) => parseInt(value, 10), { toClassOnly: true }) @IsInt() diff --git a/src/post/post.controller.ts b/src/post/post.controller.ts index 34b24bc..046bc5c 100644 --- a/src/post/post.controller.ts +++ b/src/post/post.controller.ts @@ -27,6 +27,7 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; +import { MyPostFilterDto } from './dto/req/myPostFilter.dto'; @ApiTags('Post') @Controller('post') @@ -66,8 +67,11 @@ export class PostController { @ApiBearerAuth('access-token') @Get('my-posts') @UseGuards(IdPGuard) - async getMyPosts(@GetUser() user: User): Promise { - return this.postService.getMyPostList(user.uuid); + async getMyPosts( + @GetUser() user: User, + @Query() myPostFilterDto: MyPostFilterDto, + ): Promise { + return this.postService.getMyPostList(user.uuid, myPostFilterDto); } @ApiOperation({ diff --git a/src/post/post.repository.ts b/src/post/post.repository.ts index 55b7ee8..25bf0f5 100644 --- a/src/post/post.repository.ts +++ b/src/post/post.repository.ts @@ -5,6 +5,7 @@ import { PostResponseDto } from './dto/res/postRes.dto'; import { Injectable } from '@nestjs/common'; import { UpdatePostDto } from './dto/req/updatePost.dto'; import { PostFilterDto } from './dto/req/postFilter.dto'; +import { PostType } from '@prisma/client'; @Injectable() export class PostRepository { @@ -96,9 +97,12 @@ export class PostRepository { }; } - async findPostsByUser(userUuid: string): Promise { + async findPostsByUser( + userUuid: string, + type?: PostType, + ): Promise { const posts = await this.prismaService.post.findMany({ - where: { authorId: userUuid }, + where: { authorId: userUuid, type }, include: { author: { select: { diff --git a/src/post/post.service.ts b/src/post/post.service.ts index 1e15867..f590f94 100644 --- a/src/post/post.service.ts +++ b/src/post/post.service.ts @@ -11,6 +11,7 @@ import { PostListDto, PostResponseDto } from './dto/res/postRes.dto'; import { UpdatePostDto } from './dto/req/updatePost.dto'; import { PostFilterDto } from './dto/req/postFilter.dto'; import { ImageService } from 'src/image/image.service'; +import { MyPostFilterDto } from './dto/req/myPostFilter.dto'; @Injectable() export class PostService { @@ -52,8 +53,13 @@ export class PostService { }; } - async getMyPostList(userUuid: string): Promise { - const postList = await this.postRepository.findPostsByUser(userUuid); + async getMyPostList( + userUuid: string, + myPostFilterDto: MyPostFilterDto, + ): Promise { + const { type } = myPostFilterDto; + + const postList = await this.postRepository.findPostsByUser(userUuid, type); const formattedPosts = await Promise.all( postList.map(async (post) => { @@ -87,6 +93,31 @@ export class PostService { }; } + async createPost( + createPostDto: CreatePostDto, + userUuid: string, + ): Promise { + if (createPostDto.images.length) { + await this.imageService.validateImages(createPostDto.images); + } + + const newPost = await this.postRepository.createPost( + createPostDto, + userUuid, + ); + + const signedUrls = await this.imageService.generateSignedUrls( + newPost.images, + ); + + // TODO: FCM process need to be added. + + return { + ...newPost, + images: signedUrls, + }; + } + async updatePost( id: number, updatePostDto: UpdatePostDto, @@ -123,29 +154,4 @@ export class PostService { return this.postRepository.deletePost(id); } - - async createPost( - createPostDto: CreatePostDto, - userUuid: string, - ): Promise { - if (createPostDto.images.length) { - await this.imageService.validateImages(createPostDto.images); - } - - const newPost = await this.postRepository.createPost( - createPostDto, - userUuid, - ); - - const signedUrls = await this.imageService.generateSignedUrls( - newPost.images, - ); - - // TODO: FCM process need to be added. - - return { - ...newPost, - images: signedUrls, - }; - } }