diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000000..d57fe415174
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,117 @@
+# 블랙잭 규칙
+
+## 참여자
+
+* [x] 다시한번 룰 숙지 필요
+
+**승**
+
+* (딜러 카드 합 < 참여자 카드 합) && 참여자가 버스트가 아닌 경우
+* 딜러가 버스트인 경우 && 참여자가 버스트가 아닌 경우
+
+**무**
+
+* (딜러 카드 합 == 참여자 카드 합) && 참여자가 버스트가 아닌 경우
+
+**패**
+
+* 참여자가 버스트인 경우
+* (딜러 카드 합 > 참여자 카드 합)
+
+
+
+## 블랙잭
+
+* 카드의 갯수가 2장이고 카드의 합이 21인 경우 블랙잭이다.
+
+
+
+# 기능 요구 사항
+
+* [x] 참여자 이름 입력 받는다.
+ * [x] 양 끝 공백을 제거한다.
+ * [x] 참여자는 쉼표(,)로 구분한다.
+ * [x] null 이나 공백인 경우 예외가 발생한다.
+ * [x] 쉼표로 시작시 예외가 발생한다. ex) ,pobi,jason
+ * [x] 쉼표로 끝날시 예외가 발생한다. ex) pobi,jason,
+ * [x] 쉼표가 연속으로 올시 예외가 발생한다. ex) pobi,,jason
+ * [x] 참여자 이름이 중복시 예외가 발생한다.
+* [x] 총 참여자의 수는 2이상 8이하여야 한다.
+
+
+
+* [x] 각 참여자의 배팅 금액을 입력받는다.
+ * [x] 정수가 아닐 시 예외가 발생한다.
+ * [x] 양의 정수가 아닐 시 예외가 발생한다.
+ * [x] 10으로 나누어 떨어지지 않으면 예외가 발생한다.
+ * 블랙잭시 1.5배를 해주기 위함
+ * [x] 배팅 금액은 최대 100_000_000로 이를 초과시 예외가 발생한다.
+ * 건전한 플레이를 하기 위함
+
+
+
+* [x] 딜러가 카드 2장을 분배하다.
+ * [x] 카드는 총 6벌을 둔다. (52 * 6)
+* [x] 딜러의 카드를 출력한다. (1장)
+* [x] 참여자의 카드를 출력한다. (2장)
+
+
+
+* [x] 참여자는 hit(y) / stay(n)를 선택한다.
+ * [x] y, n 가 아닐시 예외가 발생한다.
+* [x] 참여자가 hit을 하는 경우 현재 가지고 있는 카드를 출력한다.
+* [x] 참여자가 hit을 한 적 없이 stay를 하는 경우 현재 가지고 있는 카드를 출력한다.
+* [x] 카드 합이 블랙잭인 경우 블랙잭 메시지를 출력한다.
+* [x] 카드 합이 21 초과시 버스트 메시지를 출력한다.
+
+
+
+* [x] 딜러의 카드의 합을 계산한다.
+* [x] 카드 내의 ACE 가 포함된 경우
+ * ACE: 11
+ * 11로 했을 때 카드의 합이 21을 초과한 경우 1로 계산
+* [x] 17 이상이 될때까지 카드를 받는다.
+
+
+
+* [x] 모든 참여자의 카드의 합을 계산한다.
+* [x] 딜러의 승패무를 확인한다.
+* [x] 참여자의 승패무를 확인한다.
+* [x] 게임 결과를 출력한다.
+
+
+
+* [x] 딜러의 최종 수익을 계산한다.
+ * [x] 수익을 출력시 세자리마다 쉼표를 찍는다. ex) 100,000
+* [x] 참가자의 최종 수익을 계산한다.
+* [x] 참가자만 블랙잭인 경우 배팅 금액의 1.5배를 받는다.
+* [x] 참가자와 딜러 모두 블랙잭인 경우 배팅한 금액을 돌려받는다.
+* [x] 참가자가 패한다면 배팅한 금액을 잃는다.
+* [x] 참가자가 승리한다면 배팅한 금액을 받는다.
+
+---
+
+## 리팩터링 목록
+
+~~추상 클래스 활용~~
+
+* [x] 카드덱이 비었을때 꺼낸 경우 예외 발생
+* [x] createEmptyPacket 메서드명 수정
+* [x] 참가자의 이름 딜러 불가
+* [x] Name 객체 포장하기
+* [x] Result 함수형 프로그래밍 사용
+* [x] TestFixture 사용
+* [x] 패키지 정리
+* [x] 메서드 컨벤션 작성 및 확인
+* [x] final 컨벤션 통일
+* [x] CardDeck 생성자 닫기
+
+
+
+* [x] 예외 시 재입력 기능
+* [x] 예외 메시지 상수화
+ * 커스텀 예외 구현
+* [x] CardDeck 웨어하우스로 바라보기
+* [ ] ~~컨트롤러 메서드 수정하기~~
+* [x] ~~Participant 추상 클래스 대신 클래스로 변경하기~~
+ * 추상 클래스를 유지하고 추상 메서드 추가
diff --git a/src/main/java/application/Application.java b/src/main/java/application/Application.java
new file mode 100644
index 00000000000..2d98d55bfcb
--- /dev/null
+++ b/src/main/java/application/Application.java
@@ -0,0 +1,18 @@
+package application;
+
+import controller.BlackJackController;
+import controller.InputController;
+import java.util.Scanner;
+import view.InputView;
+import view.OutputView;
+
+public class Application {
+ public static void main(String[] args) {
+ final Scanner scanner = new Scanner(System.in);
+ final InputView inputView = new InputView(scanner);
+ final OutputView outputView = new OutputView();
+ final InputController inputController = new InputController(inputView, outputView);
+ final BlackJackController blackJackController = new BlackJackController(inputController, outputView);
+ blackJackController.run();
+ }
+}
diff --git a/src/main/java/constants/ErrorCode.java b/src/main/java/constants/ErrorCode.java
new file mode 100644
index 00000000000..666205e8576
--- /dev/null
+++ b/src/main/java/constants/ErrorCode.java
@@ -0,0 +1,15 @@
+package constants;
+
+public enum ErrorCode {
+
+ NOT_EXIST_MESSAGE,
+ INVALID_SEPARATOR,
+ INVALID_INPUT,
+ INVALID_SIZE,
+ INVALID_BET_AMOUNT,
+ DUPLICATE_NAME,
+ RESERVED_NAME,
+ BLANK_VALUE,
+ EMPTY_CARD,
+ INVALID_COMMAND,
+}
diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java
new file mode 100644
index 00000000000..6e680b54054
--- /dev/null
+++ b/src/main/java/controller/BlackJackController.java
@@ -0,0 +1,76 @@
+package controller;
+
+import domain.Game;
+import domain.amount.Amount;
+import domain.amount.BetAmount;
+import domain.participant.Dealer;
+import domain.participant.Name;
+import domain.participant.Names;
+import domain.participant.Player;
+import domain.participant.Players;
+import dto.ParticipantsDto;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import view.OutputView;
+
+public class BlackJackController {
+
+ private final InputController inputController;
+ private final OutputView outputView;
+
+ public BlackJackController(final InputController inputController, final OutputView outputView) {
+ this.inputController = inputController;
+ this.outputView = outputView;
+ }
+
+ public void run() {
+ final Names names = inputController.getNames();
+ final Dealer dealer = new Dealer();
+ final Players players = registerPlayers(names);
+
+ final Game game = new Game(dealer, players);
+
+ initHands(game);
+ dealWithPlayers(game);
+ dealerTurn(game);
+
+ printFinalResult(players, dealer);
+ }
+
+ private void dealerTurn(final Game game) {
+ game.dealerTurn(outputView::printDealerTurnMessage);
+ }
+
+ private Players registerPlayers(final Names names) {
+ final List playerList = new ArrayList<>();
+ for (final Name name : names.getPlayerNames()) {
+ final BetAmount betAmount = inputController.getBetAmount(name.getValue());
+ playerList.add(new Player(name, betAmount));
+ }
+ return new Players(playerList);
+ }
+
+ private void dealWithPlayers(final Game game) {
+ if (game.isAlreadyEnd()) {
+ outputView.printDealerBlackJack();
+ return;
+ }
+ game.checkingPlayersBlackJack(outputView::printBlackJack);
+ game.dealWithPlayers(
+ inputController::getAnswer,
+ outputView::printHands,
+ outputView::printPlayerEndMessage);
+ }
+
+ private void initHands(final Game game) {
+ game.initHands(outputView::printStartDeal);
+ }
+
+ private void printFinalResult(final Players players, final Dealer dealer) {
+ outputView.printHandsResult(ParticipantsDto.of(dealer, players));
+ final Map finalResult = players.calculateResult(dealer);
+ final Amount dealerAmount = dealer.calculateRevenue(finalResult);
+ outputView.printGameResult(finalResult, dealerAmount);
+ }
+}
diff --git a/src/main/java/controller/InputController.java b/src/main/java/controller/InputController.java
new file mode 100644
index 00000000000..ca00c39625d
--- /dev/null
+++ b/src/main/java/controller/InputController.java
@@ -0,0 +1,74 @@
+package controller;
+
+import domain.participant.Answer;
+import domain.amount.BetAmount;
+import domain.participant.Names;
+import exception.CustomException;
+import java.util.List;
+import view.InputView;
+import view.OutputView;
+
+public class InputController {
+
+ private final InputView inputView;
+ private final OutputView outputView;
+
+ public InputController(final InputView inputView, final OutputView outputView) {
+ this.inputView = inputView;
+ this.outputView = outputView;
+ }
+
+ public Names getNames() {
+ Names names;
+ do {
+ names = readNames();
+ } while (names == null);
+ return names;
+ }
+
+ public Answer getAnswer(String name) {
+ Answer answer;
+ do {
+ answer = readAnswer(name);
+ } while (answer == null);
+ return answer;
+ }
+
+ public BetAmount getBetAmount(String name) {
+ BetAmount betAmount;
+ do {
+ betAmount = readBetAmount(name);
+ } while (betAmount == null);
+ return betAmount;
+ }
+
+ private Names readNames() {
+ try {
+ List rawNames = inputView.readNames();
+ return Names.from(rawNames);
+ } catch (CustomException exception) {
+ outputView.printException(exception.getErrorCode());
+ return null;
+ }
+ }
+
+ private Answer readAnswer(String name) {
+ try {
+ String value = inputView.readAnswer(name);
+ return Answer.from(value);
+ } catch (CustomException exception) {
+ outputView.printException(exception.getErrorCode());
+ return null;
+ }
+ }
+
+ private BetAmount readBetAmount(final String name) {
+ try {
+ Long value = inputView.readBetAmount(name);
+ return new BetAmount(value);
+ } catch (CustomException exception) {
+ outputView.printException(exception.getErrorCode());
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/domain/Game.java b/src/main/java/domain/Game.java
new file mode 100644
index 00000000000..ff12a57050e
--- /dev/null
+++ b/src/main/java/domain/Game.java
@@ -0,0 +1,94 @@
+package domain;
+
+import domain.participant.Answer;
+import domain.participant.Dealer;
+import domain.participant.Participant;
+import domain.participant.Player;
+import domain.participant.Players;
+import dto.DealerHandsDto;
+import dto.ParticipantDto;
+import dto.ParticipantsDto;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class Game {
+
+ private final Dealer dealer;
+ private final Players players;
+
+ public Game(final Dealer dealer, final Players players) {
+ this.dealer = dealer;
+ this.players = players;
+ }
+
+ public void initHands(BiConsumer handsPrinter) {
+ dealer.initHands(players);
+ handsPrinter.accept(DealerHandsDto.from(dealer), ParticipantsDto.of(players));
+ }
+
+ public boolean isAlreadyEnd() {
+ return dealer.isBlackJack();
+ }
+
+ public void checkingPlayersBlackJack(Consumer blackJackPrinter) {
+ players.filter(Participant::isBlackJack)
+ .forEach(player -> blackJackPrinter.accept(player.getName()));
+ }
+
+ public void dealWithPlayers(Function answerReader,
+ Consumer handsPrinter,
+ Consumer endMessagePrinter) {
+ players.forEach(player -> deal(player, answerReader,
+ handsPrinter,
+ endMessagePrinter
+ ));
+ }
+
+ public void deal(final Player player, Function answerReader,
+ Consumer handsPrinter,
+ Consumer endMessagePrinter) {
+ if (player.isBlackJack()) {
+ return;
+ }
+ boolean handsChanged = false;
+ boolean turnEnded = false;
+
+ while (!turnEnded) {
+ final Answer answer = answerReader.apply(player.getName());
+ dealer.deal(player, answer);
+ printHandsIfRequired(player, handsChanged, answer, handsPrinter);
+ handsChanged = true;
+ turnEnded = isTurnEnded(player, answer, endMessagePrinter);
+ }
+ }
+
+ public void dealerTurn(Runnable dealerTurnMessagePrinter) {
+ if (players.isAllBust()) {
+ return;
+ }
+ dealer.deal();
+ for (int i = 0; i < dealer.countAddedHands(); i++) {
+ dealerTurnMessagePrinter.run();
+ }
+ }
+
+ private void printHandsIfRequired(final Player player, final boolean handsChanged, final Answer answer,
+ Consumer handsPrinter) {
+ if (shouldShowHands(handsChanged, answer)) {
+ handsPrinter.accept(ParticipantDto.from(player));
+ }
+ }
+
+ private boolean isTurnEnded(final Player player, final Answer answer, Consumer endMessagePrinter) {
+ if (player.canDeal()) {
+ return !answer.isHit();
+ }
+ endMessagePrinter.accept(player.isBust());
+ return true;
+ }
+
+ private boolean shouldShowHands(final boolean handsChanged, final Answer answer) {
+ return answer.isHit() || !handsChanged;
+ }
+}
diff --git a/src/main/java/domain/GameResult.java b/src/main/java/domain/GameResult.java
new file mode 100644
index 00000000000..926a3bcdd69
--- /dev/null
+++ b/src/main/java/domain/GameResult.java
@@ -0,0 +1,61 @@
+package domain;
+
+import domain.amount.Amount;
+import domain.amount.BetAmount;
+import domain.participant.Hands;
+import java.util.Arrays;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+public enum GameResult {
+
+ BLACK_JACK_WIN(GameResult::blackJackWinningCondition, BetAmount::blackJackWinAmount),
+ WIN(GameResult::winningCondition, BetAmount::winAmount),
+ TIE(GameResult::tieCondition, BetAmount::tieAmount),
+ LOSE(GameResult::loseCondition, BetAmount::loseAmount);
+
+ private final BiPredicate condition;
+ private final Function calculate;
+
+ GameResult(final BiPredicate condition, final Function calculate) {
+ this.condition = condition;
+ this.calculate = calculate;
+ }
+
+ public GameResult reverse() {
+ if (GameResult.WIN.equals(this) || GameResult.BLACK_JACK_WIN.equals(this)) {
+ return LOSE;
+ }
+ if (GameResult.LOSE.equals(this)) {
+ return WIN;
+ }
+ return TIE;
+ }
+
+ public static GameResult calculateOf(final Hands hands, final Hands target) {
+ return Arrays.stream(GameResult.values())
+ .filter(result -> result.condition.test(hands, target))
+ .findFirst()
+ .orElseThrow();
+ }
+
+ public Amount apply(BetAmount betAmount) {
+ return calculate.apply(betAmount);
+ }
+
+ private static boolean blackJackWinningCondition(Hands hands, Hands target) {
+ return hands.isBlackJack() && !target.isBlackJack();
+ }
+
+ private static boolean winningCondition(final Hands hands, final Hands target) {
+ return (!hands.isBust() && target.isBust()) || (hands.sum() > target.sum() && !hands.isBust());
+ }
+
+ private static boolean tieCondition(final Hands hands, final Hands target) {
+ return hands.sum() == target.sum() && !hands.isBust();
+ }
+
+ private static boolean loseCondition(final Hands hands, final Hands target) {
+ return hands.isBust() || hands.sum() < target.sum() || !target.isBust();
+ }
+}
diff --git a/src/main/java/domain/amount/Amount.java b/src/main/java/domain/amount/Amount.java
new file mode 100644
index 00000000000..15d899564d0
--- /dev/null
+++ b/src/main/java/domain/amount/Amount.java
@@ -0,0 +1,37 @@
+package domain.amount;
+
+import java.util.Objects;
+
+public class Amount {
+
+ private final long value;
+
+ public Amount(final long value) {
+ this.value = value;
+ }
+
+ public long getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Amount amount)) {
+ return false;
+ }
+ return Objects.equals(value, amount.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+}
diff --git a/src/main/java/domain/amount/BetAmount.java b/src/main/java/domain/amount/BetAmount.java
new file mode 100644
index 00000000000..dd7d46a75ad
--- /dev/null
+++ b/src/main/java/domain/amount/BetAmount.java
@@ -0,0 +1,74 @@
+package domain.amount;
+
+import constants.ErrorCode;
+import exception.InvalidBetAmountException;
+import exception.OutOfRangeBetAmount;
+import java.util.Objects;
+
+public class BetAmount {
+
+ private static final int UNIT = 10;
+ private static final int MIN_RANGE = 10;
+ private static final int MAX_RANGE = 100_000_000;
+
+ private final long value;
+
+ public BetAmount(final long value) {
+ validate(value);
+ this.value = value;
+ }
+
+ public Amount loseAmount() {
+ return new Amount(-value);
+ }
+
+ public Amount blackJackWinAmount() {
+ return new Amount(value + (value / 2));
+ }
+
+ public Amount winAmount() {
+ return new Amount(value);
+ }
+
+ public Amount tieAmount() {
+ return new Amount(0);
+ }
+
+ private void validate(final long value) {
+ validateRange(value);
+ validateUnit(value);
+ }
+
+ private void validateUnit(final long value) {
+ if (value % UNIT != 0) {
+ throw new InvalidBetAmountException(ErrorCode.INVALID_BET_AMOUNT);
+ }
+ }
+
+ private void validateRange(final long value) {
+ if (value < MIN_RANGE || MAX_RANGE < value) {
+ throw new OutOfRangeBetAmount(ErrorCode.INVALID_BET_AMOUNT);
+ }
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof BetAmount betAmount)) {
+ return false;
+ }
+ return Objects.equals(value, betAmount.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+}
diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java
new file mode 100644
index 00000000000..f32cdd138f5
--- /dev/null
+++ b/src/main/java/domain/card/Card.java
@@ -0,0 +1,63 @@
+package domain.card;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class Card {
+
+ private static final List CACHE = new ArrayList<>();
+
+ static {
+ for (Shape shape : Shape.values()) {
+ generateCard(shape);
+ }
+ }
+
+ private final Rank rank;
+ private final Shape shape;
+
+ public Card(final Rank rank, final Shape shape) {
+ this.rank = rank;
+ this.shape = shape;
+ }
+
+ private static void generateCard(final Shape shape) {
+ for (Rank rank : Rank.values()) {
+ CACHE.add(new Card(rank, shape));
+ }
+ }
+
+ public static List values() {
+ return List.copyOf(CACHE);
+ }
+
+ public boolean isAce() {
+ return rank.isAce();
+ }
+
+ public int getCardNumber() {
+ return rank.getValue();
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof Card card)) {
+ return false;
+ }
+ return Objects.equals(rank, card.rank) && Objects.equals(shape, card.shape);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rank, shape);
+ }
+
+ @Override
+ public String toString() {
+ return rank.getName() + shape.getName();
+ }
+}
diff --git a/src/main/java/domain/card/CardDeck.java b/src/main/java/domain/card/CardDeck.java
new file mode 100644
index 00000000000..b23329a53cd
--- /dev/null
+++ b/src/main/java/domain/card/CardDeck.java
@@ -0,0 +1,39 @@
+package domain.card;
+
+import constants.ErrorCode;
+import exception.NoMoreCardException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+
+public class CardDeck {
+
+ private final Deque cards;
+
+ public CardDeck(final Deque cards) {
+ this.cards = cards;
+ }
+
+ static CardDeck generate(int size) {
+ final List deck = new ArrayList<>();
+
+ for (int i = 0; i < size; i++) {
+ deck.addAll(Card.values());
+ }
+ Collections.shuffle(deck);
+ return new CardDeck(new ArrayDeque<>(deck));
+ }
+
+ public Card pop() {
+ if (cards.isEmpty()) {
+ throw new NoMoreCardException(ErrorCode.EMPTY_CARD);
+ }
+ return cards.pop();
+ }
+
+ public int size() {
+ return cards.size();
+ }
+}
diff --git a/src/main/java/domain/card/Rank.java b/src/main/java/domain/card/Rank.java
new file mode 100644
index 00000000000..aa92ef34659
--- /dev/null
+++ b/src/main/java/domain/card/Rank.java
@@ -0,0 +1,38 @@
+package domain.card;
+
+public enum Rank {
+
+ ACE("A", 1),
+ TWO("2", 2),
+ THREE("3", 3),
+ FOUR("4", 4),
+ FIVE("5", 5),
+ SIX("6", 6),
+ SEVEN("7", 7),
+ EIGHT("8", 8),
+ NINE("9", 9),
+ TEN("10", 10),
+ JACK("J", 10),
+ QUEEN("Q", 10),
+ KING("K", 10);
+
+ private final String name;
+ private final int value;
+
+ Rank(final String name, final int value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public boolean isAce() {
+ return this == ACE;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/domain/card/Shape.java b/src/main/java/domain/card/Shape.java
new file mode 100644
index 00000000000..093d73cfa8c
--- /dev/null
+++ b/src/main/java/domain/card/Shape.java
@@ -0,0 +1,19 @@
+package domain.card;
+
+public enum Shape {
+
+ SPADE("스페이드"),
+ HEART("하트"),
+ CLOVER("클로버"),
+ DIAMOND("다이아몬드");
+
+ private final String name;
+
+ Shape(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/domain/participant/Answer.java b/src/main/java/domain/participant/Answer.java
new file mode 100644
index 00000000000..0c110c0f7af
--- /dev/null
+++ b/src/main/java/domain/participant/Answer.java
@@ -0,0 +1,28 @@
+package domain.participant;
+
+import constants.ErrorCode;
+import exception.InvalidCommandException;
+import java.util.Arrays;
+
+public enum Answer {
+
+ HIT("y"),
+ STAY("n");
+
+ private final String value;
+
+ Answer(final String value) {
+ this.value = value;
+ }
+
+ public static Answer from(final String value) {
+ return Arrays.stream(Answer.values())
+ .filter(answer -> answer.value.equals(value))
+ .findFirst()
+ .orElseThrow(() -> new InvalidCommandException(ErrorCode.INVALID_COMMAND));
+ }
+
+ public boolean isHit() {
+ return HIT.equals(this);
+ }
+}
diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java
new file mode 100644
index 00000000000..a31b99cc618
--- /dev/null
+++ b/src/main/java/domain/participant/Dealer.java
@@ -0,0 +1,88 @@
+package domain.participant;
+
+import domain.amount.Amount;
+import domain.GameResult;
+import domain.card.Card;
+import domain.card.CardDeck;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+public class Dealer extends Participant {
+
+ public static final int INIT_HANDS_SIZE = 2;
+ public static final int THRESHOLD = 16;
+ public static final int DECK_SIZE = 6;
+ public static final Name DEALER_NAME = new Name("딜러");
+
+ private final CardDeck cardDeck;
+
+ public Dealer() {
+ super(DEALER_NAME, Hands.createEmptyHands());
+ this.cardDeck = new CardDeck(generate());
+ }
+
+ public Dealer(final Hands hands) {
+ super(DEALER_NAME, hands);
+ this.cardDeck = new CardDeck(generate());
+ }
+
+ private static ArrayDeque generate() {
+ final List deck = new ArrayList<>();
+ for (int i = 0; i < DECK_SIZE; i++) {
+ deck.addAll(Card.values());
+ }
+ Collections.shuffle(deck);
+ return new ArrayDeque<>(deck);
+ }
+
+ public void initHands(final Players players) {
+ for (int i = 0; i < INIT_HANDS_SIZE; i++) {
+ players.forEach(player -> player.add(cardDeck.pop()));
+ super.add(cardDeck.pop());
+ }
+ }
+
+ public void deal(final Player player, final Answer answer) {
+ if (answer.isHit()) {
+ player.add(cardDeck.pop());
+ }
+ }
+
+ public void deal() {
+ while (canDeal()) {
+ super.add(cardDeck.pop());
+ }
+ }
+
+ public int countAddedHands() {
+ return handsSize() - INIT_HANDS_SIZE;
+ }
+
+ public Map getDealerResult(final Players players) {
+ Map dealerResult = new EnumMap<>(GameResult.class);
+
+ for (GameResult value : players.getPlayersResult(this).values()) {
+ GameResult reversed = value.reverse();
+ dealerResult.put(reversed, dealerResult.getOrDefault(reversed, 0) + 1);
+ }
+ return dealerResult;
+ }
+
+ public Amount calculateRevenue(final Map finalResult) {
+ long dealerAmount = finalResult.values()
+ .stream()
+ .map(Amount::getValue)
+ .mapToLong(Long::longValue)
+ .sum();
+ return new Amount(-dealerAmount);
+ }
+
+ @Override
+ public boolean canDeal() {
+ return handsSum() <= THRESHOLD;
+ }
+}
diff --git a/src/main/java/domain/participant/Hands.java b/src/main/java/domain/participant/Hands.java
new file mode 100644
index 00000000000..89f6c1eb97e
--- /dev/null
+++ b/src/main/java/domain/participant/Hands.java
@@ -0,0 +1,94 @@
+package domain.participant;
+
+import domain.GameResult;
+import domain.card.Card;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class Hands {
+
+ public static final int BLACK_JACK = 21;
+ private static final int EXTRA_ACE_VALUE = 10;
+
+ private final List cards;
+
+ public Hands(final List cards) {
+ this.cards = new ArrayList<>(cards);
+ }
+
+ public static Hands createEmptyHands() {
+ return new Hands(new ArrayList<>());
+ }
+
+ public int sum() {
+ int total = cards.stream()
+ .mapToInt(Card::getCardNumber)
+ .sum();
+
+ return calculateTotalByAce(total);
+ }
+
+ public void add(final Card card) {
+ cards.add(card);
+ }
+
+ public boolean isBust() {
+ return sum() > BLACK_JACK;
+ }
+
+ public boolean isBlackJack() {
+ return sum() == BLACK_JACK && size() == 2;
+ }
+
+ private boolean hasAce() {
+ return cards.stream()
+ .anyMatch(Card::isAce);
+ }
+
+ public int size() {
+ return cards.size();
+ }
+
+ public List getCards() {
+ return cards.stream()
+ .map(Card::toString)
+ .toList();
+ }
+
+ public GameResult calculateResult(final Hands target) {
+ return GameResult.calculateOf(this, target);
+ }
+
+ private int calculateTotalByAce(final int total) {
+ if (hasAce() && total + EXTRA_ACE_VALUE <= BLACK_JACK) {
+ return total + EXTRA_ACE_VALUE;
+ }
+
+ return total;
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+
+ if (!(target instanceof Hands hands)) {
+ return false;
+ }
+
+ return Objects.equals(cards, hands.cards);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cards);
+ }
+
+ @Override
+ public String toString() {
+ return "Hands=" +
+ cards;
+ }
+}
diff --git a/src/main/java/domain/participant/Name.java b/src/main/java/domain/participant/Name.java
new file mode 100644
index 00000000000..d8d0744bc3f
--- /dev/null
+++ b/src/main/java/domain/participant/Name.java
@@ -0,0 +1,57 @@
+package domain.participant;
+
+import constants.ErrorCode;
+import exception.InvalidPlayerNameException;
+import java.util.Objects;
+
+public class Name {
+
+ private final String value;
+
+ public Name(final String value) {
+ validate(value);
+ this.value = value;
+ }
+
+ private void validate(final String name) {
+ validateNull(name);
+ validateBlank(name);
+ }
+
+ private void validateNull(final String name) {
+ if (name == null) {
+ throw new InvalidPlayerNameException(ErrorCode.BLANK_VALUE);
+ }
+ }
+
+ private void validateBlank(final String name) {
+ if (name.isBlank()) {
+ throw new InvalidPlayerNameException(ErrorCode.BLANK_VALUE);
+ }
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof Name name)) {
+ return false;
+ }
+ return Objects.equals(value, name.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/domain/participant/Names.java b/src/main/java/domain/participant/Names.java
new file mode 100644
index 00000000000..a6f5a1b67fa
--- /dev/null
+++ b/src/main/java/domain/participant/Names.java
@@ -0,0 +1,49 @@
+package domain.participant;
+
+import constants.ErrorCode;
+import exception.DuplicatePlayerNameException;
+import exception.InvalidPlayersSizeException;
+import java.util.List;
+import java.util.Set;
+
+public class Names {
+
+ private static final int MIN_SIZE = 2;
+ private static final int MAX_SIZE = 8;
+
+ private final List playerNames;
+
+ private Names(final List playerNames) {
+ this.playerNames = playerNames;
+ }
+
+ public static Names from(final List names) {
+ List result = names.stream()
+ .map(String::trim)
+ .map(Name::new)
+ .toList();
+ validate(result);
+ return new Names(result);
+ }
+
+ private static void validate(final List names) {
+ validateSize(names);
+ validateDuplicate(names);
+ }
+
+ private static void validateSize(final List names) {
+ if (names.size() < MIN_SIZE || MAX_SIZE < names.size()) {
+ throw new InvalidPlayersSizeException(ErrorCode.INVALID_SIZE);
+ }
+ }
+
+ private static void validateDuplicate(final List names) {
+ if (names.size() != Set.copyOf(names).size()) {
+ throw new DuplicatePlayerNameException(ErrorCode.DUPLICATE_NAME);
+ }
+ }
+
+ public List getPlayerNames() {
+ return playerNames;
+ }
+}
diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java
new file mode 100644
index 00000000000..09e1fff645b
--- /dev/null
+++ b/src/main/java/domain/participant/Participant.java
@@ -0,0 +1,79 @@
+package domain.participant;
+
+import domain.GameResult;
+import domain.card.Card;
+import java.util.List;
+import java.util.Objects;
+
+public abstract class Participant {
+
+ private final Name name;
+ private final Hands hands;
+
+ protected Participant(final Name name, final Hands hands) {
+ this.name = name;
+ this.hands = hands;
+ }
+
+ public abstract boolean canDeal();
+
+ public void add(final Card card) {
+ hands.add(card);
+ }
+
+ public boolean isBust() {
+ return hands.isBust();
+ }
+
+ public boolean isBlackJack() {
+ return hands.isBlackJack();
+ }
+
+ public int handsSum() {
+ return hands.sum();
+ }
+
+ public int handsSize() {
+ return hands.size();
+ }
+
+ public GameResult calculateResult(final Participant participant) {
+ return hands.calculateResult(participant.getHands());
+ }
+
+ public List getCardNames() {
+ return hands.getCards();
+ }
+
+ public String getName() {
+ return name.getValue();
+ }
+
+ public Hands getHands() {
+ return hands;
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof Participant participant)) {
+ return false;
+ }
+ return Objects.equals(name, participant.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ @Override
+ public String toString() {
+ return "Participant{" +
+ "name=" + name +
+ ", hands=" + hands +
+ '}';
+ }
+}
diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java
new file mode 100644
index 00000000000..48fd3197f7c
--- /dev/null
+++ b/src/main/java/domain/participant/Player.java
@@ -0,0 +1,65 @@
+package domain.participant;
+
+import static domain.participant.Dealer.DEALER_NAME;
+
+import constants.ErrorCode;
+import domain.amount.BetAmount;
+import exception.ReservedPlayerNameException;
+import java.util.Objects;
+
+public class Player extends Participant {
+
+ private final BetAmount betAmount;
+
+ Player(final Name name, final Hands hands, final BetAmount betAmount) {
+ super(name, hands);
+ this.betAmount = betAmount;
+ }
+
+ Player(final Name name, final Hands hands) {
+ super(name, hands);
+ validate(name);
+ this.betAmount = new BetAmount(10);
+ }
+
+ public Player(final Name name, final BetAmount betAmount) {
+ super(name, Hands.createEmptyHands());
+ this.betAmount = betAmount;
+ }
+
+ private void validate(final Name name) {
+ if (DEALER_NAME.equals(name)) {
+ throw new ReservedPlayerNameException(ErrorCode.RESERVED_NAME);
+ }
+ }
+
+ public BetAmount getBetAmount() {
+ return betAmount;
+ }
+
+ @Override
+ public boolean canDeal() {
+ return handsSum() < Hands.BLACK_JACK;
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof Player player)) {
+ return false;
+ }
+ return Objects.equals(getName(), player.getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(getName());
+ }
+
+ @Override
+ public String toString() {
+ return super.toString();
+ }
+}
diff --git a/src/main/java/domain/participant/Players.java b/src/main/java/domain/participant/Players.java
new file mode 100644
index 00000000000..bce0e099be4
--- /dev/null
+++ b/src/main/java/domain/participant/Players.java
@@ -0,0 +1,54 @@
+package domain.participant;
+
+import domain.GameResult;
+import domain.amount.Amount;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+public class Players {
+
+ private final List names;
+
+ public Players(final List names) {
+ this.names = names;
+ }
+
+ public void forEach(Consumer super Player> action) {
+ names.forEach(action);
+ }
+
+ public Stream filter(Predicate super Player> predicate) {
+ return names.stream().filter(predicate);
+ }
+
+ public boolean isAllBust() {
+ return names.stream()
+ .allMatch(Player::isBust);
+ }
+
+ public Map getPlayersResult(final Dealer dealer) {
+ final Map result = new LinkedHashMap<>();
+ for (Player name : names) {
+ result.put(name, name.calculateResult(dealer));
+ }
+ return result;
+ }
+
+ public Map calculateResult(final Dealer dealer) {
+ final Map playerAmountMap = new LinkedHashMap<>();
+
+ for (Player player : names) {
+ GameResult gameResult = player.calculateResult(dealer);
+ playerAmountMap.put(player, gameResult.apply(player.getBetAmount()));
+ }
+ return playerAmountMap;
+ }
+
+ public List getPlayers() {
+ return names;
+ }
+}
diff --git a/src/main/java/dto/DealerHandsDto.java b/src/main/java/dto/DealerHandsDto.java
new file mode 100644
index 00000000000..dae38677154
--- /dev/null
+++ b/src/main/java/dto/DealerHandsDto.java
@@ -0,0 +1,22 @@
+package dto;
+
+import domain.participant.Participant;
+import java.util.List;
+
+public class DealerHandsDto {
+
+ private final String displayedCard;
+
+ private DealerHandsDto(final String displayedCard) {
+ this.displayedCard = displayedCard;
+ }
+
+ public static DealerHandsDto from(final Participant dealer) {
+ List cards = dealer.getCardNames();
+ return new DealerHandsDto(cards.get(0));
+ }
+
+ public String getDisplayedCard() {
+ return displayedCard;
+ }
+}
diff --git a/src/main/java/dto/ParticipantDto.java b/src/main/java/dto/ParticipantDto.java
new file mode 100644
index 00000000000..795bae6b575
--- /dev/null
+++ b/src/main/java/dto/ParticipantDto.java
@@ -0,0 +1,11 @@
+package dto;
+
+import domain.participant.Participant;
+import java.util.List;
+
+public record ParticipantDto(String name, List cards, int totalSum) {
+
+ public static ParticipantDto from(final Participant player) {
+ return new ParticipantDto(player.getName(), player.getCardNames(), player.handsSum());
+ }
+}
diff --git a/src/main/java/dto/ParticipantsDto.java b/src/main/java/dto/ParticipantsDto.java
new file mode 100644
index 00000000000..c7a2984c4ab
--- /dev/null
+++ b/src/main/java/dto/ParticipantsDto.java
@@ -0,0 +1,42 @@
+package dto;
+
+import domain.participant.Participant;
+import domain.participant.Players;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ParticipantsDto {
+
+ private final List players;
+
+ private ParticipantsDto(final List players) {
+ this.players = players;
+ }
+
+ public static ParticipantsDto of(final Participant dealer, final Players players) {
+ List result = new ArrayList<>();
+ result.add(ParticipantDto.from(dealer));
+ for (Participant player : players.getPlayers()) {
+ result.add(ParticipantDto.from(player));
+ }
+ return new ParticipantsDto(result);
+ }
+
+ public static ParticipantsDto of(final Players players) {
+ List result = new ArrayList<>();
+ for (Participant player : players.getPlayers()) {
+ result.add(ParticipantDto.from(player));
+ }
+ return new ParticipantsDto(result);
+ }
+
+ public List getNames() {
+ return players.stream()
+ .map(ParticipantDto::name)
+ .toList();
+ }
+
+ public List getPlayers() {
+ return players;
+ }
+}
diff --git a/src/main/java/exception/CustomException.java b/src/main/java/exception/CustomException.java
new file mode 100644
index 00000000000..fa189d08d1f
--- /dev/null
+++ b/src/main/java/exception/CustomException.java
@@ -0,0 +1,16 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class CustomException extends RuntimeException {
+
+ private final ErrorCode errorCode;
+
+ public CustomException(final ErrorCode errorCode) {
+ this.errorCode = errorCode;
+ }
+
+ public ErrorCode getErrorCode() {
+ return errorCode;
+ }
+}
diff --git a/src/main/java/exception/DuplicatePlayerNameException.java b/src/main/java/exception/DuplicatePlayerNameException.java
new file mode 100644
index 00000000000..8eb7bb2b5ac
--- /dev/null
+++ b/src/main/java/exception/DuplicatePlayerNameException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class DuplicatePlayerNameException extends CustomException {
+
+ public DuplicatePlayerNameException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidBetAmountException.java b/src/main/java/exception/InvalidBetAmountException.java
new file mode 100644
index 00000000000..38c2ab15431
--- /dev/null
+++ b/src/main/java/exception/InvalidBetAmountException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidBetAmountException extends CustomException {
+
+ public InvalidBetAmountException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidCommandException.java b/src/main/java/exception/InvalidCommandException.java
new file mode 100644
index 00000000000..ea970cc7419
--- /dev/null
+++ b/src/main/java/exception/InvalidCommandException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidCommandException extends CustomException {
+
+ public InvalidCommandException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidInputException.java b/src/main/java/exception/InvalidInputException.java
new file mode 100644
index 00000000000..1ade7a5b234
--- /dev/null
+++ b/src/main/java/exception/InvalidInputException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidInputException extends CustomException {
+
+ public InvalidInputException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidPlayerNameException.java b/src/main/java/exception/InvalidPlayerNameException.java
new file mode 100644
index 00000000000..0d43def32d7
--- /dev/null
+++ b/src/main/java/exception/InvalidPlayerNameException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidPlayerNameException extends CustomException {
+
+ public InvalidPlayerNameException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidPlayersSizeException.java b/src/main/java/exception/InvalidPlayersSizeException.java
new file mode 100644
index 00000000000..9fbc5e47a08
--- /dev/null
+++ b/src/main/java/exception/InvalidPlayersSizeException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidPlayersSizeException extends CustomException {
+
+ public InvalidPlayersSizeException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidSeparatorException.java b/src/main/java/exception/InvalidSeparatorException.java
new file mode 100644
index 00000000000..36336cabc62
--- /dev/null
+++ b/src/main/java/exception/InvalidSeparatorException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidSeparatorException extends CustomException {
+
+ public InvalidSeparatorException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/MessageDoesNotExistException.java b/src/main/java/exception/MessageDoesNotExistException.java
new file mode 100644
index 00000000000..d7669bd6dd5
--- /dev/null
+++ b/src/main/java/exception/MessageDoesNotExistException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class MessageDoesNotExistException extends CustomException {
+
+ public MessageDoesNotExistException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/NoMoreCardException.java b/src/main/java/exception/NoMoreCardException.java
new file mode 100644
index 00000000000..000c233da86
--- /dev/null
+++ b/src/main/java/exception/NoMoreCardException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class NoMoreCardException extends CustomException {
+
+ public NoMoreCardException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/OutOfRangeBetAmount.java b/src/main/java/exception/OutOfRangeBetAmount.java
new file mode 100644
index 00000000000..509a26fcc52
--- /dev/null
+++ b/src/main/java/exception/OutOfRangeBetAmount.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class OutOfRangeBetAmount extends CustomException {
+
+ public OutOfRangeBetAmount(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/ReservedPlayerNameException.java b/src/main/java/exception/ReservedPlayerNameException.java
new file mode 100644
index 00000000000..836ba975412
--- /dev/null
+++ b/src/main/java/exception/ReservedPlayerNameException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class ReservedPlayerNameException extends CustomException {
+
+ public ReservedPlayerNameException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java
new file mode 100644
index 00000000000..ee7217fd8f2
--- /dev/null
+++ b/src/main/java/view/InputView.java
@@ -0,0 +1,79 @@
+package view;
+
+import constants.ErrorCode;
+import exception.InvalidBetAmountException;
+import exception.InvalidInputException;
+import exception.InvalidSeparatorException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Scanner;
+
+public class InputView {
+
+ private static final String NAME_SEPARATOR = ",";
+
+ private final Scanner scanner;
+
+ public InputView(final Scanner scanner) {
+ this.scanner = scanner;
+ }
+
+ public List readNames() {
+ System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)");
+ String rawNames = scanner.nextLine();
+ validateBlank(rawNames);
+ validateSeparators(rawNames);
+ List names = Arrays.stream(rawNames.split(NAME_SEPARATOR))
+ .map(String::trim)
+ .toList();
+ System.out.println();
+ return names;
+ }
+
+ public Long readBetAmount(String name) {
+ System.out.printf("%s의 배팅 금액은?%n", name);
+ String rawAmount = scanner.nextLine().trim();
+ validateBlank(rawAmount);
+ validateLong(rawAmount);
+ System.out.println();
+ return Long.parseLong(rawAmount);
+ }
+
+
+ public String readAnswer(String name) {
+ System.out.printf("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)%n", name);
+ String rawAnswer = scanner.nextLine().trim();
+ validateBlank(rawAnswer);
+ return rawAnswer;
+ }
+
+ private void validateBlank(final String rawNames) {
+ if (rawNames == null || rawNames.isBlank()) {
+ throw new InvalidInputException(ErrorCode.INVALID_INPUT);
+ }
+ }
+
+ private void validateSeparators(final String rawNames) {
+ if (isInvalidSeparator(rawNames)) {
+ throw new InvalidSeparatorException(ErrorCode.INVALID_SEPARATOR);
+ }
+ }
+
+ private boolean isInvalidSeparator(final String rawNames) {
+ if (rawNames.startsWith(NAME_SEPARATOR)) {
+ return true;
+ }
+ if (rawNames.endsWith(NAME_SEPARATOR)) {
+ return true;
+ }
+ return rawNames.contains(NAME_SEPARATOR.repeat(2));
+ }
+
+ private void validateLong(final String rawAmount) {
+ try {
+ Long.parseLong(rawAmount);
+ } catch (NumberFormatException exception) {
+ throw new InvalidBetAmountException(ErrorCode.INVALID_BET_AMOUNT);
+ }
+ }
+}
diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java
new file mode 100644
index 00000000000..00fe1b9271b
--- /dev/null
+++ b/src/main/java/view/OutputView.java
@@ -0,0 +1,91 @@
+package view;
+
+import static domain.participant.Dealer.INIT_HANDS_SIZE;
+import static domain.participant.Dealer.THRESHOLD;
+
+import constants.ErrorCode;
+import domain.amount.Amount;
+import domain.participant.Player;
+import dto.DealerHandsDto;
+import dto.ParticipantDto;
+import dto.ParticipantsDto;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import view.message.ErrorCodeMessage;
+
+public class OutputView {
+
+ private static final String LINE = System.lineSeparator();
+ private static final String FORM = "%s카드: %s%n";
+ private static final String TOTAL_SUM_FORM = "%s 카드: %s - 결과: %d%n";
+ private static final String RESULT_FORM = "%s: %,d%n";
+ private static final String ERROR_FORM = "[ERROR] %s%n";
+
+
+ public void printStartDeal(final DealerHandsDto dealerHandsDto, final ParticipantsDto participantsDto) {
+ final String dealerCard = dealerHandsDto.getDisplayedCard();
+
+ final List playerNames = participantsDto.getNames();
+ System.out.printf("딜러와 %s 에게 %d장을 나누었습니다.%n", format(playerNames), INIT_HANDS_SIZE);
+ System.out.printf("딜러: %s%n", dealerCard);
+
+ for (ParticipantDto participantDto : participantsDto.getPlayers()) {
+ System.out.printf(FORM, participantDto.name(), format(participantDto.cards()));
+ }
+ System.out.print(LINE);
+ }
+
+ public void printHands(final ParticipantDto participantDto) {
+ System.out.printf(FORM, participantDto.name(), format(participantDto.cards()));
+ }
+
+ public void printDealerTurnMessage() {
+ System.out.printf("딜러는 %d이하라 한장의 카드를 더 받았습니다.%n%n", THRESHOLD);
+ }
+
+ public void printHandsResult(final ParticipantsDto participantsDto) {
+ for (ParticipantDto participantDto : participantsDto.getPlayers()) {
+ System.out.printf(TOTAL_SUM_FORM, participantDto.name(), format(participantDto.cards()),
+ participantDto.totalSum());
+ }
+ System.out.print(LINE);
+ }
+
+ public void printGameResult(final Map playerAmountMap, final Amount amount) {
+ System.out.println("## 최종 수익");
+ System.out.printf("딜러: %,d%n", amount.getValue());
+
+ for (Entry entry : playerAmountMap.entrySet()) {
+ System.out.printf(RESULT_FORM, entry.getKey().getName(), entry.getValue().getValue());
+ }
+ }
+
+ private String format(final List playerNames) {
+ return String.join(", ", playerNames);
+ }
+
+ public void printBust() {
+ System.out.printf("BUST%n");
+ }
+
+ public void printBlackJack(final String name) {
+ System.out.printf("%s 축하드립니다! BLACK JACK!!!%n", name);
+ }
+
+ public void printException(final ErrorCode errorCode) {
+ System.out.printf(ERROR_FORM, ErrorCodeMessage.from(errorCode).getMessage());
+ }
+
+ public void printPlayerEndMessage(final boolean isBust) {
+ if (isBust) {
+ printBust();
+ return;
+ }
+ System.out.println("카드의 합이 21이라 더 이상 카드를 받을 수 없습니다.");
+ }
+
+ public void printDealerBlackJack() {
+ System.out.println("딜러가 블랙잭!!!");
+ }
+}
diff --git a/src/main/java/view/message/ErrorCodeMessage.java b/src/main/java/view/message/ErrorCodeMessage.java
new file mode 100644
index 00000000000..6e8a9d9457f
--- /dev/null
+++ b/src/main/java/view/message/ErrorCodeMessage.java
@@ -0,0 +1,50 @@
+package view.message;
+
+import constants.ErrorCode;
+import exception.MessageDoesNotExistException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public enum ErrorCodeMessage {
+
+ NOT_EXIST_MESSAGE(ErrorCode.NOT_EXIST_MESSAGE, "해당 메시지가 없습니다."),
+ INVALID_SEPARATOR(ErrorCode.INVALID_SEPARATOR, "유효하지 않은 구분자입니다."),
+ INVALID_INPUT(ErrorCode.INVALID_INPUT, "유효하지 않은 입력입니다."),
+ INVALID_SIZE(ErrorCode.INVALID_SIZE, "유효하지 않은 참여자 수입니다."),
+ DUPLICATE_NAME(ErrorCode.DUPLICATE_NAME, "중복된 이름은 사용할 수 없습니다."),
+ RESERVED_NAME(ErrorCode.RESERVED_NAME, "이름은 딜러일 수 없습니다."),
+ BLANK_VALUE(ErrorCode.BLANK_VALUE, "이름은 공백일 수 없습니다."),
+ INVALID_BET_AMOUNT(ErrorCode.INVALID_BET_AMOUNT, "유효하지 않은 배팅 금액입니다."),
+ EMPTY_CARD(ErrorCode.EMPTY_CARD, "뽑을 수 있는 카드가 없습니다."),
+ INVALID_COMMAND(ErrorCode.INVALID_COMMAND, "y 또는 n을 입력해주세요"),
+ ;
+
+ private static final Map SUIT_MESSAGE = Arrays.stream(values())
+ .collect(Collectors.toMap(ErrorCodeMessage::getCode, Function.identity()));
+
+
+ private final ErrorCode errorCode;
+ private final String message;
+
+ ErrorCodeMessage(final ErrorCode errorCode, final String message) {
+ this.errorCode = errorCode;
+ this.message = message;
+ }
+
+ public static ErrorCodeMessage from(ErrorCode errorCode) {
+ if (SUIT_MESSAGE.containsKey(errorCode)) {
+ return SUIT_MESSAGE.get(errorCode);
+ }
+ throw new MessageDoesNotExistException(ErrorCode.NOT_EXIST_MESSAGE);
+ }
+
+ private ErrorCode getCode() {
+ return errorCode;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/test/java/domain/AnswerTest.java b/src/test/java/domain/AnswerTest.java
new file mode 100644
index 00000000000..c4f61646564
--- /dev/null
+++ b/src/test/java/domain/AnswerTest.java
@@ -0,0 +1,19 @@
+package domain;
+
+import domain.participant.Answer;
+import exception.InvalidCommandException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class AnswerTest {
+
+ @ParameterizedTest
+ @DisplayName("y혹은 n이 아닐시 예외가 발생한다.")
+ @ValueSource(strings = {"Y", "nn", "aa"})
+ void invalidAnswer(String value) {
+ Assertions.assertThatThrownBy(() -> Answer.from(value))
+ .isInstanceOf(InvalidCommandException.class);
+ }
+}
diff --git a/src/test/java/domain/BetAmountTest.java b/src/test/java/domain/BetAmountTest.java
new file mode 100644
index 00000000000..476a1c9cbfc
--- /dev/null
+++ b/src/test/java/domain/BetAmountTest.java
@@ -0,0 +1,36 @@
+package domain;
+
+import domain.amount.BetAmount;
+import exception.InvalidBetAmountException;
+import exception.OutOfRangeBetAmount;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class BetAmountTest {
+
+ @ParameterizedTest
+ @DisplayName("베팅 금액이 0 이하일 시 예외가 발생한다.")
+ @ValueSource(longs = {0, -1, -100000})
+ void invalidBetAmount(long value) {
+ Assertions.assertThatThrownBy(() -> new BetAmount(value))
+ .isInstanceOf(OutOfRangeBetAmount.class);
+ }
+
+ @ParameterizedTest
+ @DisplayName("배팅 금액이 10으로 나누어 떨어지지 않을 시 예외가 발생한다.")
+ @ValueSource(longs = {11, 53, 10001})
+ void invalidUnitBetAmount(long value) {
+ Assertions.assertThatThrownBy(() -> new BetAmount(value))
+ .isInstanceOf(InvalidBetAmountException.class);
+ }
+
+ @Test
+ @DisplayName("배팅 금액이 1억을 초과하면 예외가 발생한다.")
+ void overBetAmount() {
+ Assertions.assertThatThrownBy(() -> new BetAmount(100_000_010))
+ .isInstanceOf(OutOfRangeBetAmount.class);
+ }
+}
diff --git a/src/test/java/domain/CardTest.java b/src/test/java/domain/CardTest.java
new file mode 100644
index 00000000000..bc362cfca08
--- /dev/null
+++ b/src/test/java/domain/CardTest.java
@@ -0,0 +1,20 @@
+package domain;
+
+import domain.card.Card;
+import java.util.List;
+import java.util.Set;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class CardTest {
+
+ @Test
+ @DisplayName("중복없는 52장의 카드를 생성한다.")
+ void generate() {
+ final List values = Card.values();
+
+ Assertions.assertThat(values).hasSize(52);
+ Assertions.assertThat(Set.copyOf(values)).hasSize(52);
+ }
+}
diff --git a/src/test/java/domain/GameResultTest.java b/src/test/java/domain/GameResultTest.java
new file mode 100644
index 00000000000..a791abe776f
--- /dev/null
+++ b/src/test/java/domain/GameResultTest.java
@@ -0,0 +1,43 @@
+package domain;
+
+import static domain.HandsTestFixture.blackJack;
+import static domain.HandsTestFixture.bustHands;
+import static domain.HandsTestFixture.sum17Size3One;
+import static domain.HandsTestFixture.sum17Size3Two;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum20Size3;
+import static domain.HandsTestFixture.sum21Size3;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class GameResultTest {
+
+ @DisplayName("카드 합이 같다면 무승부이다.")
+ @Test
+ void isTie() {
+ Assertions.assertThat(sum17Size3One.calculateResult(sum17Size3Two)).isEqualTo(GameResult.TIE);
+ Assertions.assertThat(sum20Size2.calculateResult(sum20Size3)).isEqualTo(GameResult.TIE);
+ }
+
+ @Test
+ @DisplayName("카드 합이 21이하이면서 21에 가까운 카드가 승리한다.")
+ void isWin() {
+ Assertions.assertThat(sum21Size3.calculateResult(sum20Size2)).isEqualTo(GameResult.WIN);
+ Assertions.assertThat(sum20Size2.calculateResult(sum21Size3)).isEqualTo(GameResult.LOSE);
+ }
+
+ @Test
+ @DisplayName("카드 합이 21초과이면 패배한다.")
+ void isLoseWhenCardSumGreater21() {
+ Assertions.assertThat(bustHands.calculateResult(sum20Size2)).isEqualTo(GameResult.LOSE);
+ }
+
+ @Test
+ @DisplayName("blackjack이 이긴다.")
+ void isWinBlackJack() {
+ Assertions.assertThat(blackJack.calculateResult(sum20Size2)).isEqualTo(GameResult.BLACK_JACK_WIN);
+ Assertions.assertThat(sum20Size2.calculateResult(blackJack)).isEqualTo(GameResult.LOSE);
+ }
+}
diff --git a/src/test/java/domain/HandsTest.java b/src/test/java/domain/HandsTest.java
new file mode 100644
index 00000000000..8c5bb396d81
--- /dev/null
+++ b/src/test/java/domain/HandsTest.java
@@ -0,0 +1,84 @@
+package domain;
+
+import static domain.HandsTestFixture.sum19Size3Ace1;
+import static domain.HandsTestFixture.sum19Size4Ace11;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum20Size3Ace1;
+import static domain.HandsTestFixture.sum21Size3Ace11;
+import static domain.card.Rank.EIGHT;
+import static domain.card.Shape.CLOVER;
+
+import domain.card.Card;
+import domain.participant.Hands;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class HandsTest {
+
+ @Test
+ @DisplayName("카드를 가지고 있는 객체를 생성한다.")
+ void createPacket() {
+ Assertions.assertThatCode(Hands::createEmptyHands)
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("카드를 추가한다.")
+ void addCard() {
+ //given
+ final Hands hands = Hands.createEmptyHands();
+
+ //when
+ hands.add(new Card(EIGHT, CLOVER));
+
+ //then
+ Assertions.assertThat(hands.size()).isEqualTo(1);
+ }
+
+ @DisplayName("카드의 합을 구한다.")
+ @Test
+ void sum() {
+ Assertions.assertThat(sum20Size2.sum()).isEqualTo(20);
+ }
+
+ @DisplayName("에이스를 11로 계산한다.")
+ @ParameterizedTest
+ @MethodSource("sumAce11ParameterProvider")
+ void sumAce11(final Hands hands, final int expected) {
+ // given & when
+ final int result = hands.sum();
+
+ // then
+ Assertions.assertThat(result).isEqualTo(expected);
+ }
+
+ @DisplayName("에이스를 1로 계산한다.")
+ @ParameterizedTest
+ @MethodSource("sumAce1ParameterProvider")
+ void sumAce1(final Hands hands, final int expected) {
+ // given & when
+ final int result = hands.sum();
+
+ // then
+ Assertions.assertThat(result).isEqualTo(expected);
+ }
+
+ static Stream sumAce11ParameterProvider() {
+ return Stream.of(
+ Arguments.of(sum21Size3Ace11, 21),
+ Arguments.of(sum19Size4Ace11, 19)
+ );
+ }
+
+ static Stream sumAce1ParameterProvider() {
+ return Stream.of(
+ Arguments.of(sum19Size3Ace1, 19),
+ Arguments.of(sum20Size3Ace1, 20)
+ );
+ }
+}
diff --git a/src/test/java/domain/HandsTestFixture.java b/src/test/java/domain/HandsTestFixture.java
new file mode 100644
index 00000000000..c9a8d148242
--- /dev/null
+++ b/src/test/java/domain/HandsTestFixture.java
@@ -0,0 +1,50 @@
+package domain;
+
+import static domain.card.Rank.ACE;
+import static domain.card.Rank.EIGHT;
+import static domain.card.Rank.FIVE;
+import static domain.card.Rank.FOUR;
+import static domain.card.Rank.JACK;
+import static domain.card.Rank.KING;
+import static domain.card.Rank.NINE;
+import static domain.card.Rank.QUEEN;
+import static domain.card.Rank.SEVEN;
+import static domain.card.Rank.SIX;
+import static domain.card.Rank.TEN;
+import static domain.card.Rank.THREE;
+import static domain.card.Rank.TWO;
+import static domain.card.Shape.CLOVER;
+import static domain.card.Shape.DIAMOND;
+import static domain.card.Shape.HEART;
+import static domain.card.Shape.SPADE;
+
+import domain.card.Card;
+import domain.participant.Hands;
+import java.util.List;
+
+public class HandsTestFixture {
+
+ public static final Hands sum10Size2 = new Hands(List.of(new Card(FIVE, SPADE), new Card(FIVE, HEART)));
+ public static final Hands sum17Size3One = new Hands(
+ List.of(new Card(SEVEN, SPADE), new Card(FOUR, SPADE), new Card(SIX, SPADE)));
+ public static final Hands sum17Size3Two = new Hands(
+ List.of(new Card(TEN, SPADE), new Card(THREE, SPADE), new Card(FOUR, SPADE)));
+ public static final Hands sum18Size2 = new Hands(List.of(new Card(EIGHT, CLOVER), new Card(TEN, DIAMOND)));
+ public static final Hands sum19Size4Ace11 = new Hands(
+ List.of(new Card(ACE, DIAMOND), new Card(TWO, CLOVER), new Card(FOUR, CLOVER), new Card(TWO, CLOVER)));
+ public static final Hands sum19Size3Ace1 = new Hands(
+ List.of(new Card(ACE, HEART), new Card(NINE, SPADE), new Card(NINE, CLOVER)));
+ public static final Hands sum20Size2 = new Hands(List.of(new Card(NINE, SPADE), new Card(ACE, SPADE)));
+ public static final Hands sum20Size3 = new Hands(
+ List.of(new Card(SEVEN, SPADE), new Card(TWO, SPADE), new Card(ACE, SPADE)));
+ public static final Hands sum20Size3Ace1 = new Hands(
+ List.of(new Card(ACE, DIAMOND), new Card(EIGHT, CLOVER), new Card(FIVE, CLOVER), new Card(SIX, CLOVER)));
+ public static final Hands sum21Size3 = new Hands(
+ List.of(new Card(QUEEN, HEART), new Card(SIX, SPADE), new Card(FIVE, CLOVER)));
+ public static final Hands sum21Size3Ace11 = new Hands(
+ List.of(new Card(ACE, HEART), new Card(EIGHT, SPADE), new Card(TWO, CLOVER)));
+ public static final Hands bustHands = new Hands(
+ List.of(new Card(EIGHT, DIAMOND), new Card(TWO, DIAMOND), new Card(TWO, DIAMOND), new Card(KING, CLOVER)));
+ public static final Hands noBustHands = new Hands(List.of(new Card(JACK, HEART), new Card(TEN, SPADE)));
+ public static final Hands blackJack = new Hands(List.of(new Card(JACK, HEART), new Card(ACE, SPADE)));
+}
diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java
new file mode 100644
index 00000000000..401a38f7832
--- /dev/null
+++ b/src/test/java/domain/NameTest.java
@@ -0,0 +1,28 @@
+package domain;
+
+import domain.participant.Name;
+import exception.InvalidPlayerNameException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class NameTest {
+
+ @DisplayName("공백을 입력하면 예외를 발생시킨다.")
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", " "})
+ void BlankInputThrowException(String value) {
+ Assertions.assertThatThrownBy(() -> new Name(value))
+ .isInstanceOf(InvalidPlayerNameException.class);
+ }
+
+ @DisplayName("null을 입력하면 예외를 발생시킨다.")
+ @ParameterizedTest
+ @NullSource
+ void nullInputThrowException(String value) {
+ Assertions.assertThatThrownBy(() -> new Name(value))
+ .isInstanceOf(InvalidPlayerNameException.class);
+ }
+}
diff --git a/src/test/java/domain/card/CardDeckTest.java b/src/test/java/domain/card/CardDeckTest.java
new file mode 100644
index 00000000000..f4ccfa4c8c6
--- /dev/null
+++ b/src/test/java/domain/card/CardDeckTest.java
@@ -0,0 +1,39 @@
+package domain.card;
+
+import static domain.participant.Dealer.DECK_SIZE;
+
+import exception.NoMoreCardException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class CardDeckTest {
+
+ @DisplayName("카드를 52 * 6 만큼 생성한다.")
+ @Test
+ void generate() {
+ // given
+ final CardDeck cardDeck = CardDeck.generate(DECK_SIZE);
+
+ // when && then
+ Assertions.assertThat(cardDeck.size()).isEqualTo(52 * 6);
+ }
+
+ @Test
+ @DisplayName("카드가 없는데 카드를 뽑을 경우 예외가 발생한다.")
+ void pop() {
+ //given
+ final CardDeck cardDeck = CardDeck.generate(1);
+
+ //when
+ int cardSize = 52;
+ while (cardSize > 0) {
+ cardDeck.pop();
+ cardSize--;
+ }
+
+ //then
+ Assertions.assertThatThrownBy(cardDeck::pop)
+ .isInstanceOf(NoMoreCardException.class);
+ }
+}
diff --git a/src/test/java/domain/participant/DealerTest.java b/src/test/java/domain/participant/DealerTest.java
new file mode 100644
index 00000000000..1eb573b5406
--- /dev/null
+++ b/src/test/java/domain/participant/DealerTest.java
@@ -0,0 +1,122 @@
+package domain.participant;
+
+import static domain.HandsTestFixture.blackJack;
+import static domain.HandsTestFixture.sum10Size2;
+import static domain.HandsTestFixture.sum18Size2;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum20Size3;
+import static domain.HandsTestFixture.sum21Size3;
+import static domain.GameResult.LOSE;
+import static domain.GameResult.TIE;
+import static domain.GameResult.WIN;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import domain.amount.BetAmount;
+import domain.GameResult;
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class DealerTest {
+
+ @Test
+ @DisplayName("참여자에게 카드 2장을 나눠준다.")
+ void dealCards() {
+ //given
+ final Player player1 = new Player(new Name("레디"), new BetAmount(100));
+ final Player player2 = new Player(new Name("제제"), new BetAmount(1000));
+ final Players players = new Players(List.of(player1, player2));
+ final Dealer dealer = new Dealer();
+
+ //when
+ dealer.initHands(players);
+
+ //then
+ Assertions.assertThat(players.getPlayers()).allMatch(player -> player.handsSize() == 2);
+ }
+
+ @Test
+ @DisplayName("참여자의 답변이 y라면 카드를 한장 추가한다.")
+ void addOneCard() {
+ //given
+ final Player hitPlayer = new Player(new Name("레디"), Hands.createEmptyHands());
+ final Player stayPlayer = new Player(new Name("제제"), Hands.createEmptyHands());
+
+ final Players players = new Players(List.of(hitPlayer, stayPlayer));
+
+ final Dealer dealer = new Dealer();
+ dealer.initHands(players);
+
+ //when
+ dealer.deal(hitPlayer, Answer.HIT);
+ dealer.deal(stayPlayer, Answer.STAY);
+
+ //then
+ Assertions.assertThat(hitPlayer.handsSize()).isEqualTo(3);
+ Assertions.assertThat(stayPlayer.handsSize()).isEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("딜러의 카드의 합이 17이상이 될때까지 카드를 추가한다.")
+ void dealerDeal() {
+ //given
+ final Dealer dealer = new Dealer(sum10Size2);
+
+ //when
+ dealer.deal();
+
+ //then
+ Assertions.assertThat(dealer.countAddedHands()).isPositive();
+ Assertions.assertThat(dealer.handsSum()).isGreaterThanOrEqualTo(17);
+ }
+
+ @DisplayName("딜러의 카드의 합이 17이상이라면 카드를 추가하지 않는다")
+ @Test
+ void dealerNoDeal() {
+ //given
+ final Dealer dealer = new Dealer(sum18Size2);
+
+ //when
+ dealer.deal();
+
+ //then
+ Assertions.assertThat(dealer.countAddedHands()).isZero();
+ Assertions.assertThat(dealer.handsSum()).isGreaterThanOrEqualTo(17);
+ }
+
+ @DisplayName("딜러의 승패무를 판단한다.")
+ @Test
+ void dealerResult() {
+ // given
+ final Player loser1 = new Player(new Name("레디"), sum18Size2);
+ final Player loser2 = new Player(new Name("피케이"), sum18Size2);
+ final Player winner = new Player(new Name("제제"), sum21Size3);
+ final Player tier = new Player(new Name("브라운"), sum20Size3);
+
+ final Players players = new Players(List.of(loser1, loser2, winner, tier));
+ final Dealer dealer = new Dealer(sum20Size3);
+
+ // when
+ final Map expected = Map.of(WIN, 2, LOSE, 1, TIE, 1);
+
+ // then
+ Assertions.assertThat(dealer.getDealerResult(players)).isEqualTo(expected);
+ }
+
+ @Test
+ @DisplayName("처음 나눠준 카드 두장의 합이 21이라면 블랙잭이다.")
+ void checkingBlackJack() {
+ //given
+ final Dealer dealer = new Dealer(sum20Size2);
+ final Player blackJackPlayer = new Player(new Name("수달"), blackJack);
+ final Player noBlackJackPlayer = new Player(new Name("레디"), sum18Size2);
+
+ //when && then
+ assertAll(() -> Assertions.assertThat(dealer.isBlackJack()).isFalse(),
+ () -> Assertions.assertThat(blackJackPlayer.isBlackJack()).isTrue(),
+ () -> Assertions.assertThat(noBlackJackPlayer.isBlackJack()).isFalse());
+
+ }
+}
diff --git a/src/test/java/domain/participant/NamesTest.java b/src/test/java/domain/participant/NamesTest.java
new file mode 100644
index 00000000000..4c21707a2e5
--- /dev/null
+++ b/src/test/java/domain/participant/NamesTest.java
@@ -0,0 +1,80 @@
+package domain.participant;
+
+import exception.DuplicatePlayerNameException;
+import exception.InvalidPlayersSizeException;
+import java.util.List;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class NamesTest {
+
+ @Test
+ @DisplayName("참여자 이름 중복시 예외가 발생한다.")
+ void duplicatePlayerName() {
+ //given
+ final List names = List.of("redy", "redy");
+
+ //when & then
+ Assertions.assertThatThrownBy(() -> Names.from(names))
+ .isInstanceOf(DuplicatePlayerNameException.class);
+ }
+
+ @ParameterizedTest
+ @DisplayName("참여자 이름에 공백이 있는 경우 제거하여 중복을 판단한다.")
+ @MethodSource("duplicateBlankNameParameterProvider")
+ void duplicateBlankName(final List names) {
+ Assertions.assertThatThrownBy(() -> Names.from(names))
+ .isInstanceOf(DuplicatePlayerNameException.class);
+ }
+
+ @DisplayName("총 참여자 수가 2이상 8이하이면 참여자를 생성한다.")
+ @ParameterizedTest
+ @MethodSource("validPlayersSizeParameterProvider")
+ void validPlayersSize(final List names) {
+ Assertions.assertThatCode(() -> Names.from(names))
+ .doesNotThrowAnyException();
+ }
+
+ @DisplayName("총 참여자 수는 2이상 8이하가 아니면 예외가 발생한다.")
+ @ParameterizedTest
+ @MethodSource("invalidPlayersSizeParameterProvider")
+ void invalidPlayersSize(final List names) {
+ Assertions.assertThatThrownBy(() -> Names.from(names))
+ .isInstanceOf(InvalidPlayersSizeException.class);
+ }
+
+ static Stream duplicateBlankNameParameterProvider() {
+ return Stream.of(
+ Arguments.of(List.of("a", "a ", "b")),
+ Arguments.of(List.of(" a ", " a", "b", "c")),
+ Arguments.of(List.of("a", " a"))
+ );
+ }
+
+ static Stream validPlayersSizeParameterProvider() {
+ return Stream.of(
+ Arguments.of(
+ List.of("pobi", "jason")
+ ),
+ Arguments.of(
+ List.of("1", "2", "3", "4", "5", "6", "7", "8")
+ )
+ );
+ }
+
+ static Stream invalidPlayersSizeParameterProvider() {
+ return Stream.of(
+ Arguments.of(
+ List.of("pobi")
+ ),
+ Arguments.of(
+ List.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
+ )
+ );
+ }
+}
diff --git a/src/test/java/domain/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java
new file mode 100644
index 00000000000..eaa5b170595
--- /dev/null
+++ b/src/test/java/domain/participant/PlayerTest.java
@@ -0,0 +1,67 @@
+package domain.participant;
+
+import static domain.HandsTestFixture.bustHands;
+import static domain.HandsTestFixture.sum18Size2;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum21Size3Ace11;
+
+import exception.ReservedPlayerNameException;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class PlayerTest {
+
+ @DisplayName("이름으로 참여자를 생성한다.")
+ @Test
+ void createPlayerWithName() {
+ Assertions.assertThatCode(() -> new Player(new Name("pobi"), Hands.createEmptyHands()))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("참여자 이름이 딜러이면 예외가 발생한다.")
+ void validateName() {
+ final Name name = new Name("딜러");
+ final Hands hands = Hands.createEmptyHands();
+
+ Assertions.assertThatThrownBy(() -> new Player(name, hands))
+ .isInstanceOf(ReservedPlayerNameException.class);
+ }
+
+ @ParameterizedTest
+ @DisplayName("참여자가 21 이상이면 더이상 딜을 할 수가 없다.")
+ @MethodSource("canNotDealParameterProvider")
+ void canNotDeal(Hands hands) {
+ final Name name = new Name("레디");
+ final Player player = new Player(name, hands);
+ Assertions.assertThat(player.canDeal()).isFalse();
+ }
+
+ @ParameterizedTest
+ @DisplayName("참여자가 21 이하라면 딜 할 수가 있다.")
+ @MethodSource("canDealParameterProvider")
+ void canDeal(Hands hands) {
+ final Name name = new Name("레디");
+ final Player player = new Player(name, hands);
+ Assertions.assertThat(player.canDeal()).isTrue();
+ }
+
+ static Stream canNotDealParameterProvider() {
+ return Stream.of(
+ Arguments.of(sum21Size3Ace11),
+ Arguments.of(bustHands)
+ );
+ }
+
+ static Stream canDealParameterProvider() {
+ return Stream.of(
+ Arguments.of(sum18Size2),
+ Arguments.of(sum20Size2)
+ );
+ }
+}
diff --git a/src/test/java/domain/participant/PlayersTest.java b/src/test/java/domain/participant/PlayersTest.java
new file mode 100644
index 00000000000..9ae8ac9cf10
--- /dev/null
+++ b/src/test/java/domain/participant/PlayersTest.java
@@ -0,0 +1,160 @@
+package domain.participant;
+
+import static domain.HandsTestFixture.blackJack;
+import static domain.HandsTestFixture.bustHands;
+import static domain.HandsTestFixture.noBustHands;
+import static domain.HandsTestFixture.sum10Size2;
+import static domain.HandsTestFixture.sum17Size3One;
+import static domain.HandsTestFixture.sum18Size2;
+import static domain.HandsTestFixture.sum19Size3Ace1;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum20Size3;
+import static domain.HandsTestFixture.sum21Size3;
+import static domain.GameResult.LOSE;
+import static domain.GameResult.TIE;
+import static domain.GameResult.WIN;
+
+import domain.amount.Amount;
+import domain.amount.BetAmount;
+import domain.GameResult;
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class PlayersTest {
+
+ @Test
+ @DisplayName("참가자 중 버스트 되지 않은 참가자가 있다면 isAllBust가 False를 반환한다.")
+ void isAllBustFalse() {
+ //given
+ final Player bustPlayer = new Player(new Name("레디"), bustHands);
+ final Player noBustPlayer = new Player(new Name("제제"), noBustHands);
+ final Players players = new Players(List.of(bustPlayer, noBustPlayer));
+
+ //when && then
+ Assertions.assertThat(players.isAllBust()).isFalse();
+ }
+
+ @Test
+ @DisplayName("모든 참가자가 버스트되면 isAllBust가 True를 반환한다.")
+ void isAllBustTrue() {
+ //given
+ final Player player1 = new Player(new Name("레디"), bustHands);
+ final Player player2 = new Player(new Name("제제"), bustHands);
+ final Player player3 = new Player(new Name("수달"), bustHands);
+ final Player player4 = new Player(new Name("피케이"), bustHands);
+
+ final Players players = new Players(List.of(player1, player2, player3, player4));
+
+ //when && then
+ Assertions.assertThat(players.isAllBust()).isTrue();
+ }
+
+ @Test
+ @DisplayName("참여자의 승패무를 판단한다.")
+ void playerResult() {
+ //given
+ final Player loser = new Player(new Name("레디"), sum18Size2);
+ final Player winner = new Player(new Name("제제"), sum21Size3);
+ final Player tier = new Player(new Name("수달"), sum20Size3);
+
+ final Players players = new Players(List.of(loser, winner, tier));
+ final Dealer dealer = new Dealer(sum20Size3);
+
+ //when & then
+ final Map expected = Map.of(loser, LOSE, winner, WIN, tier, TIE);
+ Assertions.assertThat(players.getPlayersResult(dealer)).isEqualTo(expected);
+ }
+
+ @Test
+ @DisplayName("딜러가 버스트일때 참여자가 버스트가 아니면 WIN")
+ void all() {
+ //given
+ final Dealer bustDealer = new Dealer(bustHands);
+ final Player winner1 = new Player(new Name("레디"), sum18Size2);
+ final Player winner2 = new Player(new Name("브라운"), sum20Size2);
+ final Player loser = new Player(new Name("제제"), bustHands);
+
+ final Players players = new Players(List.of(winner1, winner2, loser));
+
+ //when
+ final Map expectedPlayerResult = Map.of(winner1, WIN, winner2, WIN, loser, LOSE);
+ final Map expectedDealerResult = Map.of(WIN, 1, LOSE, 2);
+
+ //then
+ Assertions.assertThat(players.getPlayersResult(bustDealer)).isEqualTo(expectedPlayerResult);
+ Assertions.assertThat(bustDealer.getDealerResult(players)).isEqualTo(expectedDealerResult);
+ }
+
+
+ @Test
+ @DisplayName("모든 플레이어가 이긴 경우, 최종 수익을 계산한다. (블랙잭인 경우는 없다)")
+ void calculateTotalAmountWhenAllPlayersWin() {
+ //given
+ final Dealer dealer = new Dealer(sum17Size3One);
+ final Player winner1 = new Player(new Name("레디"), sum20Size3, new BetAmount(1_000));
+ final Player winner2 = new Player(new Name("제제"), sum21Size3, new BetAmount(2_000));
+ final Players players = new Players(List.of(winner1, winner2));
+ final Map expected = Map.of(winner1, new Amount(1_000), winner2, new Amount(2_000));
+
+ //when
+ final Map result = players.calculateResult(dealer);
+
+ //then
+ Assertions.assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @DisplayName("참여자가 블랙잭인 경우 1.5배의 수익을 얻는다.")
+ void blackJackAmount() {
+ final Dealer dealer = new Dealer(sum17Size3One);
+ final Player blackJackPlayer = new Player(new Name("수달"), blackJack, new BetAmount(10_000));
+ final Player loser = new Player(new Name("레디"), sum10Size2, new BetAmount(2_000));
+ final Players players = new Players(List.of(blackJackPlayer, loser));
+
+ final Map expected = Map.of(blackJackPlayer, new Amount(15_000), loser, new Amount(-2_000));
+
+ //when
+ final Map result = players.calculateResult(dealer);
+
+ //then
+ Assertions.assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @DisplayName("참가자와 딜러 모두 블랙잭인 경우 배팅 금액을 돌려받는다.")
+ void playerAndDealerBlackJack() {
+ final Dealer dealer = new Dealer(blackJack);
+ final Player blackJackPlayer = new Player(new Name("수달"), blackJack, new BetAmount(10_000));
+ final Player loser = new Player(new Name("레디"), sum18Size2, new BetAmount(2_000));
+ final Players players = new Players(List.of(blackJackPlayer, loser));
+ final Map expected = Map.of(blackJackPlayer, new Amount(0), loser, new Amount(-2_000));
+
+ //when
+ final Map result = players.calculateResult(dealer);
+
+ //then
+ Assertions.assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ @DisplayName("딜러의 최종 수익을 계산한다.")
+ void dealerAmount() {
+ //given
+ final Dealer dealer = new Dealer(sum19Size3Ace1);
+ final Player winner1 = new Player(new Name("레디"), sum20Size3, new BetAmount(10_000));
+ final Player winner2 = new Player(new Name("제제"), sum21Size3, new BetAmount(30_000));
+ final Player loser1 = new Player(new Name("브라운"), sum17Size3One, new BetAmount(500_000));
+ final Players players = new Players(List.of(winner1, winner2, loser1));
+
+ //when
+ final Map playerAmountMap = players.calculateResult(dealer);
+ final Amount amount = dealer.calculateRevenue(playerAmountMap);
+ final Amount expected = new Amount(460_000);
+
+ //then
+ Assertions.assertThat(amount).isEqualTo(expected);
+ }
+}