Skip to content

Commit

Permalink
feat: 아이템 선택 api 추가 (#12)
Browse files Browse the repository at this point in the history
* feat: best 댓글 가져오기 추가

* fix: comment에 user_id가 저장되지 않는 오류 수정

* feat: 베스트 댓글 가져오는 api 추가

* feat: 아이템 선택 api 추가
  • Loading branch information
Hellol77 authored Sep 28, 2024
1 parent f0fa0ca commit 19b9286
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 2 deletions.
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -24,6 +25,7 @@ import { UserModule } from './user/user.module';
LikeModule,
CommentModule,
UserModule,
SelectedItemModule,
],
controllers: [],
providers: [],
Expand Down
16 changes: 15 additions & 1 deletion backend/src/comment/comment.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions backend/src/game/game.entity.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -37,4 +38,11 @@ export class Game {
type: () => [Item],
})
items: Item[];

@OneToMany(() => SelectedItem, (selectedItem) => selectedItem.game)
@ApiProperty({
description: '게임에서 선택된 아이템들',
type: () => [SelectedItem],
})
selectedItems: SelectedItem[];
}
8 changes: 8 additions & 0 deletions backend/src/item/item.entity.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -47,4 +48,11 @@ export class Item {
example: 1,
})
selected_count: number;

@OneToMany(() => SelectedItem, (selectedItem) => selectedItem.item)
@ApiProperty({
description: '아이템이 선택된 기록들',
type: () => [SelectedItem],
})
selectedItems: SelectedItem[];
}
2 changes: 1 addition & 1 deletion backend/src/like/like.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions backend/src/selected-item/selected-item.controller.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const kakaoId = req.user.kakaoId;

await this.selectedItemService.selectOrToggleItem(kakaoId, gameId, itemId);
}
}
54 changes: 54 additions & 0 deletions backend/src/selected-item/selected-item.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
15 changes: 15 additions & 0 deletions backend/src/selected-item/selected-item.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
119 changes: 119 additions & 0 deletions backend/src/selected-item/selected-item.service.ts
Original file line number Diff line number Diff line change
@@ -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<SelectedItem>,
@InjectRepository(Item)
private itemRepository: Repository<Item>,
@InjectRepository(Game)
private gameRepository: Repository<Game>,
@InjectRepository(User)
private userRepository: Repository<User>,
private readonly dataSource: DataSource,
) {}

async selectOrToggleItem(
user_id: string,
game_id: string,
item_id: string,
): Promise<void> {
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<void> {
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<void> {
if (item.selected_count === undefined || item.selected_count === null) {
item.selected_count = 0; // 기본값 설정
}
item.selected_count += countChange;
await queryRunner.manager.save(item);
}
}
29 changes: 29 additions & 0 deletions backend/src/user/user.entity.ts
Original file line number Diff line number Diff line change
@@ -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[];
}

0 comments on commit 19b9286

Please sign in to comment.