Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3주차 기본/심화/생각 과제] 점메추 🍚 #3

Merged
merged 44 commits into from
Nov 17, 2023
Merged

Conversation

binllionaire
Copy link
Member

@binllionaire binllionaire commented Nov 10, 2023

✨ 구현 기능 명세

🌱 기본 조건

  • 기본조건1

✅ 선택 과정은 총 3단계입니다. ( 3개 → 3개 → 2개)

✅ 아이템은 총 18개 이상입니다. (3 x 3 x 2 = 18)

위는 “최소”기준이며 그 이상의 개수는 가능합니다.

  • 기본조건2

✅ 전역상태관리 라이브러리, context 사용 금지 ❌

✅ Router 사용 금지 ❌


🧩 기본 과제

  1. 추천 종류 선택
    • 취향대로 추천 / 랜덤 추천 중 선택합니다.
    • 선택시 다음화면으로 넘어갑니다.

[취향대로 추천]

  1. 답변 선택

    • 호버시 스타일 변화가 있습니다.
    • 클릭시(선택시) 스타일 변화가 있습니다.
  2. 이전으로, 다음으로(결과보기) 버튼

    • 아무것도 선택되지 않았을 시 버튼을 비활성화 시킵니다.

      → 눌러도 아무 동작 X

      → 비활성화일 때 스타일을 다르게 처리합니다.

    • 이전으로 버튼을 누르면 이전 단계로 이동합니다.

    • 다음으로 / 결과보기 버튼을 누르면 다음 단계로 이동합니다.

    • 버튼 호버시 스타일 변화가 있습니다.

  3. 결과

    • 선택한 정보들에 맞는 결과를 보여줍니다.

[ 랜덤 추천 ]

  1. 숫자 카운트 다운
    • 3 → 2 → 1 숫자 카운트 다운 후 결과를 보여줍니다.
    • 추천 결과는 반드시 랜덤으로 지정합니다.

[ 공통 ]

  1. 결과 화면
    • 다시하기 버튼

      → 랜덤추천이면 랜덤 추천 start 화면으로, 취향대로 추천이면 취향대로 추천 start 화면으로 돌아갑니다.

      → 모든 선택 기록은 리셋됩니다.


🌠 심화 과제

  1. theme + Globalstyle 적용

    • 전역으로 스타일을 사용할 수 있도록 적용해보세요
  2. 애니메이션

    • 랜덤 추천 - 카운트다운에 효과를 넣어서 더 다채롭게 만들어주세요!
  3. 헤더

    • 처음으로 버튼

      → 추천 종류 선택 화면일시 해당 버튼이 보이지 않습니다.

      → 처음 추천 종류 선택 화면으로 돌아갑니다.

      → 모든 선택 기록은 리셋됩니다.

[ 취향대로 추천 ]

  1. 단계 노출

    • 3단계의 진행 단계를 보여줍니다.
  2. 이전으로 버튼

    • 이전으로 돌아가도 선택했던 항목이 선택되어 있습니다.
  • 6. useReducer , useMemo , useCallback 을 사용하여 로직 및 성능을 최적화합니다.

생각과제

  • 리액트에 대하여
  • 컴포넌트는 어떤 기준과 방법으로 분리하는 것이 좋을까?
  • 좋은 상태 관리란 무엇일까?
  • 렌더링을 효과적으로 관리하는 방법은 무엇이 있을까?
  • Props Drilling이란 무엇이고 이를 어떻게 해결할 수 있는가?

💎 PR Point

저는 이번 과제에서 컴포넌트 분리에 신경을 써봤습니다

🧘🏻‍♀️ useReducer 를 사용하여 상태관리를 했어요

const reducer = (state, action) => {
  switch (action.type) {
    case "MOVE_TO_PREV_PAGE": // 이전 페이지로 이동
      return { ...state, page: state.page - 1 };
    case "MOVE_TO_NEXT_PAGE": // 다음 페이지로 이동
      return { ...state, page: state.page + 1 };
    case "MOVE_TO_PAGE": // 특정 페이지로 이동
      return { ...state, page: action.payload };
    ...
  }
};

const prevPage = () => {
    dispatch({ type: "MOVE_TO_PREV_PAGE" });
};

🧠 useMemo 를 사용하여 렌더링을 줄여보았아요

const mainCharacter = useMemo(() => <MainCharacter />, []);

⏱️ useInterval 을 사용하여 setInterval 관련 이슈를 해결했어요

  useInterval(() => {
    if (count === 1) {
      setIsFinish(true);
    } else {
      setCount(count - 1);
    }
  }, 1000);

최적화를 위해 react dev tool을 사용하였는데 그러던 중 컴포넌트가 넘어갔는데도 setInterval이 계속 진행되고 있는 것을 발견했어요
저는 이글을 참고해서 수정해보았습니다

📂 크게 5개의 화면으로 구성 했어요

📂pages
┣ 📄 TypeSelectPage.jsx : 추천 방식을 고르는 화면
┣ 📄 StartPage.jsx : 시작 화면
┣ 📄 TypeQuestionPage.jsx : 취향대로 추천의 질문 화면
┣ 📄 CountDownPage.jsx : 랜덤추천의 카운트다운 화면
┗ 📄 ResultPage.jsx : 결과 화면

♻️ 재사용이 가능한 공통 컴포넌트를 분리 했어요

📂components
┣ 📄 Header.jsx
┣ 📄 MainCharacter.jsx
┣ 📄 Option.jsx
┣ 📄 PageMoveButton.jsx
┗ 📄 ProgressBar.jsx

💅 여러 컴포넌트에서 사용하는 스타일은 Global Style 로 정의했어요

const GlobalStyle = createGlobalStyle`
export const Button = styled.button`
  width: 100px;
  height: 40px;
  border: none;
  border-radius: 20px;
  background: ${({ theme }) => theme.colors.secondary};
  color: ${({ theme }) => theme.colors.white};
  transition: 0.3s;
`
...
`;

🎨 색상과 폰트 크기를 Theme Provider 로 관리했어요

const colors = {
  black: "#000000",
  white: "#ffffff",
  primary: "#2bc1bc",
  ...
};

const fontSize = {
  sm: "1.0rem",
  base: "1.5rem",
  md: "1.8rem",
  lg: "3rem",
};

👀 page 넘버 를 상수화 하여 가독성을 높였어요

export const PAGE = {
  TYPE_SELECT: 0,
  START: 1,
  RECOMMEND_BY_TYPE_QUESTION_1: 2,
  RECOMMEND_BY_TYPE_QUESTION_2: 3,
  RECOMMEND_BY_TYPE_QUESTION_3: 4,
  RECOMMEND_BY_TYPE_RESULT: 5,
  RECOMMEND_BY_RANDOM: 6,
};

🍚 취향대로 추천의 로직은 다음과 같습니다

선택한 답변에 해당하는 음식들에 점수를 더해서 가장 높은 점수들을 갖는 것 중 랜덤으로 결과를 구했어요

Object.keys(foodData).forEach((food) => {
  foodData[food].region === state.selectedRegion && foodData[food].score++;
  foodData[food].amount === state.selectedAmount && foodData[food].score++;
  foodData[food].taste === state.selectedTaste && foodData[food].score++;
});

for (const food in foodData) {
  if (foodData[food].score > maxCount) {
    bestFoods = [food];
    maxCount = foodData[food].score;
  } else if (foodData[food].score === maxCount) {
    bestFoods.push(food);
  }
}

const randomIndex = Math.floor(Math.random() * bestFoods.length);

dispatch({
  type: "SET_RESULT",
  payload: foodData[bestFoods[randomIndex]].image,
  });
};

🥢 질문 별 아이템은 이렇게 나눴어요

질문 별 아이템 🍽️

🥺 소요 시간, 어려웠던 점

  • 3d
  • 렌더링 성능을 더 최적화 시키고 싶었는데 생각대로 잘 안되더라구요 🥲 아직 리액트에 대한 이해와 hook에 대한 이해가 많이 부족하다는 것을 느꼈어요
  • setInterval 관련 이슈를 수정하는데 시간이 오래 걸렷어요

🌈 구현 결과물

취향대로 추천

recommend_by_type.mov

랜덤 추천

recommand_by_random.mov

다음/이전 버튼, 처음으로

next_prev_test.mov

Copy link

@lydiacho lydiacho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드맛집이라고 소문나서 구경왔다가 리뷰 딱 하나! 의견 남기고 갑니다 ㅎ ㅎ ㅎㅎ
진짜 코드 기가 막히게 깔끔하네요.. 주석도 적절히 사용해줘서 정말 이해하기 좋은 코드 그 자체인 것 같아요! 저도 수빈이 본받아서 주석 사용해봐야겠어용 😎

수고많았습니다 ~~ 👍 🥰

Comment on lines 19 to 31
<Header restart={restart} />
<Section>
<Quetion>오늘의 추천음식은</Quetion>
<Answer>
<WhiteBox>
<FoodImg src={result} alt="추천음식"></FoodImg>
</WhiteBox>
</Answer>
<Button type="button" onClick={() => retry(type)}>
다시하기
</Button>
<MainCharacter />
</Section>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pages 디렉토리 내에 들어있는 컴포넌트들을 보면, CountdownPage를 제외하고 모두 아래와 같은 구조가 반복되고 있는 것 같아요!

<Header/>
<Section>
  {조건에따라 <ProgressBar/>}
  <Question>{질문}</Question>
  <Answer>{선택지}</Answer>
  {조건에따라 <Button>{버튼}</Button>}
  <MainCharacter/>
</Section>

네가지 페이지에서 반복 사용되는 UI 컴포넌트를 분리한 것 아주 좋아요!! 👍🏻🥰
여기서 약간 다른 모듈화 방식도 한가지 제안 드리자면,
결국 위에 쓰인 N가지 컴포넌트들이 같은/거의 유사한 조합으로 반복 사용되고 있는거잖아요??
따라서 반복되는 그 조합을 하나의 레이아웃 컴포넌트로 생성해서 반복 사용해주는 방법도 깰꼼할 것 같아서 이런 방식도 있다! 하고 의견 남기고 갑니당

즉,

  • 모듈화 하지 않았다면 N개의 컴포넌트를 3-4번씩 반복 생성했을 것을
  • 현재 수빈이는 Header, MainCharacter, Question, Section 등의 N개의 컴포넌트를 1번만 생성하여 재사용성을 높였고,
  • 제가 추가로 제안한 방식은 이들의 반복되는 조합을 1개의 컴포넌트로 1번 생성하여 재사용하는 방식인거죠!!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동의합니당!! 👍

Copy link
Member Author

@binllionaire binllionaire Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정성스런 리뷰에 감동 받았덩 🥹 리뷰 반영해서 이렇게 컴포넌트 하나로 빼서 리팩토링 해봤어용 🖤

const MainSection = () => {
  return (
    <>
      {page === PAGE.TYPE_SELECT ? (
        <Header isTypeSelectPage={true} />
      ) : (
        <Header restart={restart} />
      )}
      <Section>
        {}
        {isQuestionPage && <ProgressBar page={page - 1} />}
        <Quetion>{question}</Quetion>
        <Answer>{contents}</Answer>
        {mainCharacter}
        {nav}
      </Section>
    </>
  );
}

Copy link

@doyn511 doyn511 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

갓기를 우아한형제들로.....~ 수고 많았어 수빈이!!! 내가 좀 더 많이 알았다면 이것저것 많이 써줄텐데 지금은 내가 수빈이한테 배워가야하는 수준이라..허허.. 고생했어!!!!

Comment on lines +18 to +19
저는 보통 `UI 단위`로 나누기는 하는데, 컴포넌트가 너무 길어서 `가독성`이 떨어지거나 다른 컴포넌트에서 `재사용`될 수 있다면
컴포넌트로 분리하는 편이에요 🐝
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우리 갓기 멋져용~!

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별건 아니지만 이거 ko로 바꿔줄까....?

Comment on lines +19 to +26
const initialState = {
page: 0,
selectedRecommendType: "",
selectedRegion: "",
selectedAmount: "",
selectedTaste: "",
result: "",
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 좀 좋은거 같다......... 분명 배웠는데 어떻게 쓰는지를 몰라서 못써봤는데 다음 리팩토링 할 때 한번 해볼게!

};

function App() {
const [state, dispatch] = useReducer(reducer, initialState);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useReducer를 어떻게 쓰는지 감이 안잡혔었는데 수빈이 코드 보면서 조금은 익숙해진거같아 .. 고마워!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나도! 거의 useState라도 배워가자란 생각으로 과제 했는데 useReducer에 대해서 한번 더 찾아보고 알아가게 된 계기가 된 것 같아

Comment on lines +55 to +57
const prevPage = () => {
dispatch({ type: "MOVE_TO_PREV_PAGE" });
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useReducer로 이렇게 주니까 완전 깔끔하다..

Comment on lines +24 to +26
<DisabledNextButton type="button" disabled>
다음으로
</DisabledNextButton>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 아예 이렇게 따로 빼줬구나 ..!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

progress bar까지 만든거 완전 최고다..... 시연영상 보는데 감탄했음ㅎㅎ

<Section>
<LoadingBox>
<CountNumber>{count}</CountNumber>
<Spinner src={image} alt="회전하고 있는 음식들" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alt까지 야무지다..

nextPage,
restart,
}) => {
const mainCharacter = useMemo(() => <MainCharacter />, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

난 아직 useMemo가 어려워서 .. 아니 언제 어떻게 쓰이는지 감이 잡히질 않더라.. 그치만 사용한 수빈이 칭찬해~

<>
<Header restart={restart} />
<Section>
<ProgressBar page={currentPage - 1} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기는 왜 currentPage -1로 넘겨주는거야?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 페이지가

export const PAGE = {
  RECOMMEND_BY_TYPE_QUESTION_1: 2,
  RECOMMEND_BY_TYPE_QUESTION_2: 3,
  RECOMMEND_BY_TYPE_QUESTION_3: 4,
};

이렇게 설정돼있는데, progressbar 너비는

const Progress = styled.div`
  width: ${({ page }) => page * 33.3}%;
`;

이렇게 계산돼서 -1 해줬어!!

Copy link

@hae2ni hae2ni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수비니 넘넘 수고했어요!!! 상수로 빼는 것도 너무 좋구, 고민 많이 한 것 같아! 최고!

`;

const Title = styled.h1`
font-size: ${({ theme }) => theme.fontSize.md};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

폰트 사이즈도 걸어놨구만!! 홍홍!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그때 글로벌스타일 물어봤던 게 기억나서!
요기에 정리 잘 되어 이쏘
뺙 🐣

return (
<HeaderWrapper>
<Title>오늘의 점메추</Title>
{!isTypeSelectPage && <Button onClick={() => restart()}>처음으로</Button>}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아고 깔끔해!!!

import mainCharacter from "../assets/img/character.png";

export default function MainCharacter() {
return <Character src={mainCharacter} alt="캐릭터" />;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

캐릭터? 그 그 배민 그거맞죵,,,?ㅋㅎㅋㅎㅋㅎ

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니 디자인 미쳤나봐 당신 진짜 뭔데

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마자요 흐흐 😝

import { BalloonText, RadioBalloon, RadioInput } from "../styles/GlobalStyle";

export const Option = ({ option, saveAnswer, currentPage, selectedValue }) => (
<div key={option.id}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 구조분해할당 써서 조금 더 깔끔하게 해도 좋지 않을까? option.으로 들어오는 게 꽤 있는 것 같아서!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와앙 고마워 🖤 리팩완료 🚀

export const Option = ({ option, saveAnswer, currentPage, selectedValue }) => {
  const { id, name, label } = option;

  return (
    <div key={id}>
      <RadioInput
        type="radio"
        id={id}
        name={name}
        onChange={() => saveAnswer(id, currentPage)}
        checked={selectedValue === id}
      ></RadioInput>
      <RadioBalloon htmlFor={id}>
        <BalloonText>{label}</BalloonText>
      </RadioBalloon>
    </div>
  );
};

background-color: ${({ theme }) => theme.colors.grey};
`;

const Progress = styled.div`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니 대박,,,,,,, 진행바로 생각한 거 뭔데뭔데,,


export default GlobalStyle;

export const Section = styled.section`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요건 GlobalStyle 파일에 넣기보다는 따로 commonStyle 파일로 분리해서 정리하는 게 더 좋을 것 같다아!

Comment on lines 184 to 186
display: flex;
justify-content: center;
align-items: center;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 요소가 겹치면 겹치는 것만 따로 style 뺄 수 있어요!

Suggested change
display: flex;
justify-content: center;
align-items: center;
${centerFlex}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꿀팁 너무 고맙습니당 🖤

amount: "properly",
taste: "fat",
score: 0,
image: "src/assets/img/chicken-ribs.png",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 그냥 파일보다는 사진 주소로 넣는게 ㄷㅓ 편할거야!

},
};

export const regionOptions = [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요런 라벨들 상수로 뺀거 너무 좋아! 근데, 위에 객체랑 같은 파일에 넣은 이유가 있을까?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎㅎ 분리해야겠다!!

@@ -0,0 +1,209 @@
export const foodData = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 객체가 아니라 리스트면 method 쓰기가 더 쉬울 것 같아!

Comment on lines +30 to +35
case "MOVE_TO_PREV_PAGE": // 이전 페이지로 이동
return { ...state, page: state.page - 1 };
case "MOVE_TO_NEXT_PAGE": // 다음 페이지로 이동
return { ...state, page: state.page + 1 };
case "MOVE_TO_PAGE": // 특정 페이지로 이동
return { ...state, page: action.payload };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 있으니까 진짜 보기 편하다,,

Comment on lines +84 to +101
/* 취향대로 - 1 */
case PAGE.RECOMMEND_BY_TYPE_QUESTION_1:
dispatch({ type: "MOVE_TO_NEXT_PAGE" });
break;

/* 취향대로 - 2 */
case PAGE.RECOMMEND_BY_TYPE_QUESTION_2:
dispatch({ type: "MOVE_TO_NEXT_PAGE" });
break;

/* 취향대로 - 3 */
case PAGE.RECOMMEND_BY_TYPE_QUESTION_3:
resultPage();
break;

default:
dispatch({ type: "MOVE_TO_NEXT_PAGE" });
break;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch문 대박 잘 씀!

}
};

/* 선택된 답변에 해당하는 음식의 점수를 증가 시킨 후 가장 점수가 높은 음식을 찾아 결과 화면으로 이동 */

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

점수제로 결과 도출하는 아이디어 좋당!

Comment on lines +121 to +125
let maxCount = -1;

for (const food in foodData) {
if (foodData[food].score > maxCount) {
bestFoods = [food];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내가 아는 for문이랑 너무 달라.. ()안에 설명가능할까효..?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for ... in 구문인데 foodData를 순회하는 거라고 생각하면 돼!!!
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/for...in
요고 한번 참고해봐!

Comment on lines +160 to +168
/* 답변 초기화 */
const resetAnswer = () => {
dispatch({ type: "SET_SELECTED_REGION", payload: "" });
dispatch({ type: "SET_SELECTED_AMOUNT", payload: "" });
dispatch({ type: "SET_SELECTED_TASTE", payload: "" });
dispatch({ type: "SET_RESULT", payload: "" });
Object.keys(foodData).forEach((food) => {
foodData[food].score = 0;
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음으로 눌렀을 때 답변을 초기화 하는거 맞을까용?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마자용~!!

Comment on lines 217 to 226
<TypeQuestionPage
type={question.amount}
options={amountOptions}
selectedValue={state.selectedAmount}
saveAnswer={saveAnswer}
currentPage={state.page}
prevPage={prevPage}
nextPage={nextPage}
restart={restart}
/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오앙.. props 짱많아.. 고생많았당!

Comment on lines +9 to +11
<React.StrictMode>
<ThemeProvider theme={theme}>
<GlobalStyle />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 곳에서 theme로 style 준거를 재사용한건가요?!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThemeProvider를 적용하는 방법이에요!!

@binllionaire binllionaire merged commit f6c370f into main Nov 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants