Skip to content

[리팩토링] ReviewList 컴포넌트 SRP 위반 - 편집/삭제 상태 관리 훅 분리 및 모바일/태블릿 레이아웃 컴포넌트 분리 #281

@coderabbitai

Description

@coderabbitai

개요

src/components/base-ui/Bookcase/bookid/ReviewList.tsx는 한줄평 목록 렌더링 컴포넌트이지만, 편집 상태 관리, 삭제 확인 모달 상태, 키보드 이벤트 처리, 그리고 모바일/태블릿 두 개의 독립된 레이아웃을 하나의 컴포넌트에서 모두 처리하고 있어 SRP를 위반합니다.

관련 PR: #273

현재 문제점

책임 관련 코드
① 편집 상태 관리 editingId, draftText, draftRating, startEdit, cancelEdit, editingItem
② 삭제 확인 모달 상태 deleteTargetId, openDelete, closeDelete, confirmDelete
③ 키보드 이벤트 처리 useEffect([editingId])에서 Escape 키 감지 시 편집 취소
④ 모바일 레이아웃 t:hidden 블록 (그리드 레이아웃, 인라인 편집 폼)
⑤ 태블릿+ 레이아웃 hidden t:flex 블록 (플렉스 레이아웃, 별점 위치 다름)

제안하는 분리 방향

1. useReviewItemEdit (편집 상태 + 키보드 이벤트 전담)

// src/hooks/useReviewItemEdit.ts
// 역할: 현재 편집 중인 한줄평 ID, 임시 텍스트/별점 상태 관리 및 Escape 키 취소만 담당
export function useReviewItemEdit(): {
  editingId: ReviewItem['id'] | null;
  draftText: string;
  draftRating: number;
  setDraftRating: (v: number) => void;
  startEdit: (item: ReviewItem) => void;
  cancelEdit: () => void;
  isEditing: (id: ReviewItem['id']) => boolean;
}

2. useConfirmDelete (삭제 확인 모달 상태 전담 - 범용)

// src/hooks/useConfirmDelete.ts
// 역할: 삭제 대상 ID 저장, 모달 열기/닫기, 삭제 실행 콜백 호출만 담당
export function useConfirmDelete<T>(
  onDelete: (id: T) => void
): {
  deleteTargetId: T | null;
  openDelete: (id: T) => void;
  closeDelete: () => void;
  confirmDelete: () => void;
}

3. ReviewListItem (단일 한줄평 아이템 컴포넌트 분리)

// src/components/base-ui/Bookcase/bookid/ReviewListItem.tsx
// 역할: 단일 한줄평의 모바일/태블릿 레이아웃 렌더링만 담당
// 편집 상태와 삭제 상태는 props로 주입받음

type Props = {
  item: ReviewItem;
  isEditing: boolean;
  draftText: string;
  draftRating: number;
  canManage: boolean;
  onStartEdit: () => void;
  onCancelEdit: () => void;
  onConfirmEdit: (text: string, rating: number) => void;
  onOpenDelete: () => void;
  onReport: () => void;
  onClickAuthor: (name: string) => void;
  onChangeDraftRating: (v: number) => void;
};

export default function ReviewListItem(props: Props) { ... }

4. 개선 후 ReviewList

// ReviewList는 상태 조합 + 목록 반복만 담당
export default function ReviewList({ items, isStaff, onReport, onUpdate, onDelete, onClickAuthor }: Props) {
  const editState = useReviewItemEdit();
  const deleteState = useConfirmDelete(onDelete);

  return (
    <>
      <BookshelfDeleteConfirmModal
        isOpen={deleteState.deleteTargetId != null}
        onConfirm={deleteState.confirmDelete}
        onClose={deleteState.closeDelete}
        ...
      />
      {items.map((item) => (
        <ReviewListItem
          key={item.id}
          item={item}
          isEditing={editState.isEditing(item.id)}
          ...
        />
      ))}
    </>
  );
}

기대 효과

  • useConfirmDelete는 발제 목록(DebateList) 등 다른 삭제 확인이 필요한 곳에 재사용 가능
  • ReviewListItem이 순수 렌더링 컴포넌트가 되어 스냅샷 테스트 작성 용이
  • ReviewList 자체는 50줄 이하로 축소 가능

수정 대상 파일

  • src/components/base-ui/Bookcase/bookid/ReviewList.tsx
  • (신규) src/components/base-ui/Bookcase/bookid/ReviewListItem.tsx
  • (신규) src/hooks/useReviewItemEdit.ts
  • (신규) src/hooks/useConfirmDelete.ts

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions