본문 바로가기
내일배움캠프

내일배움캠프 Node트랙 심화 프로젝트 진행사항 3

by 코드스니펫 2024. 1. 10.
반응형

내일배움캠프 Node트랙 심화 프로젝트 역할 및 진행사항 3

 

내일배움캠프 로고

 

이번 프로젝트는 팀 프로젝트로 Node트랙 심화 프로젝트를 진행하게 되었습니다. 프로젝트를 시작하며 팀에서 맡은 역할과 현재 진행사항에 대해 소개하겠습니다.

 

 

프로젝트 발표일까지  D-1

1/5(금) 1/6(토) 1/7(일) 1/8(월) 1/9(화) 1/10(수) 1/11(목)
시작 ▶ 🏃 🏃 🏃 🏃 🏃  

 

 

내일배움캠프 Node트랙 심화 프로젝트 역할 및 진행사항 2

내일배움캠프 Node트랙 심화 프로젝트 역할 및 진행사항 2 이번 프로젝트는 팀 프로젝트로 Node트랙 심화 프로젝트를 진행하게 되었습니다. 프로젝트를 시작하며 팀에서 맡은 역할과 현재 진행사

lemonlog.tistory.com

전날 프로젝트 기록입니다

 

 

내일배움캠프 Node트랙 심화 프로젝트

 

프로젝트 소개

 

내일배움캠프 Node트랙 심화 프로젝트 역할 및 진행사항

내일배움캠프 Node트랙 심화 프로젝트 역할 및 진행사항 이번 프로젝트는 팀 프로젝트로 Node트랙 심화 프로젝트를 진행하게 되었습니다. 프로젝트를 시작하며 팀에서 맡은 역할과 현재 진행사항

lemonlog.tistory.com

프로젝트 전반 소개는 위 페이지에 있습니다

 

 

✍ 프로젝트 진행사항

 

내일배움캠프 Node트랙 심화 프로젝트 역할 및 진행사항 2

내일배움캠프 Node트랙 심화 프로젝트 역할 및 진행사항 2 이번 프로젝트는 팀 프로젝트로 Node트랙 심화 프로젝트를 진행하게 되었습니다. 프로젝트를 시작하며 팀에서 맡은 역할과 현재 진행사

lemonlog.tistory.com

전날 작업내용은 위 페이지에 있습니다

 

작업1. 발표용 PPT 작성

팀에서 맡은 작업 구현은 거의 완료된 상태로, 다른 팀원 코드와 합쳐지기 전까지 발표용 PPT를 작성하였습니다. 처음 피그마로 작성하려 했지만 피그마에서는 GIF 쓰는과정이 복잡하여 구글 슬라이드를 활용하여 작성하였습니다. 문서 작성 후 링크 공유 기능이 있고, 또한 특정한 구글 계정에게 편집 권한을 부여할 수 있어 문서 동시 작성이 가능한 장점이 있어 사용하게 되었습니다.

 

GIF를 고집한 이유는 아래 페이지에서 확인해볼 수 있습니다.

 

 

Trello Project

Trello Project

docs.google.com

Trello Project 발표용 PPT 입니다

 

 

작업2. 작업한 코드 dev Merge

팀원별로 작성한 코드를 dev 브랜치에 전체 Merge하는 작업을 하였습니다. Merge는 Pull Request에 올라온 팀원들의 코드 확인 후 팀장이 진행하였습니다. 필자도 WorkSpace 부분 작성한 코드를 Merge 하였습니다.

 

그리고, 전체 코드 잘 동작하는지 다같이 보면서 확인하였습니다. 팀장의 화면공유로 테스트하는 과정을 보았고, 다행히도 WorkSpace에서는 별 이상 없이 동작할 수 있었습니다. 테스트하면서 카드 부분에 하드코딩된 부분이 있었는데 즉시 수정 후 정상 동작하는 것을 확인할 수 있었습니다.

 

마지막으로 워크스페이스 삭제로 리스트, 카드, 댓글이 전부 CASCADE 되는지 확인하였는데 이 부분도 정상적으로 동작하는 것을 확인할 수 있었습니다.

 

테스트 중간 테스트 계정의 비밀번호를 찾지 못하여 처음부터 새로 테스트 데이터 만드는 번거로움이 있었지만 이 역시도 잘 수행하였습니다.

 

공개처형시간
전체 테스트를 가장한 일명 '공개처형'

 

 

작업3. Controller Guard 수정

전날까지 작업한 WorkSpace Controller 코드에는 UseGuards를 BearerTokenGuard를 사용했으나 팀회의간 AccessTokenGuard로 변경하라는 사항이 들어와 코드 수정을 하였습니다. 또한, 코드를 보니 controller 전체에 UseGuard를 사용하고 있기에 api당 선언하지 말고, @controller 밑에 한번에 선언해도 되는 것을 인지하게 되어 이 부분도 수정하였습니다.

 

위 사항 고려하여 작업한 코드는 아래에 있습니다.

 

 

구현한 API 소스

 

workspaces.controller.ts

import { Body, Controller, Delete, Get, Param, Post, Put, Req, UseGuards } from '@nestjs/common';
import { WorkspacesService } from './workspaces.service';
import { CreateWorkspaceDto } from './dto/create-workspace.dto';
import { MembersDto } from './dto/invite-member.dto';
import { AccessTokenGuard } from 'src/auth/guard/bearer.guard';

@Controller('workspaces')
@UseGuards(AccessTokenGuard)
export class WorkspacesController {
  constructor(private readonly workspacesService: WorkspacesService) {}

    /**
     * 워크스페이스 생성
     * (로그인 한 사용자 가능)
     */
  @Post()
  async createWorkspace(@Req() req: Request,@Body() createworkspaceDto: CreateWorkspaceDto) {
    return await this.workspacesService.createWorkspace(req['userId'],createworkspaceDto);
  }

    /**
     * 워크스페이스 조회
     * (멤버 추가된 워크스페이스만 조회)
     */
  @Get()
  async findAllWorkspace(@Req() req: Request) {
    return await this.workspacesService.findAllWorkspace(req['userId']);
  }

    /**
     * 워크스페이스 수정
     * (워크스페이스 운영자만 가능)
     */
  @Put(':workspaceId')
    async updateWorkspace(@Req() req: Request,@Param('workspaceId') workspaceId: number,@Body() createworkspaceDto: CreateWorkspaceDto) {
      return await this.workspacesService.updateWorkspace(req['userId'],workspaceId,createworkspaceDto);
  }
  
    /**
     * 워크스페이스 삭제
     * (워크스페이스 운영자만 가능)
     */
  @Delete(':workspaceId')
  async deleteWorkspace(@Req() req: Request, @Param('workspaceId') workspaceId: number) {
    return await this.workspacesService.deleteWorkspace(req['userId'],workspaceId);
  }

    /**
     * 워크스페이스 멤버 추가
     * (워크스페이스 운영자만 가능)
     */
  @Post(':workspaceId/invite')
  async inviteMember(@Req() req: Request, @Body() membersDto: MembersDto, @Param('workspaceId') workspaceId: number) {
    return await this.workspacesService.inviteMembers(req['userId'],workspaceId,membersDto);
  }
}

 

 

workspaces.service.ts

import { HttpException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { WorkspacesModel } from './entities/workspaces.entity';
import { CreateWorkspaceDto } from './dto/create-workspace.dto';
import { ListsModel } from 'src/lists/entities/lists.entity';
import { UsersModel } from 'src/users/entities/users.entity';
import { MembersDto } from './dto/invite-member.dto';

@Injectable()
export class WorkspacesService {
    constructor(
        @InjectRepository(WorkspacesModel)
        private workspaceRepository: Repository<WorkspacesModel>,

        @InjectRepository(UsersModel)
        private userRepository: Repository<UsersModel>,

        private readonly dataSource: DataSource,
      ) {}
    
      /**
       * 워크스페이스 생성
       * 
       */
      async createWorkspace(userId:number,createworkspaceDto:CreateWorkspaceDto) {

        const queryRunner = this.dataSource.createQueryRunner();

        await queryRunner.connect();
        await queryRunner.startTransaction();

        try{

          const workspace = this.workspaceRepository.create({
            name:createworkspaceDto.name,
            description:createworkspaceDto.description,
            color:createworkspaceDto.color,
            ownerId:userId
          });

          // workspace 정보 저장
          await queryRunner.manager.save(WorkspacesModel,workspace);

          const newWorkSapceId = await queryRunner.manager
          .createQueryBuilder()
          .select([
            'MAX(workspaces_model.id) AS max_id',
          ])
          .from(WorkspacesModel, 'workspaces_model')
          .where('workspaces_model.ownerId = :userId', { userId })
          .getRawMany();

          // users_model_workspaces_workspaces_model에도 멤버 정보 저장
          await queryRunner.manager
          .createQueryBuilder()
          .insert()
          .into('users_model_workspaces_workspaces_model')
          .values(
              {
                usersModelId:userId,
                workspacesModelId:newWorkSapceId[0].max_id
              }
            )
          .execute();

          await queryRunner.commitTransaction();

          return {
            "message":"워크스페이스를 생성했습니다.",
            "data": workspace
          }

        }catch(error){

          await queryRunner.rollbackTransaction();
          console.log(`error : ${error}`)
          if (error instanceof HttpException) {
            // HttpException을 상속한 경우(statusCode 속성이 있는 경우)
            throw error;
          } else {
            // 그 외의 예외
            throw new InternalServerErrorException('서버 에러가 발생했습니다.');
          }

        }finally{
          await queryRunner.release();
        }
    
    }

    /**
     * 워크스페이스 조회
     * 
     */
    async findAllWorkspace(userId:number) {
        const workspaces = await this.workspaceRepository.createQueryBuilder('workspaces_model')
        .select(['workspaces_model.id', 'workspaces_model.name'])
        .where('workspaces_model.id IN (SELECT uww.workspacesModelId FROM users_model_workspaces_workspaces_model uww WHERE uww.usersModelId = :userId)', { userId })
        .getMany();
      
        return {
          "message":"워크스페이스를 조회했습니다.",
          "data":workspaces
        }
    }

    /**
     * 워크스페이스 수정
     * 
     */
    async updateWorkspace(userId:number,workspaceId: number, createworkspaceDto:CreateWorkspaceDto) {

      await this.isOwner(userId,workspaceId);

        const workspace = await this.verifyWorkSpaceById(workspaceId);
      
          await this.workspaceRepository.update({ id:workspaceId }, 
            {
                name:createworkspaceDto.name,
                description: createworkspaceDto.description,
                color:createworkspaceDto.color
            });
      
          return {
            "message": "워크스페이스 정보를 수정했습니다."
          }

    }

    /**
     * 워크스페이스 삭제
     * 
     */
    async deleteWorkspace(userId:number,workspaceId: number) {

      await this.isOwner(userId,workspaceId);

      const queryRunner = this.dataSource.createQueryRunner();

      await queryRunner.connect();
      await queryRunner.startTransaction();

      try{
        await this.verifyWorkSpaceById(workspaceId);

        await queryRunner.manager.delete('users_model_workspaces_workspaces_model', { workspacesModelId: workspaceId });

        await queryRunner.manager.delete(WorkspacesModel,{id:workspaceId});

        await queryRunner.commitTransaction();

        return {
          "message":"해당 워크스페이스 삭제되었습니다."
        }

      }catch(error){
        await queryRunner.rollbackTransaction();
        console.log(`error : ${error}`)
        if (error instanceof HttpException) {
          // HttpException을 상속한 경우(statusCode 속성이 있는 경우)
          throw error;
        } else {
          // 그 외의 예외
          throw new InternalServerErrorException('서버 에러가 발생했습니다.');
        }
      }finally{
        await queryRunner.release();
      }
    }

    /**
     * 워크스페이스 멤버 추가
     * 
     */
    async inviteMembers(userId:number,workspaceId:number,membersDto:MembersDto) {

      await this.isOwner(userId,workspaceId);

      const members = membersDto.members;
      const isSpaceMembers = await this.listWorkSpaceMember(workspaceId);

      const values = await Promise.all(members.map(async (memberId) => {
        await this.findByUserId(memberId);
        
        // workSpaceMembers 배열에서 user_id가 memberId와 일치하는 사용자 찾기
        const isUserInWorkSpaceMembers = isSpaceMembers.some(member => member.user_id === memberId);

        if (isUserInWorkSpaceMembers) {
          // 사용자가 workSpaceMembers 배열에 존재하지 않는 경우에 대한 처리
          throw new NotFoundException(`사용자 ID ${memberId} (이)가 워크스페이스 멤버로 이미 있습니다.`);
        }
      
        return {
          usersModelId: memberId,
          workspacesModelId: workspaceId,
        };
      }));

      const queryRunner = this.dataSource.createQueryRunner();

      await queryRunner.connect();
      await queryRunner.startTransaction();

      try{

        console.log(`values : ${values}`);

        await queryRunner.manager
        .createQueryBuilder()
        .insert()
        .into('users_model_workspaces_workspaces_model')
        .values(values)
        .execute();
      
        await queryRunner.commitTransaction();

        return {
          "message":"워크스페이스 멤버 추가했습니다."
        }

      }catch(error){
        await queryRunner.rollbackTransaction();

        console.log(`error : ${error.name}`);

        if (error instanceof HttpException) {
          // HttpException을 상속한 경우(statusCode 속성이 있는 경우)
          throw error;
        } else {
          // 그 외의 예외
          new InternalServerErrorException('서버 에러가 발생했습니다.');
        }
      }finally{
        await queryRunner.release();
      }
  
  } 

    /**
     * 워크스페이스 존재여부 확인
     * 
     */
    private async verifyWorkSpaceById(id: number) {
        const workspace = await this.workspaceRepository.findOneBy({ id });
        if (!workspace) {
          throw new NotFoundException('존재하지 않는 워크스페이스입니다.');
        }
    
        return workspace;
    }

    /**
     * 사용자 ID 검증 (회원가입된 사용자 인지 확인)
     * 
     */
    private async findByUserId(userId: number) {

      const queryRunner = this.dataSource.createQueryRunner();

      await queryRunner.connect();
      await queryRunner.startTransaction();

      try{

        const user = await queryRunner.manager
        .createQueryBuilder()
        .select([
          'id AS user_id',
          'email AS email',
          'name AS name',
        ])
        .from(UsersModel, 'users_model')
        .where('users_model.id = :userId', { userId })
        .getRawMany();

        console.log(`user:${user[0].user_id}`)
        if (!user[0].user_id) {
          throw new NotFoundException('존재하지 않는 사용자입니다.');
        }
  
        await queryRunner.commitTransaction();

        return user;

      }catch(error){

        console.log(`error22222 : ${error}`);

        await queryRunner.rollbackTransaction();

        if (error instanceof HttpException) {
          // HttpException을 상속한 경우(statusCode 속성이 있는 경우)
          throw error;
        } else {
          // 그 외의 예외
          if(error.name==='TypeError') throw new NotFoundException(`가입 안된 사용자 ID (userId:${userId})가 있습니다.`);
          else throw new InternalServerErrorException('서버 에러가 발생했습니다.');
        }

      }finally{
        await queryRunner.release();
      }
    }

    /**
     * 워크스페이스 운영자 검증
     * 
     */
    private async isOwner(userId:number,workspaceId:number){

      const owner = await this.workspaceRepository.findOneBy({
        ownerId:userId,
        id:workspaceId
      })

      if(!owner){
        throw new NotFoundException('워크스페이스 운영자가 아닙니다.');
      }

      return true;

    }

    /**
     * 워크스페이스 멤버 검증
     * 
     */
    private async listWorkSpaceMember(workspaceId:number){

      const queryRunner = this.dataSource.createQueryRunner();

      await queryRunner.connect();
      await queryRunner.startTransaction();

      try{

        const user = await queryRunner.manager
        .createQueryBuilder()
        .select([
          'usersModelId AS user_id'
        ])
        .from('users_model_workspaces_workspaces_model', 'users_model')
        .where('users_model.workspacesModelId = :workspaceId', { workspaceId })
        .getRawMany();
  
        await queryRunner.commitTransaction();

        return user;

      }catch(error){

        await queryRunner.rollbackTransaction();

        if (error instanceof HttpException) {
          // HttpException을 상속한 경우(statusCode 속성이 있는 경우)
          throw error;
        } else {
          // 그 외의 예외
          throw new InternalServerErrorException('서버 에러가 발생했습니다.');
        }

      }finally{
        await queryRunner.release();
      }

    }

}

 

 

create-workspace.dto.ts

import { IsNotEmpty, IsString } from 'class-validator';

export class CreateWorkspaceDto {
  @IsString()
  @IsNotEmpty({ message: '워크스페이스명을 입력해주세요.' })
  name: string;

  @IsString()
  @IsNotEmpty({ message: '워크스페이스 내용를 입력해주세요.' })
  description: string;

  @IsString()
  @IsNotEmpty({ message: '색상을 입력해주세요.' })
  color: string;
}

 

 

invite-member.dto.ts

import { ArrayMinSize, ArrayNotEmpty, IsArray, IsInt, IsNotEmpty, IsObject, IsString } from 'class-validator';

export class MembersDto {
  @IsArray()
  @ArrayNotEmpty()
  @ArrayMinSize(1)
  members: MemberDto[];
}

export class MemberDto {
  @IsObject()
  @IsInt()
  userId: number;

  @IsObject()
  @IsInt()
  workspaceId: number;
}

 

 

workspaces.entity.ts

import { IsNumber, IsString } from 'class-validator';
import { BaseModel } from 'src/common/entities/base.entity';
import { ListsModel } from 'src/lists/entities/lists.entity';
import { UsersModel } from 'src/users/entities/users.entity';
import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany } from 'typeorm';

@Entity()
export class WorkspacesModel extends BaseModel {
    @Column()
    @IsString()
    name: string;

    @Column()
    @IsString()
    description: string;

    @Column()
    @IsString()
    color: string;

    @Column()
    @IsNumber()
    ownerId:number;

    /**
     * admin
     */
    @ManyToOne(() => UsersModel, (user) => user.myWorkspaces)
    @JoinColumn({ name: 'ownerId' })
    owner: UsersModel;

    /**
     * 멤버
     */
    @ManyToMany(() => UsersModel, (user) => user.workspaces)
    users: UsersModel;

    /**
     * 리스트
     */
    @OneToMany(() => ListsModel, (list) => list.workspace,{
        cascade: true
    })
    lists: ListsModel[];
}

 

 

앞으로의 계획

 

팀회의 간 프론트엔드 작업은 따로 진행하지 않고, 백엔드 작업만 하기로 해서 지금 구현한 기능과 팀원이 작업한 기능을 내일 합쳐보고 발생하는 오류에 대해서 대처하는 방향으로 진행하기로 했습니다. 사실 오류가 없어야 정상이지만 여러 프로젝트를 하고 보니 여러 팀원 작업이 합쳐지만 꼭 어딘가에서 오류가 발생했기에 이번엔 최소한의 오류만 나타나길 바랄뿐입니다.

 

 

▼ 이전 진행한 프로젝트 ▼

 

 

내일배움캠프 NestJS 프로젝트 코드리뷰 - 온라인 공연 예매 서비스

내일배움캠프 NestJS 프로젝트 코드리뷰 - 온라인 공연 예매 서비스 내일배움캠프를 진행한지도 벌써 3개월 정도로 접어들고 있습니다. 이 글에서는 내일배움캠프에서 필자가 진행한 NestJS 개인

lemonlog.tistory.com

 

 

내일배움캠프 백오피스 프로젝트 - 펫시터 매칭 사이트 후기, 소감

내일배움캠프 백오피스 프로젝트 - 펫시터 매칭 사이트 후기, 소감 일주일간 팀원과 작업한 펫시터 매칭 사이트가 끝났습니다. 여러 우여곡절이 있었지만 목표한 대로 마쳤기에 만족하고 있습

lemonlog.tistory.com