ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 2023 - 08 - 10 트렐로 Trello 순서변경 로직 구현
    Today I Learned/TIL 08 2023. 8. 16. 10:45

     

    트렐로는 유저가 보드를 생성할 수 있고,

    보드를 만든 유저가 다른 유저를 해당 보드의 멤버로 초대할 수 있다.

     

    유저는 보드를 생성, 수정, 삭제, 조회 할 수 있고,

    보드안에 리스트를 생성, 수정, 삭제, 조회 할 수 있고,

    리스트 안에 카드를 생성, 수정, 삭제, 조회 할 수 있고,

    카드 안에 댓글을 생성, 수정, 삭제, 조회 할 수 있다. 즉 CRUD가 4번 들어간다.

     

    무엇보다 가장 어려운 것은 보드 내에서 리스트들의 순서를 변경할 수 있어야 했고,

    리스트 안에서 카드들의 순서 변경, 및 리스트 안의 카드를 다른 리스트로 순서를 변경해야 하는 기능을 구현하기가 가장 어려웠다.

     


    프론트에서 순서 변경을 했을때 order (순서) 값만 받아오고 데이터 처리는 뒷단에서 처리하는 로직을 구현하려고 했다.

    그래서 body로 order 값을 받아준다

     

    card 의 순서를 바꿀때 , 나머지의 데이터들도 함께 바꿔줘야 하기 때문에 생각을 많이 했어야했다

    그래서 처음에 구현할때는 if 문으로 상황을 나누고 ,for 문으로 돌리면 되겠지 라고 간단하게 생각했다

    처음에 작성한 노트다.

    1234
    a b c d e f 
    a c d b e f
    b -> d             beforePosition :2    afterPosition :4    
     beforePosition 보다 큰거  -1/

    afterPosition  큰거 +1
    a b  

    if(   beforePosition  < position <afterPosition  )
       {
             }
    위치 숫자 position 
    움직여서 도착하려는 위치 afterPosition
    기존 위치 beforePosition

     

    await this.cardRepository.manager.transaction(async (transactionManager) => {
      const cards = await transactionManager.find(Cards, { where: { lid }, order: { order: 'ASC' } });
      const card = cards.find((c) => c.cid === cid);
    
      if (!card) {
        throw new NotFoundException('Card ID가 존재하지 않습니다.');
      }
    
      const oldPosition = card.order;
      const startIndex = Math.min(oldPosition, newPosition);
      const endIndex = Math.max(oldPosition, newPosition) - 1; // 수정: endIndex 값을 수정
    
      if (startIndex === endIndex) {
        return; // 이동할 필요 없음
      }
    
      // 이동하는 컬럼과 대상 위치 컬럼 사이의 컬럼들의 순서를 조정
      for (let i = startIndex; i < endIndex; i++) { // 수정: endIndex - 1까지 반복
        if (i === oldPosition) {
          console.log('일이프', cards[i].order);
          cards[i].order = newPosition;
        } else {
          console.log('몇번째 ~', i);
          console.log('2이프', cards[i].order);
          cards[i].order = cards[i + 1].order;
        }
        await transactionManager.save(cards[i]);
      }
    });

    이렇게 짜보고 실행해보니 결과값이 이상하게 나왔다.

    이 코드에서 고치려다보니 새벽까지 팀원 전부가 매달렸는데도 답이 없었다

    결국 잘하시는분께 도움을 받고 고쳐진 코드가 

     

    import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { DataSource, Repository } from 'typeorm';
    import { Cards } from './cards.entity';
    import { Comments } from 'src/Comments/comments.entity';
    import { Lists } from 'src/Lists/lists.entity';
    import { CreateCardDto } from './dto/create-card.dto';
    import { UpdateCardDto } from './dto/update-card.dto';
    import { DeadlineDto } from './dto/update-deadline.dto';
    import { ManagerDto } from './dto/update-manager.dto';
    import { OrderDto } from './dto/order.dto';
    import { create, max } from 'lodash';
    
    @Injectable()
    export class CardsService {
      constructor(
        @InjectRepository(Cards) private cardRepository: Repository<Cards>,
        @InjectRepository(Comments) private commentRepository: Repository<Comments>,
        @InjectRepository(Lists) private listRepository: Repository<Lists>,
        private readonly dataSource: DataSource,
      ) {}
    
      // 1. 카드 목록 조회
      async getCard(lid: number): Promise<Cards[]> {
        const list = await this.listRepository.findOne({ where: { lid } });
        if (!list.lid || list.lid == undefined) {
          throw new NotFoundException('List ID가 존재하지 않습니다.');
        }
        const cards = await this.cardRepository.find({ where: { lid } });
        if (!cards || cards == undefined) {
          throw new NotFoundException('카드 조회에 실패했습니다.');
        }
        return cards;
      }
    
      async findMaxOrder(lid: number): Promise<number> {
        const maxOrderRecord = await this.cardRepository.createQueryBuilder('card').select('MAX(card.order)', 'max_order').where('card.lid = :lid', { lid: lid }).getRawOne();
    
        return maxOrderRecord?.max_order || 0;
      }
    
      // 2. 카드 생성
      async createCard(lid: number, createCardDto: CreateCardDto) {
        const list = await this.listRepository.findOne({ where: { lid } });
        if (!list || list == undefined) {
          throw new NotFoundException('List ID가 존재하지 않습니다.');
        } else if (!createCardDto) {
          throw new BadRequestException('미기입된 항목이 있습니다. 모두 입력해주세요.');
        }
    
        // order 1씩증가
        const maxOrder = await this.findMaxOrder(lid);
    
        if (maxOrder == 0) {
          const data = await this.cardRepository.save({
            lid: lid,
            title: createCardDto.title,
            color: createCardDto.color,
            manager: createCardDto.manager,
            explanation: createCardDto.explanation,
            deadline: createCardDto.deadline,
          });
          return data;
        } else {
          const data = await this.cardRepository.save({
            lid: lid,
            title: createCardDto.title,
            color: createCardDto.color,
            manager: createCardDto.manager,
            explanation: createCardDto.explanation,
            deadline: createCardDto.deadline,
            order: maxOrder + 1,
          });
          return data;
        }
      }
    
      // 3. 카드 수정
      async updateCard(cid: number, updateCardDto: UpdateCardDto) {
        const IsCid = await this.cardRepository.findOne({ where: { cid } });
        if (!IsCid || IsCid == undefined) {
          throw new NotFoundException('해당 카드가 존재하지 않습니다.');
        }
    
        await this.cardRepository.update(
          { cid },
          {
            title: updateCardDto.title,
            color: updateCardDto.color,
            explanation: updateCardDto.explanation,
          },
        );
        const update = await this.cardRepository.findOne({ where: { cid } });
        return update;
      }
    
      // 4. 카드 삭제
      async deleteCard(lid: number, cid: number): Promise<void> {
        const list = await this.listRepository.findOne({ where: { lid } });
        const card = await this.cardRepository.findOne({ where: { cid } });
        if (!list || list == undefined) {
          throw new NotFoundException('해당 리스트가 존재하지 않습니다.');
        }
        if (!cid) {
          throw new BadRequestException('삭제할 카드 ID를 입력해주세요.');
        }
        const remove = await this.cardRepository.delete(cid);
        // if (remove.affected === 0) {
        //   throw new NotFoundException(`해당 카드가 조회되지 않습니다. cardId: ${cid}`);
        // }
      }
    
      // 5. 작업자 할당/변경
      async updateManager(cid: number, managerDto: ManagerDto) {
        const { manager, newManager } = managerDto;
        const managerCid = await this.cardRepository.findOne({ where: { cid } });
        if (!managerCid || managerCid == undefined) {
          throw new NotFoundException('해당 카드가 존재하지 않습니다.');
        } else if (!manager || !newManager) {
          throw new BadRequestException('미기입된 항목이 있습니다. 모두 입력해주세요.');
        }
        managerCid.manager = newManager;
        const update = this.cardRepository.save(managerCid);
        return update;
      }
    
      // 6. 마감일 수정
      async updateDeadline(cid: number, deadlineDto: DeadlineDto) {
        const { deadline, NewDeadline } = deadlineDto;
        const deadlineCid = await this.cardRepository.findOne({ where: { cid } });
        if (!deadlineCid || deadlineCid == undefined) {
          throw new NotFoundException('해당 카드가 존재하지 않습니다.');
        } else if (!deadline || !NewDeadline) {
          throw new BadRequestException('미기입된 항목이 있습니다. 모두 입력해주세요.');
        }
    
        deadlineCid.deadline = NewDeadline;
        const update = this.cardRepository.save(deadlineCid);
        return update;
      }
    
      // 7. 카드 순서변경
      async changeCards(lid: number, newlid: number, cid: number, newPosition: number): Promise<void> {
        const list = await this.listRepository.findOne({ where: { lid } });
        const cardq = await this.cardRepository.findOne({ where: { cid } });
    
        // 유효성 검사
        if (!list || list == undefined) {
          throw new NotFoundException('List ID가 존재하지 않습니다.');
        } else if (!cardq || cardq == undefined) {
          throw new NotFoundException('Card ID가 존재하지 않습니다.');
        } else if (!cid || !newPosition) {
          throw new BadRequestException('미기입된 항목이 있습니다. 모두 입력해주세요.');
        }
        const cards = await this.cardRepository.find({ where: { lid } });
        const card = cards.find((c) => c.cid === cid);
        // card  :카드 레포cid worker    cards 카드 레포 , lid
        const min = Math.min(card.order, newPosition);
        const max = Math.max(card.order, newPosition);
    
        await this.dataSource.manager.transaction(async (transactionManager) => {
          if (lid == newlid) {
            // 같은 컬럼일경우
            if (card.order === min) {
              cards
                .filter((card) => {
                  return card.order >= min && card.order <= max;
                })
                .forEach(async (card) => {
                  card.order = card.order - 1;
                  const a = await transactionManager.save(card);
                  return a;
                });
              card.order = max;
              return await transactionManager.save(card);
            } else if (card.order === max) {
              cards
                .filter((card) => {
                  return card.order <= max && card.order >= min;
                })
                .forEach(async (card) => {
                  card.order = card.order + 1;
                  await transactionManager.save(card);
                });
              card.order = min;
              return await transactionManager.save(card);
            }
          } else {
            // 다른 컬럼일경우
            cards
              .filter((card) => {
                return card.order >= newPosition;
              })
              .forEach(async (card) => {
                card.order = card.order + 1;
                await transactionManager.save(card);
              });
            cards
              .filter((val) => {
                return val.order > card.order;
              })
              .forEach(async (card) => {
                card.order = card.order - 1;
                await transactionManager.save(card);
              });
            card.order = newPosition;
            const updateCnt = await transactionManager.save(card);
            return updateCnt;
          }
        });
      }
    }

     

    cards 엔티티

     

    import { ManyToOne, Generated, BeforeInsert, JoinColumn, BaseEntity, UpdateDateColumn, CreateDateColumn, Column, Entity, Unique, OneToMany, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
    import { Comments } from '../Comments/comments.entity';
    import { Lists } from '../Lists/lists.entity';
    import { Users } from 'src/Users/users.entity';
    import { CardManagers } from 'src/CardManager/card-manager.entity';
    
    @Entity()
    @Unique(['cid']) // cardId 고유값 지정
    export class Cards extends BaseEntity {
      @PrimaryGeneratedColumn()
      cid: number;
    
      @Column()
      lid: number;
    
      @Column()
      title: string;
    
      @Column()
      color: string;
    
      @Column({ default: 1 })
      order: number;
    
      @Column()
      explanation: string;
    
      @Column()
      deadline: string;
    
      @Column()
      manager: string;
    
      @CreateDateColumn()
      createdAt: Date;
    
      @UpdateDateColumn()
      updatedAt: Date;
    
      // Cards-Lists : N:1 관계
      @ManyToOne(() => Lists, (lists) => lists.cards)
      @JoinColumn({ name: 'lid' })
      lists: Lists[];
    
      // Cards-Comments : 1:N 관계
      @OneToMany(() => Comments, (comments) => comments.cards)
      comments: Comments[];
    
      // Card - User : N:1 관계
      @ManyToOne(() => Users, (users) => users.cards)
      @JoinColumn({ name: 'uid' })
      users: Users[];
    
      // Card - card 매니저 : 1:N 관계
      @OneToMany(() => CardManagers, (cardManagers) => cardManagers.cards)
      cardManagers: CardManagers[];
    }

     


    리스트는 카드와는 달리, 보드 내에서 리스트의 이동의 경우만 생각하면 되었다. 하지만 또한, 보드 내에서 리스트가 바뀔 경우 해당 리스트 안에 있는 카드들의 리스트의 order도 변경시켜줘야 한다.

     

    import { Injectable, NotFoundException, UnauthorizedException, BadRequestException, InternalServerErrorException } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository, DataSource } from 'typeorm';
    import { Lists } from './lists.entity';
    import { CreateListsDto } from './dto/create-list.dto';
    import { UpdateListsDto } from './dto/update-list.dto';
    import { ListsDto } from './dto/order.dto';
    import { Boards } from 'src/Boards/boards.entity';
    import { Cards } from 'src/Cards/cards.entity';
    
    @Injectable()
    export class ListsService {
      constructor(
        @InjectRepository(Lists)
        private listsRepository: Repository<Lists>,
        @InjectRepository(Boards)
        private boardRepository: Repository<Boards>,
        @InjectRepository(Cards)
        private cardsRepository: Repository<Cards>,
        private readonly dataSource: DataSource,
      ) {}
    
      // 1. 리스트 전체 조회
      async getLists(bid: number): Promise<Lists[]> {
        const boards = await this.boardRepository.findOne({ where: { bid } });
        if (!boards || boards == undefined) {
          throw new NotFoundException('board ID가 존재하지 않습니다.');
        }
    
        const lists = await this.listsRepository.find({ where: { bid } });
        if (!lists || lists == undefined) {
          throw new NotFoundException('리스트 조회에 실패했습니다.');
        }
        return lists;
      }
      async findMaxOrder(bid: number): Promise<number> {
        const maxOrderRecord = await this.listsRepository.createQueryBuilder('list').select('MAX(list.order)', 'max_order').where('list.bid = :bid', { bid: bid }).getRawOne();
    
        return maxOrderRecord?.max_order || 0;
      }
    
      // 2. 리스트 생성
      async createList(bid: number, createListsDto: CreateListsDto) {
        const boards = await this.boardRepository.findOne({ where: { bid } });
    
        if (!boards || boards == undefined) {
          throw new NotFoundException('해당 보드가 존재하지 않습니다.');
        } else if (!createListsDto) {
          throw new BadRequestException('제목을 입력해주세요.');
        }
    
        const maxOrder = await this.findMaxOrder(bid);
    
        if (maxOrder == 0) {
          const data = await this.listsRepository.save({
            bid: bid,
            title: createListsDto.title,
          });
          return data;
        } else {
          const data = await this.listsRepository.save({
            bid: bid,
            title: createListsDto.title,
            order: maxOrder + 1,
          });
          return data;
        }
      }
    
      // 3. 리스트 수정
      async updateList(bid: number, lid: number, updateListsDto: UpdateListsDto) {
        const board = await this.boardRepository.findOne({ where: { bid } });
        const list = await this.listsRepository.findOne({ where: { lid } });
        if (!board || board == undefined) {
          throw new NotFoundException('해당 보드가 존재하지 않습니다.');
        } else if (!list || list == undefined) {
          throw new NotFoundException('해당 리스트가 존재하지 않습니다.');
        }
        if (!updateListsDto) {
          throw new BadRequestException('수정할 제목을 입력해주세요.');
        }
        try {
          const update = await this.listsRepository.update({ lid }, { title: updateListsDto.title });
          const result = await this.listsRepository.findOne({ where: { lid } });
          return result;
        } catch (error) {
          throw new Error('리스트 수정에 실패하였습니다.');
        }
      }
    
      // 4. 리스트 삭제
      async deleteList(bid: number, lid: number): Promise<void> {
        const board = await this.boardRepository.findOne({ where: { bid } });
        const list = await this.listsRepository.findOne({ where: { lid } });
        if (!board || board == undefined) {
          throw new NotFoundException('해당 보드가 존재하지 않습니다.');
        } else if (!list || list == undefined) {
          throw new NotFoundException('해당 리스트가 존재하지 않습니다.');
        }
    
        const remove = await this.listsRepository.delete(lid);
        if (remove.affected === 0) {
          throw new NotFoundException(`해당 리스트가 조회되지 않습니다. listId: ${lid}`);
        }
      }
      
      // 5. 리스트 순서변경
      async changeLists(bid: number, lid: number, newPosition: number): Promise<void> {
        const board = await this.boardRepository.findOne({ where: { bid } });
        const list = await this.listsRepository.findOne({ where: { lid } });
        // 유효성
        if (!board || board == undefined) {
          throw new NotFoundException('Board ID가 존재하지 않습니다.');
        } else if (!list || list == undefined) {
          throw new NotFoundException('List ID가 존재하지 않습니다.');
        } else if (!lid || !newPosition) {
          throw new BadRequestException('미기입된 항목이 있습니다. 모두 입력해주세요.');
        }
    
        const lists = await this.listsRepository.find({ where: { bid } });
        const listorder = lists.find((c) => c.lid === lid);
        const entityToUpdate = await this.cardsRepository.findOne({ where: { lid: lid } });
        // card  :카드 레포cid worker    cards 카드 레포 , lid
        const min = Math.min(list.order, newPosition);
        const max = Math.max(list.order, newPosition);
    
        await this.dataSource.manager.transaction(async (transactionManager) => {
          if (listorder.order === min) {
            lists
              .filter((list) => {
                return list.order > min && list.order <= max;
              })
              .forEach(async (list) => {
                list.order = list.order - 1;
                return await transactionManager.save(list);
              });
            list.order = max;
            return await transactionManager.save(list);
          } else if (list.order === max) {
            lists
              .filter((list) => {
                return list.order <= max && list.order >= min;
              })
              .forEach(async (list) => {
                list.order = list.order + 1;
                await transactionManager.save(list);
              });
            list.order = min;
            return await transactionManager.save(list);
          }
        });
      }
    }

     

    리스트 엔티티

     

    import { Entity, Unique, BaseEntity, BeforeInsert, UpdateDateColumn, OneToMany, CreateDateColumn, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, RelationId } from 'typeorm';
    import { Boards } from '../Boards/boards.entity';
    import { Cards } from '../Cards/cards.entity';
    
    @Entity()
    @Unique(['lid']) // listId 고유값 지정
    export class Lists extends BaseEntity {
      @PrimaryGeneratedColumn()
      lid: number;
    
      @Column()
      @RelationId((list: Lists) => list.boards)
      bid: number;
    
      @Column({ default: 1 })
      order: number; // 리스트 순서
    
      @Column()
      title: string;
    
      @CreateDateColumn()
      createdAt: Date;
    
      @UpdateDateColumn()
      updatedAt: Date;
    
      // @BeforeInsert() // position 1씩 자동증가
      // async setPosition() {
      //     if(!this.position){
      //     const lastList = await Lists.findOne({
      //         order: {
      //             position: 'DESC'
      //         }
      //     })
      //     this.position = lastList ? lastList.position + 1 : 1;
      // }
      // }
    
      // Lists : Boards = N:1 관계
      @ManyToOne(() => Boards, (boards) => boards.lists)
      @JoinColumn({ name: 'bid' })
      boards: Boards;
    
      // Lists : Cards = 1:N 관계
      @OneToMany(() => Cards, (cards) => cards.lists)
      cards: Cards;
    }

     

    order : int

     

    1. 같은 컬럼 사이의 이동

    이동하려는 카드 위치 와 제일 끝 카드 순서 를 대비했을때 

    가장 작은 값을 min , 큰값을 max 로 두고 

    - 이동하려는 카드 위치가 현재카드 위치보다 클 경우 -> 순서값에 -1

    - 이동하려는 카드 위치가 현재 카드 위치보다 작을 경우 -> 순서값에 +1

     

    2. 다른 컬럼 사이의 이동 

    이동하려는 카드 위치 와 제일 끝 카드 순서 를 대비했을때

    가장 작은 값을 min , 큰값을 max 로 두고 

    - 이동하려는 카드 위치가 현재카드 위치보다 클 경우 -> 순서값에 -1

    - 이동하려는 카드 위치가 현재 카드 위치보다 작을 경우 -> 순서값에 +1

     

    식으로 로직을 구현했다 

     

     

     


     

    최종 깃

    https://github.com/sangwoorhie/trello_sangwoo

     

    GitHub - sangwoorhie/trello_sangwoo: TypeScript/Nest.js/MySQL/AWS

    TypeScript/Nest.js/MySQL/AWS. Contribute to sangwoorhie/trello_sangwoo development by creating an account on GitHub.

    github.com

     

    댓글

Designed by Tistory.