마이페이지_진척률 보여주기(깊은비교, 깊은정렬을 사용한 객체비교)

정소현·2024년 11월 14일
1

팀프로젝트

목록 보기
42/50
post-thumbnail

내가 맡았던 마이페이지 부분에서 청첩장 제작이 얼마까지 완료되었는지 사용작에게 제공해주는 것이 유저가 계속해서 진행하는데에 도움이 될 것 같아 디자인이 변경이 되었다.

☘️ 1. 진척률 구현계획

원형프로그래스바가 채워지는 형태로 진행을 하고
팀원들과 완성의 depth을 어디기준으로 잡아야 하나라는 이야기를 나누었다.
지금 우리 데이터는 큰 객체 Form안에 갤러리정보, 글꼴 정보, 예식장 정보 등 데이터를 저장하고 있다.
글꼴 정보를 받는 폼 안에서도 글꼴 색상, 폰트선택을 담은 값들이 있지만 depth를 이 깊이까지 잡게 된다면 많이 복잡해질 수 있다고 생각을 하였다. 갤러리정보, 글꼴 정보와 같이 조금 더 큰 단위에서 formdata에 기본값이 들어오지 않는다면 진척이 되고 있는 것으로 판단을 하여 진척도를 나타낼 수 있도록 구현하려고 한다.

🌼 고려해야 할 사항

  1. 청첩장 Form안에는 bgColor, type등 사용자가 기본값으로 default값을 선택할 수 있는 Field도 있었다.
    그래서 팀원들과 함꼐 사용자가 입력을 꼭 해야만 하는 필드에 대해 이야기하고 8개를 지정하였다.

ㅁ 사용자가 입력해야하는 필드

  • personal_info
  • account
  • wedding_info
  • main_photo_info
  • navigation_detail
  • gallery
  • greeting_message
  • font_info
  1. 지정한 필드는 총 8개로 1개 필드가 완성될 떄 마다 완성도가 올라가야하는데 12.5%로 나누어 떨어졌다. 이 부분을 소수점을 반올림하여 처리하기로 결정하였다.
  2. 데이터의 각 필드들은 depth가 달랐기 떄문에 깊은 비교로 2,3단계의 depth를 가진 필드들의 내부까지 변화를 찾을 수 있도록 설정해줘야했다.

🌟 개발과정

formdata 기본값을 가져오고 , supabase에서 청첩장 ID를 비교하여 청첩장 데이터를 가져오는 로직을 먼저 작성했다.
id를 받아 id에 맞게 등록되어있는 데이터를 가져왔다.

export const fetchInvitationFields = async (id: string) => {
  const { data } = await browserClient
    .from('invitation')
    .select('*')
    .eq('user_id', id)
    .order('created_at', { ascending: false });

  return data?.[0];
};

<기존 데이터 default값>

const DEFAULT_VALUE = {
  bg_color: { r: 255, g: 255, b: 255, a: 1, name: '흰색' },
  personal_info: {
    bride: {
      name: '',
      relation: '',
      phoneNumber: '',
      father: { name: '', relation: '', phoneNumber: '', isDeceased: false },
      mother: { name: '', relation: '', phoneNumber: '', isDeceased: false },
    },
    groom: {
      name: '',
      relation: '',
      phoneNumber: '',
      father: { name: '', relation: '', phoneNumber: '', isDeceased: false },
      mother: { name: '', relation: '', phoneNumber: '', isDeceased: false },
    },
  },
  account: {
    title: '',
    content: '',
    bride: [
      { bank: '', accountNumber: '', depositor: '', kakaopay: '' },
      { bank: '', accountNumber: '', depositor: '', kakaopay: '' },
      { bank: '', accountNumber: '', depositor: '', kakaopay: '' },
    ],
    groom: [
      { bank: '', accountNumber: '', depositor: '', kakaopay: '' },
      { bank: '', accountNumber: '', depositor: '', kakaopay: '' },
      { bank: '', accountNumber: '', depositor: '', kakaopay: '' },
    ],
  },
  guestbook: true,
  attendance: false,
  wedding_info: {
    date: '2024.11.22',
    time: { hour: '오전 00', minute: '00' },
    weddingHallAddress: '',
    weddingHallName: '',
    weddingHallContact: '',
  },
  main_photo_info: {
    leftName: '',
    rightName: '',
    icon: '♥︎',
    introduceContent: '',
    imageUrl: '',
  },
  navigation_detail: {
    map: false,
    navigationButton: false,
    subway: '',
    bus: '',
  },
  gallery: {
    images: [],
    grid: 3,
    ratio: 'square',
  },
  type: 'scroll',
  mood_preset: {
    mood: 'classic',
    preset: {
      name: 'preset1',
      label: '프리셋 1',
      image: '/assets/images/presets/classicPreset1.svg',
    },
  },
  stickers: [],
  img_ratio: {
    ratio: '',
    position: 0,
  },
  greeting_message: {
    title: '',
    content: '',
  },
  d_day: true,
  main_view: {
    name: '기본',
    type: 'default',
  },
  isPrivate: false,
  render_order: extractOrderAndType(),
  font_info: {
    size: 0,
    fontName: 'Main',
    color: {
      r: 0,
      g: 0,
      b: 0,
      a: 100,
      name: '커스텀',
    },
  },
};

  • 위의 고려사항들을 참고해 체크해야하는 필드를 선택해주고 for of 를 사용하여 객체들을 반복하여 검사할 수 있도록 로직을 작성해주었다.
  • 반복하여 검색을 한 후에 큰 필드 안의 객체안의 값들이 하나라도 달라졌으면 그 필드는 완성이 된 것으로 판단하여 completedFields에 +1을 해주었다.
  • 검사한 필드의 개수 / 전체 필드 를 하여 % 로 환산해주고 return 해주었다.
  • deepEqual이라는 함수를 별도로 만들어 객체의 depth가 2,3단계인 필드들도 모두 검사할 수 있도록 로직을 작성해주었다.
  • lodash의 isEqual 라이브러리를 활용하여 깊은 비교를 실행했다.
// 깊은 복사를 위한 deepEquals 함수
function deepEquals(target1: unknown, target2: unknown): boolean {
 
  return isEqual(sortedTarget1, sortedTarget2);
}
const calculateProgress = (supabaseData: Record<string, unknown>, defaultValue: Record<string, unknown>): number => {
  let completedFields = 0;

  const fieldsToCheck = [
    // 체크해야하는 필드
    'personal_info',
    'account',
    'wedding_info',
    'main_photo_info',
    'navigation_detail',
    'gallery',
    'greeting_message',
    'font_info',
  ];

  // 업로드 되어있는 데이터와 기본값 데이터를 반복하여 검사
  for (const field of fieldsToCheck) {
    const fieldData = supabaseData?.[field];
    const defaultFieldData = defaultValue?.[field];

    if (deepEquals(fieldData, defaultFieldData)) {
    } else {
      completedFields += 1;
    }
  }

  const totalFields = fieldsToCheck.length;

  const progressPercentage = Math.min(Math.round((completedFields / totalFields) * 100), 100);

  return progressPercentage;
};
  • 마지막으로 supabase에 저장되어있는 청첩장 데이터가 없을 때는 0%를 반환하고 아닐경우에는 계산을 할 수 있도록 로직을 작성해주었다.
export const calculateProgressPercentage = async (id: string): Promise<number> => {
  const supabaseData = await fetchInvitationFields(id);
  if (!supabaseData) {
    return 0;
  }
  return calculateProgress(supabaseData, DEFAULT_VALUE);
};

🔥 트러블 슛팅


💥 문제상황
: 이렇게 처리를 해주었음에도 여전히 데이터들을 제대로 비교하지 못하고 있었다. 진척률에 영향을 주는 필드들에 데이터를 넣고 supabase에서 들어오는 데이터와 기본값들을 비교해보았다. supabase와 기본값에서 보이는 객체 key들의 순서가 달랐고 이로 인해 제대로 된 비교가 되지 않았던 것이었다.

👀 고려했던 방법

  • JSON.stringify() & Lodash의 isEqual()을 사용하여 객체 문자열로 변환 후 일치 연산자(===)를 통해 비교
    => 선택하지 않은 이유
    : 키-값 쌍의 순서가 다르면 같은 객체로 취급되지 않는다.
  • lodash의 isEqual 함수를 사용하여 비교
    => 부족한 이유
    : 객체 비교 시 속성의 순서가 달라도 값이 동일하다면 두 객체가 판단하지만 배열에서의 isEqual은 순서를 고려한다.따라 배열이 다르게 들어온다면 두개의 객체가 같지않다고 판단
  • 깊은 정렬 + lodash isEqual(깊은 비교) 를 함께 사용하기로 결정

🌟 해결방법

  • 깊은 비교를 하기 전 객체의 데이터들을 알파벳순으로 내부 값까지 깊은 정렬을 할 수 있는 코드를 작성해주었다.
  • 각 필드의 depth마다 객체인 곳도 있고 배열인 곳도 존재했기 때문에 두가지 상황을 만들고 그에 따른 수행을 할 수 있도록 로직을 작성했다.
function deepSort(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(deepSort);
  }

  if (value && typeof value === 'object' && value.constructor === Object) {
    const sortedObj: Record<string, unknown> = {};
    Object.keys(value)
      .sort()
      .forEach((key) => {
        sortedObj[key] = deepSort((value as Record<string, unknown>)[key]);
      });

    return sortedObj;
  }

  return value;
}

깊은 정렬 후 깊은 비교를 할 수 있도록 수정

function deepEquals(target1: unknown, target2: unknown): boolean {
  if (target1 === undefined) {
    return false;
  }

  const sortedTarget1 = deepSort(target1);
  const sortedTarget2 = deepSort(target2);

  return isEqual(sortedTarget1, sortedTarget2);
}

☘️ 최종 완성된 전체로직

function deepSort(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(deepSort);
  }

  if (value && typeof value === 'object' && value.constructor === Object) {
    const sortedObj: Record<string, unknown> = {};
    Object.keys(value)
      .sort()
      .forEach((key) => {
        sortedObj[key] = deepSort((value as Record<string, unknown>)[key]);
      });

    return sortedObj;
  }

  return value;
}

function deepEquals(target1: unknown, target2: unknown): boolean {
  if (target1 === undefined) {
    return target1 === target2;
  }
  const sortedTarget1 = deepSort(target1);
  const sortedTarget2 = deepSort(target2);
  console.log('sortedTarget1', sortedTarget1);
  console.log('sortedTarget2', sortedTarget2);

  return isEqual(sortedTarget1, sortedTarget2);
}

export const fetchInvitationFields = async (id: string) => {
  const { data } = await browserClient.from('invitation').select('*').eq('user_id', id);
 
  return data?.[0];
};

const calculateProgress = (supabaseData: Record<string, unknown>, defaultValue: Record<string, unknown>): number => {
  let completedFields = 0;

  const fieldsToCheck = [
    'personal_info',
    'account',
    'wedding_info',
    'main_photo_info',
    'navigation_detail',
    'gallery',
    'greeting_message',
    'font_info',
  ];

  for (const field of fieldsToCheck) {
    const fieldData = supabaseData?.[field];
    const defaultFieldData = defaultValue?.[field];

    defaultFieldData);

    if (!deepEquals(fieldData, defaultFieldData)) {
      completedFields += 1;
      
    }
  }

  const totalFields = fieldsToCheck.length;

  const progressPercentage = Math.min(Math.round((completedFields / totalFields) * 100), 100);
  console.log('Completed Fields:', completedFields);
  console.log('Progress Percentage:', progressPercentage);

  return progressPercentage;
};

export const calculateProgressPercentage = async (id: string): Promise<number> => {
  const supabaseData = await fetchInvitationFields(id);
  if (!supabaseData) {
    return 0;
  }
  return calculateProgress(supabaseData, DEFAULT_VALUE);
};

✨ 마무리

원형 프로그래스바를
conic-gradient를 사용하여 퍼센트에 따라 색상이 채워지도록 설정해주었다.

 const gradientColor =
    progressPercentage !== null
      ? `conic-gradient(#ff6666 ${progressPercentage}%, #e0e0e0 0%)`
      : 'conic-gradient( #e0e0e0 0%, #FFFFFF 100%)';

 <div
                className='w-[96px] h-[96px] absolute inset-0 rounded-full flex justify-center items-center overflow-hidden'
                style={{
                  background: gradientColor,
                }}
              >
                <div className='w-[90%] h-[90%] overflow-hidden flex rounded-full'>
                  <Image
                    src={invitationCard.main_photo_info?.imageUrl || '/assets/images/defaultImg.png'}
                    alt='invitationImg'
                    width={90}
                    height={90}
                    className='rounded-full'
                  />
                </div>
              </div>

✨ 완성화면

0개의 댓글