카테고리 없음

내일배움캠프 14주차 월요일 TIL

news0516 2025. 1. 27. 19:56

금일은 작성하던 List 쪽 구성을 그대로 진행하였는데, 변경사항에  따라 진행하였다.

변경점 1. 조회 메서드의 경우 최상위 개념은 보드에서 모든 처리를 담당하도록 확인하였기 때문에 List 구성 중엔 조회 메서드 제외
변경점 2. 순서 변경에 따른 추가 메서드, 검증, DTO 파일 추가 구성

기존의 업데이트 메서드를 title 업데이트 메서드, position 업데이트 메서드로 나누어 따로 적용하였다.
클라이언트에서 처리 후 결과를 전달하고, 이를 받아 서비스 계층에서 비즈니스 로직에 따른 처리를 진행하는 것을 가정하고 진행하였으며, 이에따라 서버 측의 트랜잭션 처리는 상정하지 않았다.

// src/lists/dto/update-list-positions.dto.ts
import { IsInt, IsNotEmpty, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

class ListPositionUpdate {
  @IsInt()
  @IsNotEmpty()
  id: number;

  @IsInt()
  @IsNotEmpty()
  position: number;
}

export class UpdateListPositionsDto {
  @IsInt()
  @IsNotEmpty()
  boardId: number;

  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => ListPositionUpdate)
  lists: ListPositionUpdate[];
}


앞서 말했듯 클라이언트에서 위치변경에 따른 처리 결과를 서버에 전달하는 형태이기 때문에, UpdateListPositionsDto에서 전체 요청의 구조를 검증하고, ListPositionUpdate에서 Lists 배열 내의 각 요소의 구조를 검증하는 형태로 구성하였다. 

이후 컨트롤러 계층에서 조회 메서드는 일단 제외하고, 업데이트 메서드를 두가지로 나누었다.

// src/lists/lists.controller.ts
import {
  Controller, Post, Body, Patch, Param, Delete, Put, } from '@nestjs/common';
import { ListsService } from './lists.service';
import { CreateListDto } from './dto/create-list.dto';
import { UpdateListDto } from './dto/update-list.dto';
import { UpdateListPositionsDto } from './dto/update-list-positions.dto';

@Controller('lists')
export class ListsController {
  constructor(private readonly listsService: ListsService) {}

  // 리스트 생성
  @Post()
  async create(@Body() createListDto: CreateListDto) {
    return this.listsService.create(createListDto);
  }

  //특정 리스트 업데이트
  @Patch(':id')
  async update(@Param('id') id: string, @Body() updateListDto: UpdateListDto) {
    return this.listsService.update(+id, updateListDto);
  }

  //특정 리스트 삭제
  @Delete(':id')
  async remove(@Param('id') id: string) {
    return this.listsService.remove(+id);
  }

  // 리스트 위치 업데이트
  @Patch()
  async updatePositions(
    @Body() updateListPositionsDto: UpdateListPositionsDto,
  ) {
    return this.listsService.updatePositions(updateListPositionsDto);
  }
}

 

// src/lists/lists.service.ts
import {
  Injectable,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { List } from './entities/list.entity';
import { CreateListDto } from './dto/create-list.dto';
import { UpdateListDto } from './dto/update-list.dto';
import { UpdateListPositionsDto } from './dto/update-list-positions.dto';

@Injectable() // 주입 가능
export class ListsService {
  constructor(
    @InjectRepository(List)
    private listsRepository: Repository<List>,
  ) {}

  async create(createListDto: CreateListDto): Promise<List> {
    const { boardId, title } = createListDto;

    // 보드 id로 리스트 조회
    const lists = await this.listsRepository.find({
      where: { boardId },
      select: ['position'],
    });
    // const lists = [
    //   { position: 1 },
    //   { position: 2 },
    //   { position: 3 },
    // ];

    // 최대 포지션 찾기
    const maxPosition =
      lists.length > 0 ? Math.max(...lists.map((list) => list.position)) : 0;
    // 1. 맵 함수를 통해 position 값만 추출한 새로운 배열 생성
    // 2. Math.max(...arrays) : 배열의 모든 요소는 개별 인자로 전달, 그중 최대값 구함
    // 3. 삼항연상자 형태 >> 배열 길이가 0이라면 >> 아직 리스트가 없다면 maxPosition은 0
    // 배열 길이가 0 이상이라면 >> 기존 리스트가 있다면 maxPosition은 배열 중 position 최대값

    // maxPosition에 +1 하여 최종 포지션 결정
    const newPosition = maxPosition + 1;

    // 리스트 엔티티 생성
    const list = this.listsRepository.create({
      boardId,
      position: newPosition,
      title,
    });

    // 리스트 저장 및 반환
    return this.listsRepository.save(list);
  }

  // 리스트 업데이트(파라미터로 id 받음)
  async update(id: number, updateListDto: UpdateListDto): Promise<List> {
    const list = await this.listsRepository.findOne({ where: { id } });

    if (!list) {
      throw new NotFoundException(`리스트를 찾을 수 없습니다.`);
    }

    const { title } = updateListDto;

    // title 업데이트가 있는 경우
    if (title !== undefined) {
      list.title = title;
    }

    return this.listsRepository.save(list);
  }

  // 특정 리스트 삭제(파라미터로 id 받음)
  async remove(id: number): Promise<void> {
    const list = await this.listsRepository.findOne({ where: { id } });

    if (!list) {
      throw new NotFoundException(`List with ID ${id} not found`);
    }

    await this.listsRepository.remove(list);
  }

  // 위치 변경 업데이트
  // 포지션은 무조선 1부터 시작, 연속적으로 존재해야함.
  // ex) 1,2,3,4 >> O , 1,2,4 >> X
  async updatePositions(
    updateListPositionsDto: UpdateListPositionsDto,
  ): Promise<void> {
    const { boardId, lists } = updateListPositionsDto;
    // const lists = [
    // { "id": 1, "position": 1 },
    // { "id": 2, "position": 2 },
    // { "id": 3, "position": 3 }
    // ];

    // 보드 id로 리스트 조회
    const DBLists = await this.listsRepository.find({
      where: { boardId },
      select: ['id', 'position'],
    });

    // 검증 1: 클라이언트 전송 리스트, 실제 보드에 속한 리스트 수 비교 검증
    if (lists.length !== DBLists.length) {
      throw new BadRequestException(
        '전송된 리스트의 수가 보드에 속한 리스트의 수와 일치하지 않습니다.',
      );
    }

    // 검증 2: 클라이언트 제공 리스트 ID가 실제로 지정된 보드에 속해있는지 검증

    // DBLists 배열의 list 객체에서 id만 추출하여 새로운 배열 생성
    const DBIds = DBLists.map((list) => list.id);

    // lists(클라이언트가 전달한) 배열의 list 객체에서 id만 추출하여 새로운 배열 생성
    const providedIds = lists.map((list) => list.id);

    // array.every() = 배열의 모든 요소가 조건을 만족하는지 확인하여 만족 시 T or F 반환
    // Array.includes() = 배열에 특정 요소가 포함되어 있는지 확인하여 존재 시 T or F 반환
    // providedIds의 모든 id가 DBIds의 id에 포함되는지 전부 확인 >> allIdsMatch
    const allIdsMatch = providedIds.every((id) =>
      DBIds.includes(id),
    );

    if (!allIdsMatch) {
      throw new BadRequestException(
        '일부 리스트 ID가 지정된 보드에 속해 있지 않습니다.',
      );
    }

    // 검증 3: 포지션 순서 확인 (1, 2, 3, ...)
    // 포지션이 1부터 시작하여 연속적인지 확인
    for (let i = 0; i < lists.length; i++) {
      if (lists[i].position !== i + 1) {
        throw new BadRequestException('잘못된 요청입니다');
      }
    }

    // 위치 업데이트
    const updatePromises = lists.map((list) =>
      this.listsRepository.update(list.id, { position: list.position }),
    );

    await Promise.all(updatePromises);
  }
}



금일 작업, 배운 내용

  • updatePositions 메서드 구현
    • 트랜잭션 없이(클라이언트에서 서버측의 동시작업이 필요없도록 처리) 리스트 포지션 업데이트
  • DTO 구조 설계
    • ListPositionUpdate 클래스
    • UpdateListPositionsDto 클래스
    • 두 클래스 구성, 역할 설정
  • 전달값과 DB 값 비교, 포지션 순서 유효성 검사 추가
    • 기존 DB와 클라이언트 전달값 검증 중 필수적이라 생각하는 로직 먼저 업데이트 메서드에 적용
    • map과 every 메서드를 활용한 간결한 방식
  • map과 every 메서드 상세 설명 주석 추가
    • Array.map()
    • Array.every()
    • 두 메서드를 결합하여 로직 양 줄여 적용


추가적인 모듈, 테스트 코드 작성 후 테스트 하여 이번주까지 API 완성을 목표로 개발 예정