Skip to content

[1단계 - 콘솔 기반 로또 게임] 치코(이재민) 미션 제출합니다.#262

Merged
liswktjs merged 67 commits into
woowacourse:jaeml06from
jaeml06:step1
Feb 24, 2024
Merged

[1단계 - 콘솔 기반 로또 게임] 치코(이재민) 미션 제출합니다.#262
liswktjs merged 67 commits into
woowacourse:jaeml06from
jaeml06:step1

Conversation

@jaeml06
Copy link
Copy Markdown

@jaeml06 jaeml06 commented Feb 22, 2024

안녕하세요! 샐리
이번에 요구사항을 충족하면서 TDD를 적용해보기 위해 노력했습니다.

이번 미션 파일들에 간략하게 설명드리겠습니다.

  • constants 로직과 입출력에 필요한 문자열과 숫자를 저장하고 있는 폴더
  • controller : 전체 로또 게임의 진행을 통제하는 코드
  • domain
    • Lotto.js 정수 배열을 생성자로 받아 로또 자체를 생성하는 클래스
    • LottosProcess.js Lotto 클래스 배열을 받아 당첨 결과를 계산하는 클래스
  • Util
    • Console : 콘솔 입력을 담당하는 유틸리티
    • Random: from부터 to까지의 정수를 배열로 반환하는 유틸리티
  • validation: 각 입력값의 유효성을 검사라는 함수 객체
  • View: 입출력을 담당하는 함수

미션을 진해하면서 몇가지 궁금점이 생겨 질문을 남깁니다.

  1. TDD 적용 과정 문제

    이번에 TDD를 적용해보면서 테스트를 먼저 작성하고 함수를 작성하였는데, 그렇게 기능을 구현하다 보니 테스트 했던 함수를 프라이빗 함수로 바꿀 필요가 있다고 생각되어 바꾸려고 하였습니다. 하지만 이러면 테스트 코드에서 프라이빗 함수를 사용할 수가 없었습니다. 예를 들어 처음에 LottoProcess.js내의 matchLottoNumbers()함수를 만들려고 테스트를 만들었는데 나중에 기능을 완성하고 보니 같은 LottoProcess클래스의 getResult함수 내부에서만 호출하여 matchLottoNumbers함수를 프라이빗 함수로 만들고 싶었습니다. 저는 클래스에서 외부에서 호출할 경우가 있는 함수 이외에는 private함수로 만드는 것이 맞다라고 생각하기 때문입니다. 그런데 이러면 기존에 작성했던 테스트를 삭제해야 하는데 저는 결국 최종 목표는 getResult()를 작성하는 것이 목표이기 때문에 getResult() 테스트 코드를 만들고 삭제하는 것이 맞다고 생각합니다. 샐리의 생각이 궁금합니다.

  2. validation의 위치

    위치 이번 미션을 진행하면서 프로그램에 필요한 모든 유효값 로직을 validation폴더에 모아 두었습니다, 하지만 이 중 LottoValidation은 로또 관련 검사이기 때문에 domain폴더 내 위치하는 것이 더 좋을까? 라는 생각이 듭니다. 하지만 막상 위치를 변경하려고 하니 그럼 validation폴더를 만든 의미가 있나? 라는 생각이듭니다.

  3. 함수는 한가지의 기능

    이번 요구사항에도 함수의 길이를 10줄 이하로 제한한다는 조건이 있었습니다. 하지만 LottoProcess.js의 getResult함수를 만들면서 저는 하나의 함수에 하나의 기능만 한다고 생각하는데 10줄이 넘어버렸습니다. 저는 10줄 제한이 함수 하나에 하나의 기능만 구현한다라는 목표만 달성되었다면 유동적으로 조절할 수 있다고 생각합니다.

    또한 다음과 같은 from과 to로 정수의 범위를 인수로 넘기고 count수를 넘기면 count만큼 겹치지 않은 무작위 수를 배열로 반환하는 함수를 만들었지만, 매개변수가 3개이고 함수의 깊이가 2라는 이유로 결국 사용하지 못했습니다.
    저는 이 함수가 함수가 한가지 기능만 한다 규칙을 위배하지 않았다고 생각합니다.

    pickUniqueNumbersInRange(from, to, count) {
        const uniqueArr = [];
        while (uniqueArr.length !== count) {
          const randomNumber = Math.floor(Math.random() * (to - from + 1)) + from;
          if (!uniqueArr.includes(randomNumber)) uniqueArr.push(randomNumber);
        }
        return uniqueArr;
      },

    샐리는 어떠신가요? 10줄 제한이라는 것, 3개 미만의 매개변수, 깊이 1이하를 반드시 지켜야하는 철칙이라고 생각하시나요?

  4. 상수 선언

    LottoController클래스 내부 buyLottos에서 상수 선언이 너무 많다고 느껴집니다. 그래서 한번만 사용되는 const로 선언된 변수라면 함수의 반환값을 인자로 넘겨줘도 되지 않을까 생각됩니다만, 가독성이 오히려 떨어질까 생각이 듭니다.

    샐리의 생각은 어떠신가요

    const lottos = this.getLottos(lottoCount);
    const lottoProcess = new LottoProcess(lottos);
    ///
    
    const lottoProcess = new LottoProcess(this.getLottos(lottoCount));
  5. 상수화

OUTPUT: Object.freeze({
    LOTTO_PURCHASED: `개를 구매했습니다.`,
    WINNING_STATISTICS_TITLE: `\n당첨 통계\n--------------------`,
    MATCH_COUNT: `개 일치`,
    BONUS_MATCH: `, 보너스 볼 일치`,
    WIN_COUNT: `개`,
    EMPTY: ``,
    HYPEN: '-',
    RATE_OF_REVENUE: (result) => `총 수익률은 ${result}%입니다.`,
    WIN_PRICE: (price) => `(${price})원)`,
  }),
});

​ 위의 RATE_OF_REVENUE()와 WIN_PRICE같이 함수로 선언하여 사용해도 되는지 궁금합니다. 아직 제가 문자열 형태로 존재해야한다는 편견이 있는 것 같아 질문드립니다.

유효성 검사 함수 작성
당첨 결과를 배열로 반환하는 로직 구현
else if 구문 삭제
당첨 결과에 따른 상금을 저장하는 객체
Copy link
Copy Markdown

@liswktjs liswktjs left a comment

Choose a reason for hiding this comment

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

안녕하세요 치코 👋
이번에 리뷰어를 맡게된 샐리입니다

리뷰 코멘트에 남겨주신 질문들 3번은 빼고 나머지 부분들은 코멘트에 남겼습니다! 😊

함수는 한가지의 기능

우선 예시로 적어주신 코드를 사용하지 못해서 아쉬운 점이 있으신 것 같아요
물론 현실에서는 함수가 10줄이 넘어갈 때가 있고 인자도 3개일 때 더 적절한 함수가 될 수도 있습니다
오히려 너무 무리한 분리는 로직이 파편화 되서 추후에 유지보수가 힘들어 질 수 도 있습니다

하지만 우테코에서 학습을 하시는 동안에는 해당 규칙들을 의식적으로 지킬려는 노력이 필요하다고 생각합니다!

교육과정에서 해당 컨벤션들을 지킬려는 노력들이 적절한 단위의 함수를 만들 수 있는 경험치를 쌓을 수 있는 디딤돌이 될 수 있다고 생각합니다
추가로 우테코 생활이 아니면 이렇게 함수를 깔끔하게 짤 경험이 많지 않을 수도 있기 때문에 우테코에 있는 동안이라도 최대한 컨벤션을 지켜서 나눠보시는게 어떨까요?

pickUniqueNumbersInRange(from, to, count) {
    const uniqueArr = [];
    while (uniqueArr.length !== count) {
      const randomNumber = Math.floor(Math.random() * (to - from + 1)) + from;
      if (!uniqueArr.includes(randomNumber)) uniqueArr.push(randomNumber);
    }
    return uniqueArr;
  },

추가로 이 코드를 같이 살펴보면 우선, 여러 함수들로 한 번 더 나눌 수 있다고 생각합니다
예를 들어,

 const randomNumber = Math.floor(Math.random() * (to - from + 1)) + from;
      if (!uniqueArr.includes(randomNumber)) uniqueArr.push(randomNumber);

이 부분을
unique한 randomNumber를 뱉는 함수로 빼서 호출 할 수 있다고 생각합니다

이렇게 분리해서 한 번 적용해보시는 것도 나쁘지 않을 것 같습니다!

추가로 제안 드리고 싶은 점

  • 커밋 이력을 보았을 때 커밋 메세지가 상세하지 못하다고 생각을 했습니다 어떤 내용의 코드를 커밋했는 지 커밋 메세지에 더 들어내주었으면 좋을 것 같아요
  • 코드와 관련된 내용을 pr template에 질문을 많이 써주셨는데 코드 단에서 코멘트로 남겨주시면 리뷰할 때 빠르게 찾을 수 있어서 도움이 될 수 있을 것 같습니다 👍

Comment thread src/constants/message.js Outdated
import RestartResponseValidation from '../validation/responseValidation';
import MESSAGE from '../constants/message';

class LottoController {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LottoController 클래스에서 따로 상태를 가지고 있지 않고 안에는 함수들 만 있는데 클래스로 묶은 이유가 있을까요?

Copy link
Copy Markdown
Author

@jaeml06 jaeml06 Feb 22, 2024

Choose a reason for hiding this comment

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

LottoController가 따로 필드를 가지고 있을 필요가 없다고 생각합니다. LottoController 클래스의 기능을 로또의 전체적인 진행만을 담당하고자 하였습니다.

그리고 하나의 함수에서 시작하여 흐름에 따라 코드가 진행되기 때문에 필요한 값들을 인자로 넘겨 주면 된다고 생각했습니다. 또한 불필요한 필드의 경우 유지 보수성이 떨어진다고 생각해서 선언하지 않았습니다.

그럼에도 굳이 클래스로 묶은 이유는 로또라는 전체적인 실행을 하나로 묶는 무엇인가 필요하다고 생각해서 이렇게 작성했었습니다. 샐리의 질문을 듣고 다시 생각 해보니 자주 사용하는 lottoCount와 lottoProcess는 필드에 값을 가지고 있는 것이 좋나? 라는 생각이 드네요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

또한 이 클래스가 컨트롤러여서 값을 가지고 필드에 값을 가지고 있는 것이 옳은가 라는 생각도 들었습니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

질문해주신 내용을 정리해보았을 때, LottoController를 함수를 묶는 용도로만 사용하신 것 같아요
그렇다면, 묶는방법이 굳이 클래스였냐 했을까요? 객체로 묶어도 상관이 없지 않을까요?

함수들을 클래스로 묶는 것도 좋을 수 있습니다
하지만 클래스의 주요 속성이 상속, 은닉, 다형성 등등을 사용하지 않는다면 클래스로 묶을 필요가 있을지 의문이 드네요

추가로, LottoController에서 현재 함수 개수가 너무 많이 위치해 있어요(지금 세워보니 12개 정도 되네요)
이렇게 되면 유지 보수가 힘들어 집니다 더헤서 안의 함수들이 충분히 여러 묶음으로 나눠어 질 수 있을 것 같아 보입니다

만약 LottoController에 필드값을 가지고 있게 수정을 해서 그대로 클래스 사용을 유지하는 방식으로 진행한다 하더라도 클래스를 분리해야할 것같습니다

Comment thread src/controller/LottoController.js Outdated
Comment thread src/controller/LottoController.js
Comment thread src/controller/LottoController.js Outdated
Comment on lines +27 to +35
test('모든 로또의 숫자를 배열로 잘 반환하는지 확인', () => {
const lotto1 = new Lotto([1, 2, 3, 4, 5, 6]);
const lotto2 = new Lotto([5, 10, 15, 20, 25, 30]);
const lottoProcess = new LottoProcess([lotto1, lotto2]);

expect(lottoProcess.getAllLottosNumbers()).toEqual([
[1, 2, 3, 4, 5, 6],
[5, 10, 15, 20, 25, 30],
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 함수를 테스트의 대상으로 선정하신 이유가 궁금합니다!
lotto의 도메인의 주요 로직과는 동떨어져있다고 생각이 들어서요

Copy link
Copy Markdown
Author

@jaeml06 jaeml06 Feb 23, 2024

Choose a reason for hiding this comment

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

LottoProcess생성이 잘되는지 테스트 케이스를 작성한 것 같습니다. 확실히 동떨어져 있다는 생각이 드네요. 별 생각없이 의무감에 작성한것 같습니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

동의하신다면, 해당 테스트 케이스는 지워도 괜찮을 것 같습니다

Comment thread __tests__/lottoProcess.test.js Outdated
Comment on lines +12 to +23
test('당첨결과를 배열로 반환', () => {
const lottos = [new Lotto([1, 2, 3, 4, 5, 6])];
const winLotto = new Lotto([1, 2, 3, 4, 5, 6]);
const bonusNumber = 7;
const lottoProcess = new LottoProcess(lottos);

expect(lottoProcess.getResult(winLotto, bonusNumber)).toEqual([
[3, false, 5_000, 0],
[4, false, 50_000, 0],
[5, false, 1_500_000, 0],
[5, true, 30_000_000, 0],
[6, false, 2_000_000_000, 1],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 테스트코드도 케이스 별로 나눠주면 좋을 것 같습니다

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

테스트 케이스를 더 만들어야 한다는 말씀인가요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

기존에 테스트 코드 설명이 당첨결과를 배열로 반환 로 상세하지 않은 설명이기 때문에

로또에서 제일 중요한 당첨자 뽑기에 대한 테스트가 가장 잘 들어나야 한다고 생각했어요
현재는 1등 케이스에 대해서 잘 명시해주었네요 👍

Comment thread src/constants/message.js Outdated
Comment on lines +22 to +23
const lottoCount = await this.getValidateLottoAmount();
const lottos = this.getLottos(lottoCount);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pr 에서 상수 선언이 너무 많은 것 같다고 말씀을 해주셨는데
저는 const로 선언해주는 것이 더 좋다고 생각합니다
차라리 lottos를 반환하는 함수로 분리는 하는 건 어떨까요?
lottoNumbers도 마찬가지 입니다!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

상수선언이 많아도 상관 없는 것 같아 다행입니다. 다만 lottos를 반환하는 함수로 분리는 하는 건 어떨까요? 이 말이 무슨 말인지 잘 모르겠습니다. 아예 lottoCount를 가져오는 것을 gertLottos함수에서 가져오라는 건가요? 저는 lottoCount 다른 함수에서 인자로 넘겨줘서 getLottos안에 구현하면 사용하지 못합니다. 이럴 때는 그냥 private 필드에 lottoCount 변수를 만드는 것이 좋을까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

하나의 함수 내에서 상수 선언이 너무 많이 이루어지고 있다는게 고민으로 보았습니다
그래서 상수 선언을 줄이기 위해서 함수로 한 번 더 분리하면 좋을 것 같다고 생각했어요!

 const lottoCount = await this.getValidateLottoAmount();
 const lottos = this.getLottos(lottoCount);

이 2개를 하나의 함수로 묶어서 lottos만 반환하는 함수를 만들면 되지 않을까요?

Comment thread src/constants/message.js Outdated
@jaeml06
Copy link
Copy Markdown
Author

jaeml06 commented Feb 23, 2024

프리코스에서부터 공통 피드백에 클래스에서의 필드을 줄이기 위해 노력해야 한다는 글을 보고 클래스에서 필드 선언을 자제하려고 했던것 같습니다. 자주 사용하는 lottoCount나 lottoProcess정도는 필드에 상태를 가지고 있어도 되었을 것 같습니다.

저는 지금 클래스를 정리 상자 개념으로 사용하고 있었습니다. 클래스명에 해당하는 기능을 구현하기 위한 함수를 담는 정리 상자 정도로 사용했습니다. 곰곰이 생각해보면 그냥 .js 파일에 함수로만 선언하는 해도 되는 것 이였습니다. 다만 어떤 형식으로 분리해야 맞는지 감이 잡히지 않아 여러 시도가 필요해 보입니다.

반면에 굳이 클래스 밖으로 빼야하는 이유도 잘 모르겠습니다. 당연히 인스턴스로 생성하면 그만큼 비효율적일 수 있다고 생각합니다.
클래스가 하나의 형식을 여러개 생성할 때 코드의 중복을 피하기 위해 사용하는 것도 알고 있습니다

하지만 명시적인 이름 클래스 이름이 있고 그 클래스에서 구현하고자 목표에 해당되는 함수들을 모아 두는 것이 가독성과 유지보수에 이점이 있다고 생각합니다. 또한 클래스 인스턴스 내부의 값을 수정하고 이용하지 않지만 내부에서 핵심 함수안에서 호출하여 결합해 기능하므로 도메인 로직에 있는 것이 맞다고 생각이 듭니다.

이번 로또 게임 피드백을 받으면서 제가 어느순간 부터 클래스를 잘못 생각해온건 아닌지 의심이 들었습니다. 샐리의 의견이 궁금합니다. 제가 클래스에 대해 어느 부분이 오해하고 있고 다시 알아야 한다면 뭔가 참고할게 있을까요?

@jaeml06
Copy link
Copy Markdown
Author

jaeml06 commented Feb 23, 2024

갑자기 답변들에 대한 궁금증이 늘어 이번 피드백에 대한 답변들이 두서없이 적힌 점 죄송합니다.

Copy link
Copy Markdown

@liswktjs liswktjs left a comment

Choose a reason for hiding this comment

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

안녕하세요 치코
매우 빠르게 1단계 반영을 해주셨네요
추가로 질문 주신 점들 코멘트로 남겨두었습니다 👍
궁금하신 사항 있다면 또 코멘트 남겨주세요

2단계는 직접 화면을 그리는 구현사항이 있어 꽤나 구현이 오래 걸릴 것 같아
이만 머지하겠습니다!
코멘트에 대한 내용들은 2단계에 함께 반영해주면 좋을 것 같습니다 🎱

그럼 행복한 주말 보내세요

await this.showRnadomLottos(lottoCount, lottoProcess);
}

async showRnadomLottos(lottoCount = 0, lottoProcess = {}) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기 함수에 오타가 있네요! 수정이 필요해보여요

Comment on lines +10 to +13
matchLottoNumbers(lotto, winLotto) {
const lottoNumbers = lotto.getNumbers();
return lottoNumbers.filter((value) => winLotto.getNumbers().includes(value)).length;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

제가 코멘트 드린 거에 대해서 설명이 부족해서 오히려 혼란스러우신것 같아요
matchLottoNumbers와 같은 경우에는 util스럽게 함수를 재 선언 해서 분리할 수 있다고 생각했습니다
추가로 LottoProcess의 getResult함수에서만 사용하고 있고요

현재 matchLottoNumbers는 winLotto와 일치하는 lottoNumbers의 개수를 반환해주는 역할을 하고 있기 때문에 인자로
이 2가지의 배열을 받아서 비교후 결과값만을 반환하는 함수를 util스럽게 만들 수 있지 않을까요?

그렇게 되면 함수 네이밍도 matchLottoNumbers가 될 필요가 없고 인자도 lotto, winLotto가 될 필요가 없겠죠?

Comment thread src/constants/message.js Outdated
Comment thread __tests__/lottoController.test.js Outdated
test.each([
[[1, 0, 0, 0, 0], 8, '62.5'],
[[0, 0, 0, 1, 0], 30, '100000.0'],
])('로또 결과에 따른 총 수익률을 계산하여 반환', (winResult, lottoCount, revenue) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'lottoProcess.getResult함수가 반환하는 결과 값을 계산하여 수익률을 반환하는 함수 테스트입니다'
라는 문구는 설명이 모호한 것 같습니다
해당 테스트 문구만 살펴보면 lottoProcess.getResult 라는 함수가 테스트 코드 내에서 실행될 것 처럼 기대하고 테스트 코드를 살펴보게 되는데 이곳에서는 현재 호출하고 있지 않죠

each를 사용지 말자는 이야기는 아닙니다
다만,
함수가 어떤 기준으로 수익율을 계산하는 지도 명시되어 있지 않았었기 때문에 이런 경우라면,
오히려 테스트 케이스 별로 작성해주는게 유저에게 이해가 더 쉽다고 생각했습니다

Comment on lines +27 to +35
test('모든 로또의 숫자를 배열로 잘 반환하는지 확인', () => {
const lotto1 = new Lotto([1, 2, 3, 4, 5, 6]);
const lotto2 = new Lotto([5, 10, 15, 20, 25, 30]);
const lottoProcess = new LottoProcess([lotto1, lotto2]);

expect(lottoProcess.getAllLottosNumbers()).toEqual([
[1, 2, 3, 4, 5, 6],
[5, 10, 15, 20, 25, 30],
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

동의하신다면, 해당 테스트 케이스는 지워도 괜찮을 것 같습니다

Comment thread __tests__/lottoProcess.test.js Outdated
Comment on lines +12 to +23
test('당첨결과를 배열로 반환', () => {
const lottos = [new Lotto([1, 2, 3, 4, 5, 6])];
const winLotto = new Lotto([1, 2, 3, 4, 5, 6]);
const bonusNumber = 7;
const lottoProcess = new LottoProcess(lottos);

expect(lottoProcess.getResult(winLotto, bonusNumber)).toEqual([
[3, false, 5_000, 0],
[4, false, 50_000, 0],
[5, false, 1_500_000, 0],
[5, true, 30_000_000, 0],
[6, false, 2_000_000_000, 1],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

기존에 테스트 코드 설명이 당첨결과를 배열로 반환 로 상세하지 않은 설명이기 때문에

로또에서 제일 중요한 당첨자 뽑기에 대한 테스트가 가장 잘 들어나야 한다고 생각했어요
현재는 1등 케이스에 대해서 잘 명시해주었네요 👍

Comment on lines +22 to +23
const lottoCount = await this.getValidateLottoAmount();
const lottos = this.getLottos(lottoCount);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

하나의 함수 내에서 상수 선언이 너무 많이 이루어지고 있다는게 고민으로 보았습니다
그래서 상수 선언을 줄이기 위해서 함수로 한 번 더 분리하면 좋을 것 같다고 생각했어요!

 const lottoCount = await this.getValidateLottoAmount();
 const lottos = this.getLottos(lottoCount);

이 2개를 하나의 함수로 묶어서 lottos만 반환하는 함수를 만들면 되지 않을까요?

@liswktjs liswktjs merged commit 70b18fd into woowacourse:jaeml06 Feb 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants