-
Notifications
You must be signed in to change notification settings - Fork 192
[클린코드 8기 조세민] 로또 미션 STEP1 #328
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
Changes from all commits
54db27f
6424440
95fe914
c2c32c9
5e8718d
fe9692f
c8486c8
730d0a6
5dc4e3b
bb8cb74
08345b5
75de237
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
| 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()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| }) | ||
| }) | ||
| }) | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. winningNumbers를 beforeEach를 돌 때마다 초기화하도록 선언하신 이유가 있을까요? 물론, 클린 코드 관점에서 테스트 코드도 코드이기 때문에 중복되는 코드는 최대한 간결하게 유지해야 한다는 부분도 타당합니다. 하지만, 테스트 코드의 경우 검증 값을 확인해야 한다는 특성상 제 생각에는 반복되는 부분이 있다고 하더라도 하나의 테스트 케이스에 같이 선언해 주는 것이 보다 타당하다는 생각이 들었어요. 그래서 저의 경우, beforeEach는 테스트를 수행하기 이전에 초기화되어야 할 작업 내용들을 담을 때 쓰고 있어요. 그러면 저의 호(好)에 관해 예시를 포함해서 설명해 드릴게요! 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) | ||
| }) | ||
| }) | ||
| 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) | ||
| - 수익률은 소수점 셋째 자리에서 반올림하여 둘째 자리까지만 유지한다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| function ticketPriceTooLow() { | ||
| return new Error('구매 금액은 최소 1,000원 이상이어야 합니다.') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어떤 의도로 만드신 것인지는 이해가 되었지만, 함수의 이름은 동사로 시작해야 하는데 |
||
| } | ||
|
|
||
| export const LottoError = { | ||
| TicketPriceTooLow: ticketPriceTooLow, | ||
| } | ||
| 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getTicket이 반환하는 건 티켓의 배열이라고 이해했습니다! 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) | ||
| } | ||
| 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 } | ||
| } |
| 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()}% 입니다.`) | ||
| } |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 다른 리뷰이분께 드린 피드백과 유사한 질문을 드리겠습니다! 질문: Number객체로 변환했을 때 어떤 우려사항이 있을까요? 이 우려사항을 해소하기 위해서는 어떻게 개선해 볼 수 있을까요? 우려사항 Type 1
우려사항 Type 2
우려사항 Type 3
|
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자가 입력한 값과, 사용자의 입력 값을 토대로 winingNumber를 추출한 값을 따로 따로 선언해 준다면 보다 분명해지고 다만, 도메인이 복잡하지 않기도 하고 살려뒀다고 해서 고쳐야 하는 코드라고 생각지는 않습니다!
Suggested change
|
||||||||||
|
|
||||||||||
| 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() | ||||||||||
| 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); | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lottoGame.test.js와 일관성을 맞추시면 좋겠어요!천 단위 구분 기호를 사용해 맞춰 주세요 😊