문제의 발생

우리 프로젝트 팀은 프론트엔드 개발자 3명, 백엔드 개발자 1명, 디자이너 1명으로 구성되어 있었다. 부족하지만 완전한 이 팀은 기획자가 없었는데, 스타트업이라는 특성을 생각하면 크게 이상한 일은 아니다 그래서 장바구니 화면도 디자이너와 개발자들이 직접 머리를 맞대며 구성했고 결국 클라이언트의 요구사항 + 우리들의 아이디어가 합쳐져 아래와 같은 화면을 만들었다.

스크린샷 2024-07-08 16.33.55.png

스크린샷 2024-07-08 16.33.10.png


문제에 접근 하기 문제의 발생2

우선 유저가 장바구니에 음식들을 담은 후 가능한 플로우를 생각해보자.

A.음식의 수량을 변경한다 → 플러스 마이너스 버튼을 사용 B.담은 음식을 삭제한다 → x 버튼을 사용해서 제거 C.더 음식을 더 담는다. → 화면에는 없지만 제일 하단에 있는 음식 더 담기 버튼 클릭 D.음식의 옵션을 변경한다. → 옵션 변경 버튼 클릭

여기 까지는 구현에 문제가 없었다. ‘이게 끝인가?’ 라고 생각하면 문제는 항상 등장한다.

예를 들면 D에서, 장바구니에 삼계탕 (대) 삼계탕 (소) 각 1개가 담겨 있는 상황에서 소를 대로 변경을 하면…. 장바구니에는 동일한 음식 두 개가 표시된다.

다행스럽게도 QR 코드를 찍어서 주문을 하던 배달을 시키던 결국에 어느 정도 비즈니스 로직은 비슷하고 특히 한국이라면 참조할 플랫폼은 많다. 평소에 아무 생각없이 사용했던 배달 어플들을 열어 장바구니가 어떻게 담기는지 확인을 해본다.

<aside> 💡

AH-HA moment: 아이템이 중복이면 아이템을 합치고 수량과 총 금액을 더한다

</aside>

생각보다 간단하다. 하지만 이제는 로직을 만들어야 한다.


문제의 해결

우선 아이템이 중복인지 감지를 해야된다. 아이템은 아래 구조처럼 담기고 주석 처리된 부분 처럼 결과가 출력되어야 한다.

const items = [
  {
    menu_id: "1",
    name: "삼계탕",
    option_list: [{ option_id: 101 }, { option_id: 102 }],
    count: 1,
    sum: 32,
  },
  {
    menu_id: "1",
    name: "삼계탕",
    option_list: [{ option_id: 101 }, { option_id: 102 }],
    count: 1,
    sum: 32,
  },
  {
    menu_id: "2",
    name: "베트남 연유 커피",
    option_list: [{ option_id: 201 }],
    count: 3,
    sum: 4,
  },
];

// 결과:
// [
//   { menu_id: "1", name: "삼계탕", option_list: [...], count: 2, sum: 64 },
//   { menu_id: "2", name: "베트남 연유 커피", option_list: [...], count: 1, sum: 4 }
// ]

로직을 구현하기 위한 조건

  1. 어떤 메뉴를 합칠지 정해져야 한다. → unique한 값을 만든다.
  2. unique한 값을 중복 없이 저장한다. → map 구조를 사용한다.
  3. 중복 메뉴의 값과 수량을 합친다. → map 구조는 중복 key 값을 안 받으므로 if문 처리
// 중복 항목을 제거하고 동일한 항목의 count와 sum 값을 누적하는 함수
export const useRemoveDuplicateItem = (items) => {
  // 중복을 확인하기 위해 사용되는 Map 객체
  const seenItems = new Map();

  // items 배열을 순회하며 중복을 제거한 배열을 생성
  return items.reduce((uniqueItems, item) => {
    // 각 항목의 option_list에서 option_id를 문자열로 변환한 배열 생성
    const orderMenuOptionId = item.option_list.map((option) =>
      JSON.stringify(option.option_id)
    );

    // option_id 배열을 숫자로 변환 후 내림차순 정렬
    const descendingMenuOptionId = orderMenuOptionId
      .map(Number)
      .sort((a, b) => b - a);

    // 항목의 고유 키 생성 (menu_id, name, 정렬된 option_id 배열을 조합)
    const itemKey = `${item.menu_id}-${item.name}-${JSON.stringify(
      descendingMenuOptionId
    )}`;

    // 고유 키가 Map에 존재하지 않는 경우
    if (!seenItems.has(itemKey)) {
      // Map에 항목을 추가하고 count와 sum 기본값을 설정
      seenItems.set(itemKey, {
        ...item, // 기존 항목의 모든 속성 복사
        count: item.count || 1, // count가 없으면 기본값 1
        sum: item.sum || 1, // sum이 없으면 기본값 1
      });
      // 고유 항목 배열에 추가
      uniqueItems.push(seenItems.get(itemKey));
    } else {
      // 이미 존재하는 경우 count와 sum 값을 누적
      seenItems.get(itemKey).count += item.count;
      seenItems.get(itemKey).sum += item.sum;
    }

    // 중복 제거 및 누적 처리된 배열 반환
    return uniqueItems;
  }, []); // 초기값은 빈 배열
};