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

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

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

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

 

내일배움캠프 로고

 

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

 

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

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

 

 

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

 

🎯 프로젝트 주제 및 목표

이번 프로젝트는 "프로젝트 협업 도구"를 만드는 것입니다. 프로젝트 협업 도구 중 Trello 라는 서비스를 고르게 되었습니다. Trello는 칸반 보드 기반 서비스로 유명한 프로젝트 협업 도구로, Trello에서 제공하는 다양한 기능을 구현하는 것이 이번 프로젝트의 목표 입니다.

 

trello 화면
trello 화면

 

 

 

✅ 프로젝트 필수 구현 기능

Trello를 참고하여 프로젝트에 필수로 구현될 기능은 다음과 같습니다.

 

  • 사용자 관리 기능 (로그인 / 회원가입 / 사용자 정보 수정 및 삭제)
  • 보드 관리 기능 (보드 생성 / 보드 수정 / 보드 삭제 / 보드 초대)
  • 컬럼 관리 기능 (컬럼 생성 / 컬럼 이름 수정 / 컬럼 삭제 / 컬럼 순서 이동)
  • 카드 관리 기능 (카드 생성 / 카드 수정 / 카드 삭제 / 카드 이동)
  • 카드 상세 기능 (댓글 달기 / 날짜 지정)

 

위 기능 중 필자는 보드 관리 기능 (팀에서는 보드 대신 워크스페이스라고 명칭을 바꿔 사용)을 담당하였습니다.

 

 

GitHub - Beardevelope/collabotools-Project

Contribute to Beardevelope/collabotools-Project development by creating an account on GitHub.

github.com

팀프로젝트 소스코드 확인시 위 링크를 참고바랍니다

 

 

👩‍💻 기술스택

  • 프로그래밍 언어: TypeScript, JavaScript (Node.js)
  • 프레임워크: Nest.js
  • 데이터베이스: TypeORM, AWS RDS
  • 버전 관리 시스템: Git
  • 개발 도구: Visual Studio Code
  • 배포 환경: GitHub
  • 테스트 도구: Thunder Client

 

🗺️ 와이어프레임

프로젝트 와이어프레임

 

 

📜 API 명세서

API의 경우 필자가 담당한 API 명세서만 넣었습니다.

워크스페이스 생성 API
워크스페이스 생성 API
워크스페이스 조회API
워크스페이스 조회 API

 

워크스페이스 삭제 API
워크스페이스 삭제 API

 

워크스페이스 멤버 추가 API
워크스페이스 멤버추가 API

 

 

📌 ERD

프로젝트 erd

 

 

✍ 프로젝트 진행사항

현재 필자가 맡은 워크스페이스 생성,조회,삭제 및 멤버 추가 API 1차 구현 완료되었습니다. 하지만 사용자 정보에 대한 인가 부분이 아직 구현되지 않아 팀원이 기능 완성하는 데로 추가할 예정입니다. 

 

또한, 멤버 추가하기 전 멤버 추가하려는 테이블에 추가하려는 데이터가 있는지 검증 후 insert 하는 로직도 추가할 예정입니다.

 

 

구현한 API 소스

 

workspaces.controller.ts

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

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

  @Post()
  async createWorkspace(@Body() createworkspaceDto: CreateWorkspaceDto) {
    return await this.workspacesService.createWorkspace(createworkspaceDto);
  }

  @Get()
  async findAllWorkspace() {
    return await this.workspacesService.findAllWorkspace();
  }

  @Put(':workspaceId')
    async updateWorkspace(@Param('workspaceId') workspaceId: number,@Body() createworkspaceDto: CreateWorkspaceDto) {
      return await this.workspacesService.updateWorkspace(workspaceId,createworkspaceDto);
    }
  
  @Delete(':workspaceId')
  async deleteWorkspace(@Param('workspaceId') workspaceId: number) {
    return await this.workspacesService.deleteWorkspace(workspaceId);
  }

  @Post('invite')
  async inviteMember(@Body() membersDto: MembersDto) {
    return await this.workspacesService.inviteMembers(membersDto);
  }
}

 

 

workspaces.service.ts

import { HttpException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { DataSource, Repository, getConnection } 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(createworkspaceDto:CreateWorkspaceDto) {

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

        // TODO: worksapce 추가시 ownerId 추가 어떻게? 인가된 값으로 추가

        // TODO: 작성자 멤버(users_model_workspaces_workspaces_model)도 owner에 저장하는 로직 추가!!!!



        // workspace 정보 저장
        await this.workspaceRepository.save(workspace);


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

    async findAllWorkspace() {
        const workspaces = await this.workspaceRepository.find({
            select:['id','name']
        });
      
        return {
          "message":"워크스페이스를 조회했습니다.",
          "data":workspaces
        }
    }

    async updateWorkspace(workspaceId: number, createworkspaceDto:CreateWorkspaceDto) {

        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(workspaceId: number) {

      const queryRunner = this.dataSource.createQueryRunner();

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

      try{
        await this.verifyWorkSpaceById(workspaceId);

        await queryRunner.manager.delete(ListsModel,{workspaceId: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(membersDto:MembersDto) {

      const members = membersDto.members;

      const values = members.map(({ userId, workspaceId }) => ({
        usersModelId: userId,
        workspacesModelId: workspaceId,
      }));

      const queryRunner = this.dataSource.createQueryRunner();

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

      try{

        // TODO: 멤버 추가시 동일 워크스페이스에 기존 등록한 멤버 있는지 확인

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

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

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

        if (error instanceof HttpException) {
          // HttpException을 상속한 경우(statusCode 속성이 있는 경우)
          throw error;
        } else {
          // 그 외의 예외
          throw 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;
    }

    private async findByEmail(email: string) {
      const user = await this.userRepository.findOneBy({ email });

      if (!user) {
        throw new NotFoundException('존재하지 않는 사용자입니다.');
      }

      return user;
    }

}

 

 

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 { 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, ManyToMany, ManyToOne, OneToMany } from 'typeorm';

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

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

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

    /**
     * admin
     */
    @ManyToOne(() => UsersModel, (user) => user.myWorkspaces)
    owner: UsersModel;

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

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

 

 

앞으로의 계획

 

인가 기능 추가하여 테스트 한 후 프론트엔드 작업 및 다른 팀원의 미비된 기능을 도와줄 예정입니다. 기간 내에 팀에서 목표로한 모든 기능을 원활히 동작할 수 있도록 최선을 다할 예정입니다.

 

▼ 이전 진행한 프로젝트 ▼

 

 

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

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

lemonlog.tistory.com

 

 

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

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

lemonlog.tistory.com