챌린지 생성

먼지·2022년 10월 7일
1

TIL

목록 보기
36/57
post-thumbnail

챌린지 기능은 참고할 만한 사이트가 없어서 챌린저스 앱을 따라 만들었다. 거의 클론!이라 하기엔 내가 좀 하찮,,ㅜ 일단 만들고 세세한 건 나중에 수정해야겠다

챌린지 태그 선택

태그는 비건, 환경, 제로 웨이스트 등 총 7가지로
처음엔 input checkbox로 구현했는데 css 부분이 힘들어서 button으로 만들었는데 최대 3개만 선택할 수 있어서 조금 걸렸다. 왜냐면 3개가 다 선택되면 checked 값이 바뀌지 않게 만들었더니 이미 선택된 버튼 데이터의 체크 해제까지 되지 않았다.

다시 설명
그래서 setState에서 가져오는 이전 state 값을 map으로 돌 때 현재 선택되지 않은 데이터이고 체크된 개수가 2개 이하 (length가 -1임)이 true인지를 확인해 이전 데이터를 그대로 반환하고 아니면 이전 객체에서 checked 값만 반대된 데이터를 return 했다.

const tagsData = [
  { checked: false, value: 'ZERO_WASTE', name: '제로 웨이스트' },
  { checked: false, value: 'VEGAN', name: '비건' },
  { checked: false, value: 'LIFESTYLE', name: '라이프스타일' },
  { checked: false, value: 'TUMBLER', name: '텀블러' },
  { checked: false, value: 'RECYCLING', name: '재활용' },
  { checked: false, value: 'ENVIRONMENT_DAY', name: '환경의 날' },
  { checked: false, value: 'ETC', name: '기타' },
];

export default function Challenge() {
  const [tags, setTags] = useState(tagsData);
  
  const handleChageTags = (value) => {
    setTags((prev) => {
      const checkable = prev.filter((p) => p.checked).length <= 2;
      return prev.map((p) => {
        // 여기서 if문으로 체크한 이유는 단순히 3개가 됐을 때 막아버리면
        if (!p.checked && !checkable) return p;
        return p.value === value ? { ...p, checked: !p.checked } : p;
      });
    });
  };
  ...
  return (
    <>
      ...
        <div className="w-full mt-4">
          <label>
            <h2 className="font-bold">태그를 선택해주세요 (최대 3)</h2>
          </label>
          {tags.map((tag, i) => (
            <button
              key={i}
              type="button"
              name="tags"
              onClick={() => handleChageTags(tag.value)}
              value={tag.value}
              className={`border rounded-full py-1 px-2 mr-1 mt-2 text-sm ${
                tag.checked ? 'bg-black text-white' : ''
              }`}
            >
              {tag.name}
            </button>
          ))}
        </div>
      ...
    </>
  );
}

GIF?


시작일 계산?

  • 인증 빈도 (매일, 평일 매일, 주말 매일 주 1~6일)
  • 챌린지 기간 (1~4주)
    에 맞게 시작일과 예상 기간을 도출해야 한다.

챌린저스는 10.07 금요일을 기준으로

  • 인증 빈도를 매일(월~일), 기간을 1주 동안 선택했으면 시작일이 10.7 (금) ~ 13 (목) 까지 결과
  • 평일 매일 & n주 동안 => 10.7 (금)
  • 주말 매일 & n주 동안 => 10.8 (토)
    ...
    이런 식으로 결과가 나온다

우리는 공휴일에도 인증하는 방식으로 만들기로 해서 기간은 크게 중요하지 않은 것 같고..?

나는 일단 인증 빈도로 시작일을 계산하려는데 대충

  • 매일 / 내일부터 7일
  • 평일 매일 / 내일부터 주말 전
  • 주말 매일 / 가까운 주말
  • 주 n 일
    - 다음 주 월욜 (10.10, 다다음주?)
    이렇게 표시하려고 한다..

아마 이런 날짜 계산 라이브러리가 있는지도 모르겠고 라이브러리 없이 자바스크립트의 Date 객체를 이용해서 구현해 보고 싶은데 코드 작성 전에 대충 적어보자면

인증 빈도와 현재 날짜 데이터를 기준으로

  • 매일이면 date.getDay, Month 등의 메소드의 +1을 해서 뒤로 7일까지 표시하고
  • 평일 매일이면 위의 매일 결과랑 비슷한데 주말을 빼고 표시
  • 주말 매일이면 getDay() 반환값이 6, 0이 토 일이니까 가까운 토요일 일요일을 찾기
  • 주 n 일이면 ...!

구현 전에 해야할 것은 JavaScript Date 공부하기 (MDN)

구현 시작

매일

인증 빈도와 기간의 선택에 맞게는 시작일을 계산하는 함수가 완성되면 적용하려고 하고 getCurrentDate, getToday 등의 날짜 관련 함수들은 utils.ts로 뺐다

export function getToday(index: number) {
  return ['일', '월', '화', '수', '목', '금', '토'].find((_, i) => i === index);
}

export function getDay(year: number, month: number, date: number) {
  return new Date(year, month, date).getDay();
}

export function getCurrentDate() {
  const currDate = new Date();
  return {
    currDate,
    year: currDate.getFullYear(),
    month: currDate.getMonth(),
    date: currDate.getDate(),
    day: currDate.getDay(),
  };
}

export function getLastDate(year: number, month: number) {
  // 마지막 날짜 구하기 new Date(년, 월, 0)
  return new Date(year, month + 1, 0).getDate();
}

export function removeWeekend(year: number, month: number, days: number[]) {
  return days.filter(
    (n) => getDay(year, month, n) > 0 && getDay(year, month, n) < 6
  );
}

일단 기본으로 오늘로 부터 7일을 표시할 수 있게 new Array(7).fill('').map(...)을 return하고, 만약 현재 요일 + 7일(표시되야하는수) 했을 때 이번 달의 마지막 요일을 넘어간 경우엔 overflow 변수를 만들어서 [...넘어가지 않은 날 만큼, ...넘어간 날로 만든 다음 달 1일부터 배열] 을 반환하게끔 구현했다. 아직 12월 31일 넘어가서 다음 년이 됐을 때는 체크하지 못함..

function getChallengeStartDate(af = 'EVERYDAY') {
  // af [ EVERYDAY, WEEKDAY, WEEKEND, ONCEAWEEK, TWICEAWEEK, THIRDAWEEK, FORTHAWEEK, FIFTHAWEEK, SIXTHAWEEK ]
  // p [ ONEWEEK, TWOWEEK, THREEWEEK, FOURWEEK ]
  const { currDate, year, month, day } = getCurrentDate();
  const lastMonth = 11; // 말월
  const lastDate = getLastDate(); // 말일
  const currMon = month + 1;

  const date = 28;

  switch (af) {
    case 'EVERYDAY':
      // 문제1. lastDate가 넘었을 때
      // 그전에 현재 date + 7이 lastDate를 넘는지 체크
      // 그리고 month도 체크
      // 마지막 year
      const overflow = date + 7 > lastDate;
      const overflowDay = lastDate - (date + 7);
      return !overflow
        ? new Array(7).fill('').map(
            (_, i) =>
              // 28 29 30 31(lastDate) 1 2 3
              `${currMon}. ${date + i + 1} (${getToday(
                getDay(year, month, date + i + 1)
              )})`
          )
        : [
            ...new Array(7 - Math.abs(overflowDay)).fill('').map(
              (_, i) =>
                // 28 29 30 31(lastDate) 1 2 3
                `${currMon}. ${date + i + 1} (${getToday(
                  getDay(year, month, date + i + 1)
                )})`
            ),
            ...new Array(Math.abs(overflowDay))
              .fill('')
              .map(
                (_, i) =>
                  `${currMon + 1}. ${i + 1} (${getToday(
                    getDay(year, month + 1, i + 1)
                  )})`
              ),
          ];
    default:
      return [];
  }
}

function () {
  ...
  return (
    ...
        <div>
          <h2 className="font-bold">
            시작일<span className="text-red-400">*</span>
          </h2>
          <ul className="flex flex-wrap">
            {getChallengeStartDate().map((d, i) => (
              <li className="border rounded-full px-2 py-1 mt-1 mr-1">{d}</li>
            ))}
          </ul>
        </div>
  );
}

10.08 토 기준

말일을 넘어간 경우 표시

평일 매일

내가 생각한 거에서 한 80% 성공..?
switch 문 case 안에선 블록 스코프? 문제가 있어서 그냥 overflow 부분을 밖으로 뺐다.

function getChallengeStartDate(af = 'WEEKDAY') {
  // af [ EVERYDAY, WEEKDAY, WEEKEND, ONCEAWEEK, TWICEAWEEK, THIRDAWEEK, FORTHAWEEK, FIFTHAWEEK, SIXTHAWEEK ]
  // p [ ONEWEEK, TWOWEEK, THREEWEEK, FOURWEEK ]
  const {
    currDate,
    year,
    month,
    date, //
    day,
  } = getCurrentDate();
  const lastMonth = 11; // 말월
  const lastDate = getLastDate(year, month); // 말일
  // console.log('lastDate:', lastDate);
  const currMon = month + 1;
  // const date = 26; // test
  const overflow = date + 7 > lastDate;
  const overflowDay = lastDate - (date + 7);

  // 말일 넘는지?
  // 8일(토)기준 removeWeekend => [10, 11, 12, 13, 14, 17]
  // 만약 26(수) 기준이면 [27, 28, 31, 32, 33, 34, 35]
  // [27 28 31 1 2 3 4]
  // 이상하게나옴; 내생각엔 공휴일 제거 전에 해야할것같음

  const overflow9 = date + 9 > lastDate;
  // const overflowDay9 = lastDate - (date + 9);

  // 총 9일
  // 26 => [27, 28, 29, 30, 31, 32, 33, 34, 35]
  const nineDays = new Array(9).fill('').map((_, i) => date + i + 1);
  const weekDays = removeWeekend(year, month, nineDays);

  // 공휴일, 주말 제거..
  const removeOverLastDate = nineDays.filter((d, _) => d <= lastDate);
  const removeOAndW = removeWeekend(
    year,
    month,
    nineDays.filter((d, _) => d <= lastDate)
  );
  const nextMonthDays = removeWeekend(
    year,
    month + 1,
    new Array(9 - removeOverLastDate.length).fill('').map((_, i) => i + 1)
  );

  switch (af) {
    case 'EVERYDAY':
      ...
    case 'WEEKDAY':
      return !overflow9
        ? weekDays.map(
            (d) => `${currMon}. ${d} (${getToday(getDay(year, month, d))})`
          )
        : [
            ...removeOAndW.map(
              (d) => `${currMon}. ${d} (${getToday(getDay(year, month, d))})`
            ),
            ...nextMonthDays.map(
              (d) =>
                `${currMon + 1}. ${d} (${getToday(getDay(year, month + 1, d))})`
            ),
          ];
    default:
      return [];
  }
}

10.08 토 기준

10.26 수 기준

정리가 안 됐고 변수명도 그렇고 복잡해서 직관적으로 보이지 않는다. 다른 사람이 이해하려면 설명이 좀 필요할 것 같은..? 이건 뭔가 단순히 한정된 경우의 작업만 걸쳐서 위처럼 만약 내년으로 넘어가는 경우 등의 많은 케이스에 적용할 수 있게 고치고 싶다.. 일단 여기까지 ^^

주말 매일

첫 번째 시도

  // 가까운 주말
  // - 이미 생성하려는 날이 주말이면 다음 주 주말
  // 8(토-6) 기준 => 15, 16
  // - 아니면 곧 다가올 주말을 찾아야 함
  // 9(일-0)~14(금-5) 기준 => 15, 16
  // 1. 오늘로부터 +1 하면서 0, 6을 찾기?

  function test(month: number, date: number, day: number, lastDate: number) {
    // 말일 체크 && 2주

    let tempMonth = month;
    let tempDate = date;
    let tempDay = day;

    if (day === 6) {
      tempDate += 2;
      tempDay = 1;
    } else if (day === 0) {
      tempDate += 1;
      tempDay = 1;
    }

    while (tempDay !== 6) {
      if (tempDate >= lastDate) {
        tempDate = 0;
        tempMonth += 1;
        continue; // 차이는잘모르겠지만 일단생각나서써봣음..
      }
      tempDay++;
      tempDate++;
      // console.log(tempDay);
    }

    return [tempMonth, tempDate];
  }

  console.log(test(month, 29, 6, lastDate)); 
  // 8(일), 6(토) => [10, 15] 10.15
  // 29(일), 6(토) => [11, 5] 

  const t = getNearSaturday(month, date, day, lastDate);
  const t1 = [t, [t[0], t[1] + 1]]; // [ [9, 15], [9, 16] ] // 첫주주말
이후 문제
  • 이것도 12.31이 넘어갔을 때를 체크
  • 2주치 주말을 보여줘야 함
매번 헷갈리는 부분
  • dateObj.getMonth() -> 0~11
  • dateObj.getDay() -> 0~6
둘째 주? 주말 얻기
// utils.ts
export function getNearSaturday(
  month: number,
  date: number,
  day: number,
  lastDate: number
) {
  ...
  // day도 반환하는 배열에 추가했다.
  return [tempMonth, tempDate, tempDay];
}

// Challenge.tsx
...
  const firstSat = getNearSaturday(month, date, day, lastDate);
  // 아까 만든 함수에 첫째 주 토요일 데이터를 넣기!
  const secondSat = getNearSaturday(
    firstSat[0],
    firstSat[1],
    firstSat[2],
    lastDate
  );
  console.log(secondSat); // [9, 22, 6] = 10.22(6-토)   
  const firstAndSecondWeekendData = [
    [firstSat[0], firstSat[1]],
    [firstSat[0], firstSat[1] + 1],
    [secondSat[0], secondSat[1]],
    [secondSat[0], secondSat[1] + 1],
  ];
  console.log(firstAndSecondWeekendData); // [[9, 15], [9 ,16], [9, 22], [9, 23]]
할 일

다른 케이스에서도 데이터가 잘 반환되는지
[secondSat[0], secondSat[1] + 1], 만약 31 -> 1 이면..?
그 외의 경우도 생각,,

첫 번째 시도 마무리

function getChallengeStartDate(af = 'WEEKEND') {
  // af [ EVERYDAY, WEEKDAY, WEEKEND, ONCEAWEEK, TWICEAWEEK, THIRDAWEEK, FORTHAWEEK, FIFTHAWEEK, SIXTHAWEEK ]
  // p [ ONEWEEK, TWOWEEK, THREEWEEK, FOURWEEK ]
  const {
    currDate,
    year,
    month,
    date, //
    day,
  } = getCurrentDate();
  const lastMonth = 11; // 말월
  const lastDate = getLastDate(year, month); // 말일
  const currMon = month + 1;
  // const date = 26; // test
   
  ...
  
  // 주말 매일
  const firstSat = getNearSaturday(month, date, day, lastDate);
  const secondSat = getNearSaturday(
    firstSat[0],
    firstSat[1],
    firstSat[2],
    lastDate
  );
  const firstAndSecondWeekendData = [
    [firstSat[0], firstSat[1], 6],
    [firstSat[0], firstSat[1] + 1, 0],
    [secondSat[0], secondSat[1], 6],
    [secondSat[0], secondSat[1] + 1, 0],
  ];

  switch (af) {
    case 'EVERYDAY': ...
    case 'WEEKDAY': ...
    case 'WEEKEND':
      return firstAndSecondWeekendData.map(
        ([mon, date, day], i) => `${mon}. ${date} (${getToday(day)})`
      );
    default:
      return [];
  }
}

export default function Challenge() { ... }

저 위의 문제들과, 내가 아예 반환하는 배열에 월. 일 (요일) 계산된? 문자열의 배열을 반환했는데 문자열이 아니라 월, 일, 요일 데이터로 이루어진 배열로 리턴해서 jsx 문법 부분에서 map을 돌 때 추출해서 사용해도 될 것 같다.

수정
export function getNearSaturday(month: number, date: number, day: number) {
  console.log(month, date, day);
  // 말일 체크 && 2주
  const lastDate = getLastDate(new Date().getFullYear(), month);
  ...
}
//
  switch (af) {
    case 'EVERYDAY': ...
    case 'WEEKDAY': ...
    case 'WEEKEND':
      return firstAndSecondWeekendData.map(
        ([mon, date, day], i) => `${mon + 1}. ${date} (${getToday(day)})`
      );

10.29 (토) 기준

주 n 회

다음 주 월요일, 다다음 주 월요일 표시

//
export function getNearMonday(month: number, date: number, day: number) {
  const lastDate = getLastDate(new Date().getFullYear(), month);
  let tempMonth = month;
  let tempDate = date;
  let tempDay = day;
  if (day === 1) {
    tempDate += 1;
    tempDay = 2;
  } else if (day === 6) {
    tempDate += 1;
    tempDay = 0;
  }
  while (tempDay !== 1) {
    if (tempDate >= lastDate) {
      console.log('lastDate!', lastDate);
      tempDate = 1;
      tempMonth += 1;
    }
    if (tempDay === 6) {
      tempDate += 1;
      tempDay = 0;
    }
    tempDay++;
    tempDate++;
  }
  return [tempMonth, tempDate, tempDay];
}

// C
function getChallengeStartDate(af = 'NWEEK') {
  ...
  const firstMon = getNearMonday(month, date, day);
  const secondMon = getNearMonday(firstMon[0], firstMon[1], firstMon[2]);
  const firstAndSecondMondayData = [
    [firstMon[0], firstMon[1], firstMon[2]],
    [secondMon[0], secondMon[1], secondMon[2]],
  ];

  switch (af) {
    case 'EVERYDAY':
	  ...
    case 'NWEEK':
      return firstAndSecondMondayData.map(
        ([mon, date, day]) => `${mon + 1}. ${date} (${getToday(day)})`
      );
    default:
      return [];
  }
}

10.10 월 기준

이제 해야 할 것은 빈도와 기간 선택에 따라서 시작일과 예상 기간을 동적으로 표시하기랑 저거 함수 리팩토링하기.


참고
MDN - Date
Javascript - 입력한 년, 월의 마지막 날짜 구하기

profile
꾸준히 자유롭게 즐겁게

0개의 댓글