diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 1c34f76..f925796 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -8,6 +8,7 @@ import { GameModule } from './game/game.module'; import { LikeModule } from './like/like.module'; import { CommentModule } from './comment/comment.module'; import { UserModule } from './user/user.module'; +import { SelectedItemModule } from './selected-item/selected-item.module'; @Module({ imports: [ @@ -24,6 +25,7 @@ import { UserModule } from './user/user.module'; LikeModule, CommentModule, UserModule, + SelectedItemModule, ], controllers: [], providers: [], diff --git a/backend/src/comment/comment.controller.ts b/backend/src/comment/comment.controller.ts index afd2dfa..67f2143 100644 --- a/backend/src/comment/comment.controller.ts +++ b/backend/src/comment/comment.controller.ts @@ -4,7 +4,13 @@ import { CreateCommentDto } from 'src/comment/dto/create-comment.dto'; import { AuthGuard } from '@nestjs/passport'; import { Request } from 'express'; import { DeleteCommentDto } from 'src/comment/dto/delete-comment.dto'; -import { ApiBody, ApiCookieAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiBody, + ApiCookieAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; @Controller('comment') @ApiTags('댓글 api') @@ -15,6 +21,14 @@ export class CommentController { @ApiCookieAuth('accessToken') @Post() @ApiOperation({ summary: '아이템에 댓글을 입력합니다.' }) + @ApiResponse({ + status: 200, + description: '댓글이 성공적으로 입력되었습니다.', + }) + @ApiResponse({ + status: 404, + description: '아이템 또는 유저를 찾을 수 없습니다.', + }) @ApiBody({ type: CreateCommentDto }) async createComment( @Req() req: Request, diff --git a/backend/src/game/game.entity.ts b/backend/src/game/game.entity.ts index 13c24e0..c8b0c2e 100644 --- a/backend/src/game/game.entity.ts +++ b/backend/src/game/game.entity.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Item } from 'src/item/item.entity'; +import { SelectedItem } from 'src/selected-item/selected-item.entity'; import { User } from 'src/user/user.entity'; import { Entity, @@ -37,4 +38,11 @@ export class Game { type: () => [Item], }) items: Item[]; + + @OneToMany(() => SelectedItem, (selectedItem) => selectedItem.game) + @ApiProperty({ + description: '게임에서 선택된 아이템들', + type: () => [SelectedItem], + }) + selectedItems: SelectedItem[]; } diff --git a/backend/src/item/item.entity.ts b/backend/src/item/item.entity.ts index 15b799c..b67661e 100644 --- a/backend/src/item/item.entity.ts +++ b/backend/src/item/item.entity.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Comment } from 'src/comment/comment.entity'; import { Game } from 'src/game/game.entity'; +import { SelectedItem } from 'src/selected-item/selected-item.entity'; import { Entity, PrimaryGeneratedColumn, @@ -47,4 +48,11 @@ export class Item { example: 1, }) selected_count: number; + + @OneToMany(() => SelectedItem, (selectedItem) => selectedItem.item) + @ApiProperty({ + description: '아이템이 선택된 기록들', + type: () => [SelectedItem], + }) + selectedItems: SelectedItem[]; } diff --git a/backend/src/like/like.controller.ts b/backend/src/like/like.controller.ts index 216bc30..a6ccafe 100644 --- a/backend/src/like/like.controller.ts +++ b/backend/src/like/like.controller.ts @@ -26,7 +26,7 @@ export class LikeController { @Post(':comment_id') @ApiCookieAuth('accessToken') @ApiOperation({ summary: '댓글 좋아요' }) - @ApiResponse({ status: 201, description: '정상' }) + @ApiResponse({ status: 200, description: '정상' }) @ApiResponse({ status: 404, description: '유저 또는 댓글이 없습니다.' }) @ApiResponse({ status: 409, diff --git a/backend/src/selected-item/selected-item.controller.ts b/backend/src/selected-item/selected-item.controller.ts new file mode 100644 index 0000000..6cc8600 --- /dev/null +++ b/backend/src/selected-item/selected-item.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { SelectedItemService } from './selected-item.service'; +import { AuthGuard } from '@nestjs/passport'; +import { Request } from 'express'; + +@Controller('games/:gameId/items/:itemId/select') +export class SelectedItemController { + constructor(private readonly selectedItemService: SelectedItemService) {} + + @UseGuards(AuthGuard('jwt')) + @Post() + async selectOrToggleItem( + @Param('gameId') gameId: string, + @Param('itemId') itemId: string, + @Req() req: Request, + ): Promise { + const kakaoId = req.user.kakaoId; + + await this.selectedItemService.selectOrToggleItem(kakaoId, gameId, itemId); + } +} diff --git a/backend/src/selected-item/selected-item.entity.ts b/backend/src/selected-item/selected-item.entity.ts new file mode 100644 index 0000000..599a3d8 --- /dev/null +++ b/backend/src/selected-item/selected-item.entity.ts @@ -0,0 +1,54 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { User } from 'src/user/user.entity'; +import { Item } from 'src/item/item.entity'; +import { Game } from 'src/game/game.entity'; +import { + Entity, + PrimaryGeneratedColumn, + ManyToOne, + CreateDateColumn, + Unique, + JoinColumn, +} from 'typeorm'; + +@Entity() +@Unique(['user', 'game']) // 특정 사용자가 같은 게임에서 한 번만 선택하도록 제한 +export class SelectedItem { + @PrimaryGeneratedColumn('uuid') + @ApiProperty({ + description: '선택된 아이템 기록의 고유 식별자', + example: '12312-12312-12312-12312', + }) + selected_item_id: string; + + @ManyToOne(() => User, (user) => user.selectedItems, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) // 외래 키 이름을 'user_id'로 명시적으로 설정 + @ApiProperty({ + description: '아이템을 선택한 사용자', + type: () => User, + }) + user: User; + + @ManyToOne(() => Game, (game) => game.selectedItems, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'game_id' }) // 외래 키 이름을 'game_id'로 명시적으로 설정 + @ApiProperty({ + description: '선택이 이루어진 게임', + type: () => Game, + }) + game: Game; + + @ManyToOne(() => Item, (item) => item.selectedItems, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'item_id' }) // 외래 키 이름을 'item_id'로 명시적으로 설정 + @ApiProperty({ + description: '선택된 아이템', + type: () => Item, + }) + item: Item; + + @CreateDateColumn() + @ApiProperty({ + description: '아이템이 선택된 날짜와 시간', + example: '2024-09-21T10:20:30Z', + }) + selected_at: Date; +} diff --git a/backend/src/selected-item/selected-item.module.ts b/backend/src/selected-item/selected-item.module.ts new file mode 100644 index 0000000..3a61c1f --- /dev/null +++ b/backend/src/selected-item/selected-item.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { SelectedItemService } from './selected-item.service'; +import { SelectedItemController } from './selected-item.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Item } from 'src/item/item.entity'; +import { User } from 'src/user/user.entity'; +import { Game } from 'src/game/game.entity'; +import { SelectedItem } from 'src/selected-item/selected-item.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Item, User, Game, SelectedItem])], + controllers: [SelectedItemController], + providers: [SelectedItemService], +}) +export class SelectedItemModule {} diff --git a/backend/src/selected-item/selected-item.service.ts b/backend/src/selected-item/selected-item.service.ts new file mode 100644 index 0000000..755c256 --- /dev/null +++ b/backend/src/selected-item/selected-item.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { SelectedItem } from './selected-item.entity'; +import { User } from 'src/user/user.entity'; +import { Item } from 'src/item/item.entity'; +import { Game } from 'src/game/game.entity'; +import { NotFoundException, ConflictException } from '@nestjs/common'; + +@Injectable() +export class SelectedItemService { + constructor( + @InjectRepository(SelectedItem) + private selectedItemRepository: Repository, + @InjectRepository(Item) + private itemRepository: Repository, + @InjectRepository(Game) + private gameRepository: Repository, + @InjectRepository(User) + private userRepository: Repository, + private readonly dataSource: DataSource, + ) {} + + async selectOrToggleItem( + user_id: string, + game_id: string, + item_id: string, + ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const user = await this.userRepository.findOneBy({ user_id }); + if (!user) { + throw new NotFoundException('User not found'); + } + + const game = await this.gameRepository.findOneBy({ game_id }); + const item = await this.itemRepository.findOneBy({ item_id }); + if (!game || !item) { + throw new NotFoundException('Game or item not found'); + } + + const existingSelection = await queryRunner.manager.findOne( + SelectedItem, + { + where: { + user: { user_id: user.user_id }, + game: { game_id: game.game_id }, + }, + relations: ['item'], + }, + ); + + if (existingSelection) { + if (existingSelection.item.item_id === item.item_id) { + // 동일한 아이템을 선택하면 취소 (삭제) + await queryRunner.manager.remove(existingSelection); + await this.updateItemSelectionCount(queryRunner, item, -1); // 선택 횟수 감소 + } else { + // 다른 아이템을 선택하면 이전 선택을 취소하고 새로운 아이템 선택 + await queryRunner.manager.remove(existingSelection); + await this.updateItemSelectionCount( + queryRunner, + existingSelection.item, + -1, + ); // 이전 아이템 선택 횟수 감소 + await this.createNewSelection(queryRunner, user, game, item); // 새로운 선택 + } + } else { + // 중복 확인 + const duplicateCheck = await queryRunner.manager.findOne(SelectedItem, { + where: { user, game, item }, + }); + if (duplicateCheck) { + throw new ConflictException('Duplicate selection exists'); + } + // 처음 선택하는 경우 + await this.createNewSelection(queryRunner, user, game, item); + } + + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + private async createNewSelection( + queryRunner: any, + user: User, + game: Game, + item: Item, + ): Promise { + const newSelection = this.selectedItemRepository.create({ + user, + game, + item, + }); + await queryRunner.manager.save(newSelection); + await this.updateItemSelectionCount(queryRunner, item, 1); // 선택 횟수 증가 + } + + private async updateItemSelectionCount( + queryRunner: any, + item: Item, + countChange: number, + ): Promise { + if (item.selected_count === undefined || item.selected_count === null) { + item.selected_count = 0; // 기본값 설정 + } + item.selected_count += countChange; + await queryRunner.manager.save(item); + } +} diff --git a/backend/src/user/user.entity.ts b/backend/src/user/user.entity.ts index c71d098..12b9b16 100644 --- a/backend/src/user/user.entity.ts +++ b/backend/src/user/user.entity.ts @@ -1,23 +1,52 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Comment } from 'src/comment/comment.entity'; import { Game } from 'src/game/game.entity'; import { Like } from 'src/like/like.entity'; +import { SelectedItem } from 'src/selected-item/selected-item.entity'; import { Entity, Column, OneToMany, Unique, PrimaryColumn } from 'typeorm'; @Entity() @Unique(['user_id']) export class User { @PrimaryColumn() + @ApiProperty({ + description: '사용자의 고유 식별자', + example: '1234567890', + }) user_id: string; @Column({ unique: true }) + @ApiProperty({ + description: '사용자의 닉네임', + example: 'username', + }) username: string; @OneToMany(() => Game, (balanceGame) => balanceGame.user) + @ApiProperty({ + description: '사용자가 만든 게임들', + type: () => [Game], + }) games: Game[]; @OneToMany(() => Comment, (comment) => comment.user) + @ApiProperty({ + description: '사용자가 단 댓글들', + type: () => [Comment], + }) comments: Comment[]; @OneToMany(() => Like, (like) => like.user) + @ApiProperty({ + description: '사용자가 누른 좋아요들', + type: () => [Like], + }) likes: Like[]; + + @OneToMany(() => SelectedItem, (selectedItem) => selectedItem.user) + @ApiProperty({ + description: '사용자가 선택한 아이템들', + type: () => [SelectedItem], + }) + selectedItems: SelectedItem[]; }