BOOK_MAKER 개발기 #3 — 초안 내 기록 순서 변경 및 백엔드 연동 구현

사용자가 초안(Draft)을 구성할 때 기록들의 배치 순서를 조정하고 싶어 하는 니즈를 반영하여, 드래그 앤 드롭(또는 순서 변경 UI)을 통해 기록의 position 값을 업데이트하는 기능을 백엔드 서비스와 프론트엔드 컴포저블에 구현했습니다.


목표

  • 초안 상세 화면(/app/drafts/[id])에서 포함된 기록들의 순서를 변경하는 기능 구현
  • 백엔드 DraftsService에 순서 변경 API(PATCH /drafts/:id/entries/reorder) 로직 추가
  • 프론트엔드 useDrafts 컴포저블을 통한 상태 동기화 및 에러 핸들링 보강

핵심 구현 포인트

1) 백엔드: 효율적인 순서 업데이트 로직

DraftsService에서 초안 내 기록들의 순서를 일괄적으로 업데이트하는 기능을 구현했습니다. 요청 시 전달된 entryIds 배열의 인덱스를 기반으로 각 기록의 position 값을 재설정합니다. 이때, 기존 초안에 포함된 기록들과 요청된 기록들의 ID 셋(Set)이 일치하는지 검증하여 데이터 무결성을 보장했습니다.

// apps/api/src/drafts/drafts.service.ts
async reorderDraftEntries(
  userId: string,
  draftId: string,
  entryIds: string[],
): Promise<DraftDetailRecord> {
  // ... 검증 로직 생략 ...
  let position = 0;
  for (const entryId of entryIds) {
    position += 1;
    await this.databaseService.getPool().query(
      `UPDATE draft_entries SET position = $3
       WHERE draft_id = $1 AND entry_id = $2`,
      [draftId, entryId, position],
    );
  }
  // 초안의 updatedAt 갱신
  await this.databaseService.getPool().query(
    `UPDATE drafts SET updated_at = NOW() WHERE id = $1 AND user_id = $2`,
    [draftId, userId],
  );
  return this.findDraftById(userId, draftId);
}

2) 프론트엔드: useDrafts 컴포저블 확장

프론트엔드에서는 reorderDraftEntries 메서드를 추가하여 API 호출 결과를 반응형 상태(currentDraft)에 즉시 반영하도록 구성했습니다. 또한 reorderState를 통해 작업 진행 상태(Submitting, Error 등)를 관리하여 사용자에게 피드백을 제공합니다.

// apps/web/app/composables/useDrafts.ts
async function reorderDraftEntries(draftId: string, entryIds: string[]) {
  reorderState.value = 'submitting';
  try {
    const draft = await options.api.reorderDraftEntries(draftId, entryIds);
    currentDraft.value = draft;
    syncDraft(draft); // 목록 상태와 동기화
    reorderState.value = 'idle';
    return draft;
  } catch {
    reorderState.value = 'error';
    reorderError.value = '초안 순서를 바꾸지 못했습니다. 잠시 후 다시 시도해 주세요.';
    return null;
  }
}

트러블슈팅 / 고민 포인트

데이터 무결성 검증

  • 원인: 순서 변경 요청 시 일부 기록 ID가 누락되거나 초안에 없는 ID가 포함될 경우, 데이터의 일관성이 깨질 위험이 있었습니다.
  • 해결: hasSameEntrySet 유틸리티 함수를 만들어, 현재 DB에 저장된 초안 내 기록 ID 목록과 클라이언트가 보낸 목록을 비교하여 정확히 일치할 때만 업데이트를 수행하도록 방어 로직을 강화했습니다.

결과

  • 초안 상세 페이지에서 기록 순서 변경 및 실시간 데이터 반영 기능 구현 완료
  • NestJS 백엔드와 Nuxt 프론트엔드 간의 PATCH 메서드를 활용한 데이터 통신 구조 확립
  • 순서 변경 로직에 대한 백엔드 통합 테스트 및 프론트엔드 단위 테스트 케이스 추가

다음 개선 아이디어

  • 실제 드래그 앤 드롭 라이브러리(예: vuedraggable)를 연동하여 더 직관적인 UX 제공
  • 초안 내의 기록을 개별적으로 삭제하거나 다른 초안으로 이동하는 기능 추가
  • 대량의 기록이 포함된 초안의 경우, 순서 변경 시 DB 부하를 줄이기 위한 벌크 업데이트 방식 고민