Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions __tests__/lotto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { generateLottoNumber, getTicket } from '../src/lotto.js'
import { LottoError } from '../src/errors/lottoError.js'

describe('getTicket 함수 테스트', () => {
it('1,000원을 입력하면 1장을 발급한다', () => {
const money = 1000
Copy link
Contributor

Choose a reason for hiding this comment

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

lottoGame.test.js와 일관성을 맞추시면 좋겠어요!
천 단위 구분 기호를 사용해 맞춰 주세요 😊

Suggested change
const money = 1000
const money = 1_000


expect(getTicket(money)).toHaveLength(1)
})

it('1,000원 단위로 티켓을 발급하며, 2,500원을 입력하면 2장을 발급한다', () => {
const money = 2500

expect(getTicket(money)).toHaveLength(2)
})

it('1,000원 미만은 에러를 발생시킨다', () => {
const money = 900

expect(() => getTicket(money)).toThrow(LottoError.TicketPriceTooLow())
Copy link
Contributor

Choose a reason for hiding this comment

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

LottoError객체에서 예외 메시지를 생성하는 함수를 호출해서 검증한다는 접근법이 틀렸다고 말씀드리고 싶지 않습니다!
그렇지만, 조금 제가 익숙지 않은 코드 스타일이기도 해서요. 오히려 아래와 같이 커스텀 클래스로 관리해 보시는 건 어떠신가요? 그게 좀 더 컨벤션(명사)에도 부합하는 접근법일 것 같아요!

class TicketPriceTooLowError extends Error {
  constructor() {
    super('구매 금액은 최소 1,000원 이상이어야 합니다.')
    this.name = 'TicketPriceTooLowError'
  }
}
expect(() => getTicket(money)).toThrow(TicketPriceTooLowError)

(혹시)... 함수형 프로그래밍으로 작성하고 싶으셔서 이런 접근법을 취하신 거라면.... 저는 둘 다 적재적소에 맞게 사용하는 방법을 익히시는 편이 좋겠다는 생각이 듭니다. 자바스크립트는 멀티 패러다임 프로그래밍 언어이기도 하고요. 이에 관해서는 9/9 피드백 강의에서 다룰 예정이니 많관부탁드립니다 🙇‍♂️

})

it('금액을 입력하지 않으면 에러를 발생시킨다', () => {
expect(() => getTicket()).toThrow(LottoError.TicketPriceTooLow())
})
})

describe('generateLottoNumber 함수 테스트', () => {
it('6개의 숫자로 구성된다', () => {
const numbers = generateLottoNumber()

expect(numbers).toHaveLength(6)
})

it('6개의 숫자는 중복되지 않는다', () => {
const numbers = new Set(generateLottoNumber())

expect(numbers.size).toBe(6)
})

it('1~45 범위의 숫자들로 구성된다', () => {
const numbers = generateLottoNumber()

numbers.forEach(number => {
expect(number).toBeGreaterThanOrEqual(1)
expect(number).toBeLessThanOrEqual(45)
})
})
})
55 changes: 55 additions & 0 deletions __tests__/lottoGame.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { calculateProfitRate, getRank } from '../src/lottoGame.js'

describe('getRank 함수 테스트', () => {
let winningNumbers
let winningBonusNumber

beforeEach(() => {
winningNumbers = [1, 2, 3, 4, 5, 6]
winningBonusNumber = 7
})
Comment on lines +4 to +10
Copy link
Contributor

Choose a reason for hiding this comment

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

winningNumbers를 beforeEach를 돌 때마다 초기화하도록 선언하신 이유가 있을까요?
어림짐작하는 의도로는, 동일한 검증 값이기도 하고 재사용 목적으로 두신 것 같기도 합니다. 충분합니다 👍🏻. 그렇지만 입체적인 관점을 제시해 드리기 위해 아래의 코멘트도 남겨 드려요! (틀린 게 아닙니다! 하나의 관점으로만 해석하세요)


물론, 클린 코드 관점에서 테스트 코드도 코드이기 때문에 중복되는 코드는 최대한 간결하게 유지해야 한다는 부분도 타당합니다. 하지만, 테스트 코드의 경우 검증 값을 확인해야 한다는 특성상 제 생각에는 반복되는 부분이 있다고 하더라도 하나의 테스트 케이스에 같이 선언해 주는 것이 보다 타당하다는 생각이 들었어요.

그래서 저의 경우, beforeEach는 테스트를 수행하기 이전에 초기화되어야 할 작업 내용들을 담을 때 쓰고 있어요.
이러한 내용은 E2E Cypress 테스트에서 익숙해지실 거예요👍🏻 (예를 들어, 웹 페이지 초기화를 위해 검증 페이지로 이동하는 작업)

그러면 저의 호(好)에 관해 예시를 포함해서 설명해 드릴게요!
저는 검증 값을 모두 상수 선언을 해두면서 명시적으로 확인할 것 같아요 :)
그게 BDD 관점에서 보다 가깝다고 생각했기 때문입니다.

it("...", () => {
    // given
    const winningNumbers = [1, 2, 3, 4, 5, 6]
    const winningBonusNumber = 7
    const 상금 = 5_000

   // when
    const ticketNumbers = [1, 2, 3, 14, 15, 16] // 검증 대상
    const rank = getRank(ticketNumbers, winningNumbers, winningBonusNumber) // 검증될 rank 객체

   // then
    expect(rank.prize).toBe(상금)
});


it('숫자 3개가 일치하면 5,000원을 받는다', () => {
const ticketNumbers = [1, 2, 3, 14, 15, 16]
const rank = getRank(ticketNumbers, winningNumbers, winningBonusNumber)

expect(rank.prize).toBe(5_000)
})

it('보너스 번호 제외 숫자 5개가 일치하면 1,500,000원을 받는다', () => {
const ticketNumbers = [1, 2, 3, 4, 5, 16]
const rank = getRank(ticketNumbers, winningNumbers, winningBonusNumber)

expect(rank.prize).toBe(1_500_000)
})

it('보너스 번호 포함 숫자 6개가 일치하면 30,000,000원을 받는다', () => {
const ticketNumbers = [1, 2, 3, 4, 5, 7]
const rank = getRank(ticketNumbers, winningNumbers, winningBonusNumber)

expect(rank.prize).toBe(30_000_000)
})

it('숫자 6개가 전부 일치하면 2,000,000,000원을 받는다', () => {
const ticketNumbers = [1, 2, 3, 4, 5, 6]
const rank = getRank(ticketNumbers, winningNumbers, winningBonusNumber)

expect(rank.prize).toBe(2_000_000_000)
})
})

describe('calculateProfitRate 함수 테스트', () => {
it('구매 금액과 당첨금으로 총 수익률을 계산한다', () => {
const purchaseAmount = 10_000
const totalWinnings = 50_000

expect(calculateProfitRate(purchaseAmount, totalWinnings)).toBe(500)
})

it('수익률은 소수점 셋째 자리에서 반올림하여 둘째 자리까지만 유지한다', () => {
const purchaseAmount = 30_000
const totalWinnings = 5_000

expect(calculateProfitRate(purchaseAmount, totalWinnings)).toBe(16.67)
})
})
28 changes: 28 additions & 0 deletions src/docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 기능 요구 사항

## 1단계 - 콘솔 기반 로또 게임

### 입출력
-[x] 사용자로부터 구입 금액을 입력받는다.
-[x] 사용자로부터 당첨 번호(6개)와 보너스 번호(1개)를 입력받는다.
-[x] 발행된 로또 번호들을 출력한다.
-[x] 등수별 당첨 개수를 출력한다.
- `3개 일치 (5,000원) - 1개` 의 형식으로 출력한다.
-[x] 총 수익률을 출력한다.

### 로또
-[x] 로또 1장의 가격은 1,000원이다.
-[x] 구입 금액에 따라 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
-[x] 로또 1장은 1~45 범위의 중복되지 않은 6개의 숫자로 구성된다.

### 로또 게임
-[x] 입력받은 당첨 번호와 보너스 번호를 검증한다.
-[x] 사용자가 구매한 로또 번호와 당첨 번호를 비교해 등수를 판정한다.
-[x] 당첨 기준 및 상금은 다음과 같다:
- 1등: 6개 번호 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원
-[x] 총 수익률을 계산한다. (총 당첨금 ÷ 구입 금액 * 100)
- 수익률은 소수점 셋째 자리에서 반올림하여 둘째 자리까지만 유지한다.
7 changes: 7 additions & 0 deletions src/errors/lottoError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function ticketPriceTooLow() {
return new Error('구매 금액은 최소 1,000원 이상이어야 합니다.')
Copy link
Contributor

Choose a reason for hiding this comment

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

어떤 의도로 만드신 것인지는 이해가 되었지만, 함수의 이름은 동사로 시작해야 하는데 ticketPriceTooLow라는 함수에서 ticket은 동사의 의미로 쓰인 것 같지는 않습니다. 테스트 코드에서 피드백을 드렸던 것처럼 커스텀 오류 클래스를 만들고 관리하시는 접근법으로 방향을 전환하신다면 조금 더 자연스러워질 것 같아요!

}

export const LottoError = {
TicketPriceTooLow: ticketPriceTooLow,
}
24 changes: 24 additions & 0 deletions src/lotto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { LottoError } from './errors/lottoError.js'

export const LOTTO_PRICE = 1000
const LOTTO_NUMBERS_LENGTH = 6

export function getTicket(money) {
Copy link
Contributor

Choose a reason for hiding this comment

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

getTicket이 반환하는 건 티켓의 배열이라고 이해했습니다!
그렇다면 getTickets가 되어야 하지 않을까요?

return ([
  [1,2,3,4,5,6],
  [2,3,4,5,6,7],
]) // 로또 배열

if (!money || money < LOTTO_PRICE) {
throw LottoError.TicketPriceTooLow()
}

const ticketCount = Math.floor(money / LOTTO_PRICE)
return Array.from({ length: ticketCount }, () => generateLottoNumber())
}

export function generateLottoNumber() {
const numbers = new Set()

while (numbers.size < LOTTO_NUMBERS_LENGTH) {
const num = Math.floor(Math.random() * 45) + 1
numbers.add(num)
}

return Array.from(numbers)
}
47 changes: 47 additions & 0 deletions src/lottoGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const LOTTO_RANK_TABLE = [
{ rank: 1, matchCount: 6, prize: 2_000_000_000 },
{ rank: 2, matchCount: 6, prize: 30_000_000, bonus: true },
{ rank: 3, matchCount: 5, prize: 1_500_000 },
{ rank: 4, matchCount: 4, prize: 50_000 },
{ rank: 5, matchCount: 3, prize: 5_000 },
]

export function getRank(ticketNumbers, winningNumbers, winningBonusNumber) {
const matchCount = ticketNumbers.filter(num => winningNumbers.includes(num)).length
const hasBonusNumber = ticketNumbers.includes(winningBonusNumber)

return LOTTO_RANK_TABLE.find(rank => {
if (rank.bonus) {
return hasBonusNumber && rank.matchCount === matchCount
}

return rank.matchCount === matchCount
})
}

export function calculateProfitRate(purchaseAmount, totalWinnings) {
const rate = (totalWinnings / purchaseAmount) * 100

return Math.round(rate * 100) / 100
}

export function getRankStatistics(results) {
let totalWinnings = 0
const stats = LOTTO_RANK_TABLE.map(rank => ({
...rank,
count: 0,
}))

results.forEach(result => {
totalWinnings += result.prize
const rankIndex = stats.findIndex(rank =>
rank.matchCount === result.matchCount && rank.bonus === result.bonus
)

if (rankIndex !== -1) {
stats[rankIndex].count += 1
}
})

return { stats: stats.reverse(), totalWinnings }
}
22 changes: 22 additions & 0 deletions src/lottoPrint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function printTicket(tickets) {
console.log(`${tickets.length}개를 구매했습니다.`)
tickets.forEach(ticket => console.log(ticket))
}

export function printResults(stats, totalWinnings) {
if (!totalWinnings) {
console.log('낙첨되었습니다.')
return
}

console.log('당첨 통계')
console.log('--------------------')

stats.forEach(({ matchCount, prize, bonus, count }) => {
console.log(`${matchCount}개 일치${bonus ? ', 보너스 볼 일치': ''} (${prize.toLocaleString()}원) - ${count.toLocaleString()}개`)
})
}

export function printProfit(profitRate) {
console.log(`총 수익률은 ${profitRate.toLocaleString()}% 입니다.`)
}
41 changes: 37 additions & 4 deletions src/step1-index.js
Copy link
Contributor

Choose a reason for hiding this comment

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

제가 다른 리뷰이분께 드린 피드백과 유사한 질문을 드리겠습니다!
비슷한 현상이 나타나고 있습니다.
이에 관해서 테스트 케이스도 함께 작성해 두시면 좋겠죠!


질문: Number객체로 변환했을 때 어떤 우려사항이 있을까요? 이 우려사항을 해소하기 위해서는 어떻게 개선해 볼 수 있을까요?
또는 의도적으로 Number로 변환하신 것인지 제가 생각한 아래의 우려사항을 검토하셔서 의견을 남겨 주세요!

우려사항 Type 1

  1. 금액 입력란에 Infinity, -Infinity 입력한다면? 배열 에러가 발생하네요!
  2. 금액 입력란에 1999원을 입력한다면? 1장이 구매됩니다.

우려사항 Type 2

  1. 사용자가 당첨번호/보너스 번호에 Infinity를 입력한다면?
  2. 사용자가 당첨번호/보너스 번호에 유리수(소숫점이 있는 숫자)를 입력한다면?
  3. 사용자가 당첨번호/보너스 번호에 음의 정수를 입력한다면?
  4. 사용자가 당첨번호/보너스 번호에 지수(1e2)를 입력한다면?
  5. 사용자가 당첨번호/보너스 번호에 보너스 번호에 아무것도 입력하지 않는다면?

우려사항 Type 3

  1. 사용자가 당첨번호에 ,,,,,를 입력한다면?

Original file line number Diff line number Diff line change
@@ -1,4 +1,37 @@
/**
* step 1의 시작점이 되는 파일입니다.
* 브라우저 환경에서 사용하는 css 파일 등을 불러올 경우 정상적으로 빌드할 수 없습니다.
*/
import { readLineAsync } from './utils/readlineAsync.js'
import { getTicket, LOTTO_PRICE } from './lotto.js'
import { calculateProfitRate, getRank, getRankStatistics } from './lottoGame.js'
import { printProfit, printResults, printTicket } from './lottoPrint.js'

async function start() {
const tickets = await getTickets()
printTicket(tickets)

const { winningNumbers, winningBonusNumber } = await getWinningNumber()
const { stats, totalWinnings } = getStatistics(tickets, winningNumbers, winningBonusNumber)
printResults(stats, totalWinnings)

const profitRate = calculateProfitRate(tickets.length * LOTTO_PRICE, totalWinnings)
printProfit(profitRate)
}

async function getTickets() {
const purchaseAmount = await readLineAsync('구입 금액을 입력해 주세요. ')
return getTicket(purchaseAmount)
}

async function getWinningNumber() {
let winningNumbers = await readLineAsync('당첨 번호를 입력해 주세요. ')
winningNumbers = winningNumbers.split(',').map(num => Number(num.trim()))
Comment on lines +24 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

사용자가 입력한 값과, 사용자의 입력 값을 토대로 winingNumber를 추출한 값을 따로 따로 선언해 준다면 보다 분명해지고 let 키워드의 사용 빈도도 줄어들 수 있을 거예요 :)

다만, 도메인이 복잡하지 않기도 하고 살려뒀다고 해서 고쳐야 하는 코드라고 생각지는 않습니다!

Suggested change
let winningNumbers = await readLineAsync('당첨 번호를 입력해 주세요. ')
winningNumbers = winningNumbers.split(',').map(num => Number(num.trim()))
const inputWinningNumbers = await readLineAsync('당첨 번호를 입력해 주세요. ')
const winningNumbers = winningNumbers.split(',').map(num => Number(num.trim()))


const winningBonusNumber = await readLineAsync('보너스 번호를 입력해 주세요. ')

return { winningNumbers, winningBonusNumber: Number(winningBonusNumber) }
}

function getStatistics(tickets, winningNumbers, winningBonusNumber) {
const results = tickets.map(ticket => getRank(ticket, winningNumbers, winningBonusNumber)).filter(Boolean)
return getRankStatistics(results)
}

await start()
23 changes: 23 additions & 0 deletions src/utils/readlineAsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import readline from 'readline';

export function readLineAsync(query) {
return new Promise((resolve, reject) => {
if (arguments.length !== 1) {
reject(new Error('arguments must be 1'));
}

if (typeof query !== 'string') {
reject(new Error('query must be string'));
}

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

rl.question(query, (input) => {
rl.close();
resolve(input);
});
});
}