ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -7,13 +7,25 @@
  *  smtpHandleInboundConnection로직을 통해 Connection을 맺는걸 권유 드립니다.(해당 로직을 사용안하셔도 무방합니다.)
  *
  */
+
+int count = 0;
+
 void smtpWaitAsync(int server_fd) {
         /* TODO 과제 2-1
         *  smtpSvrRecvAsync.c 파일은 비동기 처리를 이용하여 데이터를 제어 하는 로직이 작성 되어 있습니다.
         *  N개의 H_SERVER_WORK_TH 스레드와 하나의 smtpWaitAsync 스레드가 동시에 동작하고 있습니다.
         *  smtpWaitAsync Function에서 Connection을 맺고(smtpHandleInboundConnection로직 사용 권유)
         *  비동기 통신이 가능하게 개발한후 session정보를 WorkThread로 전달하는 로직을 개발하여야 합니다.
         */
+	while (1) {
+		smtp_session_t *session = NULL;
+		if ((session = smtpHandleInboundConnection(server_fd)) == NULL) {
+            		msleep(25);
+			continue;
+		}
+            	//msleep(25);
+	}
+	return;
 }
 
 void *H_SERVER_WORK_TH(void *args) {
@@ -26,12 +38,21 @@ void *H_SERVER_WORK_TH(void *args) {
         /* TODO 과제 2-2
         *  session정보를 해당 위치에 받아 올수있게 개발하여야 합니다.
         */
+	int idx = -1;
+	if ((idx = getSmtpSessionIdxForPool()) == -1) {
+            	msleep(25);
+		continue;
+	}
+	
+	session = sessions[idx].session;
 
         if (session == NULL) {
             msleep(25);
             continue;
         }
 
+	if (++count % 100 == 0) msleep(25);
+
         if ((nLine = smtpReadLine(session->sock_fd, buf, sizeof(buf))) <= 0) {
             LOG (LOG_INF, "%s : %sSMTP Connection closed%s : fd = %d, session_id=%s\n", __func__, C_YLLW, C_NRML,
                  session->sock_fd,
@@ -46,6 +67,8 @@ void *H_SERVER_WORK_TH(void *args) {
             }
             continue;
         }
+	
+	unusedSmtpSession(idx);
 
     }
     return NULL;
@@ -68,4 +91,4 @@ int smtpStartWorkThreads(int n_work_threads) {
     }
 
     return 0;
-}
\ No newline at end of file
+}

리뷰: 저 자리에 sleep이 들어간 이유가 궁금합니다!
동기화 처리가 잘 되었다면 필요 없을 코드인 것 같습니다.
@@ -0,0 +1,41 @@
+//
+//  MyReviewContentConfiguration.swift
+//  Pyonsnal-Color
+//
+//  Created by 조소정 on 5/19/24.
+//
+
+import UIKit
+
+struct MyReviewContentConfiguration: UIContentConfiguration {
+    var storeImageIcon: ConvenienceStore
+    var imageUrl: URL
+    var title: String
+    var date: String?
+    var mode: ProductInfoStackView.Mode
+    var tastesTag: [String]?
+    
+    init(
+        mode: ProductInfoStackView.Mode,
+        storeImageIcon: ConvenienceStore,
+        imageUrl: URL,
+        title: String,
+        date: String? = nil,
+        tastesTag: [String]? = nil
+    ) {
+        self.mode = mode
+        self.storeImageIcon = storeImageIcon
+        self.imageUrl = imageUrl
+        self.title = title
+        self.date = date
+        self.tastesTag = tastesTag
+    }
+    
+    func makeContentView() -> any UIView & UIContentView {
+        return MyReviewContentView(configuration: self, mode: mode)
+    }
+    
+    func updated(for state: any UIConfigurationState) -> MyReviewContentConfiguration {
+        return self
+    }
+}

리뷰: `UIContentConfiguration` 활용하는 방법 덕분에 배웠습니다 👍
@@ -0,0 +1,58 @@
+//
+//  MyReviewContentView.swift
+//  Pyonsnal-Color
+//
+//  Created by 조소정 on 5/19/24.
+//
+
+import UIKit
+import SnapKit
+
+class MyReviewContentView: UIView, UIContentView {
+    static let identifier = "MyReviewContentView"
+    
+    var configuration: any UIContentConfiguration
+    private let storeImageViewHeight: CGFloat = 20
+    
+    private var productInfoStackView: ProductInfoStackView
+    
+    init(configuration: any UIContentConfiguration, mode: ProductInfoStackView.Mode) {
+        self.configuration = configuration
+        self.productInfoStackView = ProductInfoStackView(mode: mode)
+        super.init(frame: .zero)
+        configureView()
+        configureUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    private func configureView() {
+        self.addSubview(productInfoStackView)
+        productInfoStackView.snp.makeConstraints {
+            $0.edges.equalToSuperview()
+        }
+    }
+    
+    private func configureUI() {
+        guard let configuration = configuration as? MyReviewContentConfiguration else { return }
+        self.productInfoStackView.productImageView.setImage(with: configuration.imageUrl)
+        self.productInfoStackView.productNameLabel.text = configuration.title
+        self.productInfoStackView.storeImageView.setImage(configuration.storeImageIcon.storeIcon)
+        self.productInfoStackView.storeImageView.snp.makeConstraints { make in
+            guard let storeIcon = configuration.storeImageIcon.storeIcon.image else { return }
+            let ratio = storeIcon.size.width / storeIcon.size.height
+            let newWidth = self.storeImageViewHeight * ratio
+            make.width.equalTo(newWidth)
+        }
+        
+        if let date = configuration.date {
+            self.productInfoStackView.dateLabel.text = configuration.date
+        }
+        
+        if let tastesTag = configuration.tastesTag {
+            self.productInfoStackView.setTastes(tastesTag: tastesTag)
+        }
+    }
+}

리뷰: [C]
오~ 처음 보는 친구네요 👍
@@ -0,0 +1,61 @@
+package bridge.controller;
+
+import bridge.domain.BridgeGame;
+import bridge.domain.BridgeMaker;
+import bridge.domain.Direction;
+import bridge.domain.GameCommand;
+import bridge.dto.GameResultDto;
+import bridge.dto.RoundResultDto;
+import bridge.dto.RoundResultsDto;
+import bridge.view.InputView;
+import bridge.view.OutputView;
+import bridge.util.Repeater;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BridgeGameController {
+    private final BridgeMaker bridgeMaker;
+    private final InputView inputView;
+    private final OutputView outputView;
+
+    public BridgeGameController(BridgeMaker bridgeMaker, InputView inputView, OutputView outputView) {
+        this.bridgeMaker = bridgeMaker;
+        this.inputView = inputView;
+        this.outputView = outputView;
+    }
+
+    public void run() {
+        outputView.printGreeting();
+        BridgeGame bridgeGame = Repeater.repeatUntilNoException(this::createBridge);
+
+        List<RoundResultDto> roundResults = new ArrayList<>();
+        GameCommand gameCommand = GameCommand.RETRY;
+
+        int tryCount = 1;
+        while (!bridgeGame.isGameEnd() && gameCommand.equals(GameCommand.RETRY)) {
+
+            RoundResultDto roundResultDto = executeRound(bridgeGame);
+            roundResults.add(roundResultDto);
+            outputView.printMap(new RoundResultsDto(roundResults));
+            if (!roundResultDto.isMoveSuccess()) {
+                gameCommand = Repeater.repeatUntilNoException(inputView::readGameCommand);
+                tryCount++;
+                roundResults.clear();
+                bridgeGame.retry();
+            }
+        }
+        outputView.printResult(new GameResultDto(new RoundResultsDto(roundResults), tryCount, bridgeGame.isGameEnd()));
+    }
+
+    private BridgeGame createBridge() {
+        Integer length = Repeater.repeatUntilNoException(inputView::readBridgeSize);
+        return BridgeGame.from(bridgeMaker.makeBridge(length));
+    }
+
+    private RoundResultDto executeRound(BridgeGame bridgeGame) {
+        Direction direction = Repeater.repeatUntilNoException(inputView::readMoving);
+        return new RoundResultDto(direction, bridgeGame.move(direction));
+    }
+
+}

리뷰: 컨트롤러에서 run()을 통해 다리 게임을 수행하고 있네요!
다만 해당 메서드가 너무 많은 일을 하고있는 것 같습니다!
기능의 분리가 이루어지면 메서드가 한가지일만 할 수 있을것 같아요😁
@@ -0,0 +1,64 @@
+package bridge.domain;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 다리 건너기 게임을 관리하는 클래스
+ */
+public class BridgeGame {
+    private static final int MIN_LENGTH = 3;
+    private static final int MAX_LENGTH = 20;
+    private static final String INVALID_LENGTH_BRIDGE_MESSAGE = "[ERROR] 다리 길이는 3부터 20 사이의 숫자여야 합니다.";
+
+    private final List<Direction> escapeRoute;
+    private int userPosition = 0;
+
+    private BridgeGame(List<Direction> escapeRoute) {
+        validate(escapeRoute);
+        this.escapeRoute = escapeRoute;
+    }
+
+    public static BridgeGame from(List<String> directions) {
+        List<Direction> escapeRoute = directions.stream()
+                .map(Direction::from)
+                .collect(Collectors.toList());
+        return new BridgeGame(escapeRoute);
+    }
+    /**
+     * 사용자가 칸을 이동할 때 사용하는 메서드
+     * <p>
+     * 이동을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다.
+     */
+    public boolean move(Direction direction) {
+        if (escapeRoute.get(userPosition).equals(direction)) {
+            userPosition++;
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isGameEnd () {
+        return userPosition == escapeRoute.size();
+    }
+
+    /**
+     * 사용자가 게임을 다시 시도할 때 사용하는 메서드
+     * <p>
+     * 재시작을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다.
+     */
+    public void retry() {
+        userPosition = 0;
+    }
+
+    private void validate(List<Direction> escapeRoute) {
+        validateLength(escapeRoute);
+    }
+
+    private void validateLength(List<Direction> escapeRoute) {
+        if (escapeRoute.size() < MIN_LENGTH || escapeRoute.size() > MAX_LENGTH) {
+            throw new IllegalArgumentException(INVALID_LENGTH_BRIDGE_MESSAGE);
+        }
+    }
+
+}

리뷰: 정적 팩토리 메서드 활용 👍 
혹시 정적 팩토리 메서드를 만드는 기준이 있을까요?
저부터 이야기를 드리면  생성자와 필드 타입이 일치하는 경우에는 생성자를 통해 생성하고,
다른 타입을 통해 생성할때는 정적 팩토리 메서드를 작성하고 있습니다!
@@ -0,0 +1,22 @@
+package bridge.domain;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class BridgeMaker {
+
+    private final BridgeNumberGenerator bridgeNumberGenerator;
+
+    public BridgeMaker(BridgeNumberGenerator bridgeNumberGenerator) {
+        this.bridgeNumberGenerator = bridgeNumberGenerator;
+    }
+
+    public List<String> makeBridge(int size) {
+        return IntStream.range(0, size)
+                .map(i -> bridgeNumberGenerator.generate())
+                .mapToObj(Direction::from)
+                .map(Direction::getInputCode)
+                .collect(Collectors.toList());
+    }
+}

리뷰: 스트림 활용 👍
@@ -1,4 +1,4 @@
-package bridge;
+package bridge.domain;
 
 @FunctionalInterface
 public interface BridgeNumberGenerator {

리뷰: BridgeNumberGenerator는 다리를 만들기 위한 숫자를 생성하는 클래스로 보입니다!
저는 해당 부분이 서비스에 가깝다고 생각했는데 도메인으로 생각하신 이유가 있으실까요?😀
@@ -0,0 +1,17 @@
+package bridge.util;
+
+import java.util.function.Supplier;
+
+public class Repeater {
+
+    public static <T> T repeatUntilNoException(Supplier<T> inputSupplier) {
+        while (true) {
+            try {
+                return inputSupplier.get();
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+}

리뷰: 사용자가 잘못된 값을 입력할 경우 반복해서 다시 입력 받기 위해서 만드신 클래스로 보입니다!😎
저는 입력받을때 해당 부분을 사용하고 있기 때문에 따로 클래스로 만들지 않고 InputView 내부에 정의했는데
이렇게 분리하였을때의 장점엔 무엇이 있나요?
@@ -0,0 +1,60 @@
+package bridge.view;
+
+import bridge.domain.Direction;
+import bridge.dto.GameResultDto;
+import bridge.dto.RoundResultDto;
+import bridge.dto.RoundResultsDto;
+
+import java.util.List;
+
+public class OutputMessageResolver {
+
+    private static final String ROUND_MESSAGE_PREFIX = "[";
+    private static final String ROUND_MESSAGE_POSTFIX = "]";
+    private static final String SEPARATOR = "|";
+    private static final String MOVE_SUCCESS_MARK = " O ";
+    private static final String MOVE_FAIL_MARK = " X ";
+    private static final String MOVE_NONE_MARK = "   ";
+    private static final String GAME_RESULT_MESSAGE_PREFIX = "최종 게임 결과";
+
+    public String resolveRoundResultMessage(RoundResultsDto roundResultsDto) {
+
+        List<RoundResultDto> roundResultDtos = roundResultsDto.getRoundResultDtos();
+        String upDirectionFootPrint = ROUND_MESSAGE_PREFIX;
+        String downDirectionFootPrint = ROUND_MESSAGE_PREFIX;
+        for (RoundResultDto roundResultDto : roundResultDtos) {
+            if (roundResultDto.getMoveDirection() == Direction.UP) {
+                upDirectionFootPrint += mapToMoveMark(roundResultDto.isMoveSuccess()) + SEPARATOR;
+                downDirectionFootPrint += MOVE_NONE_MARK + SEPARATOR;
+            }
+            if (roundResultDto.getMoveDirection() == Direction.DOWN) {
+                upDirectionFootPrint += MOVE_NONE_MARK + SEPARATOR;
+                downDirectionFootPrint += mapToMoveMark(roundResultDto.isMoveSuccess()) + SEPARATOR;
+            }
+        }
+
+        upDirectionFootPrint = upDirectionFootPrint.substring(0, upDirectionFootPrint.length() - 1);
+        downDirectionFootPrint = downDirectionFootPrint.substring(0, downDirectionFootPrint.length() - 1);
+        return upDirectionFootPrint + ROUND_MESSAGE_POSTFIX + "\n" + downDirectionFootPrint + ROUND_MESSAGE_POSTFIX;
+    }
+
+    public String resolveGameResultMessage(GameResultDto gameResultDto) {
+        StringBuilder stbd = new StringBuilder();
+        stbd.append(GAME_RESULT_MESSAGE_PREFIX + "\n");
+        stbd.append(resolveRoundResultMessage(gameResultDto.getRoundResultsDto()) + "\n");
+        if (gameResultDto.isGameSuccess()) {
+            stbd.append("게임 성공 여부: 성공\n");
+        }
+        if (!gameResultDto.isGameSuccess()) {
+            stbd.append("게임 성공 여부: 실패\n");
+        }
+        stbd.append(String.format("총 시도한 횟수: %d", gameResultDto.getRetryCount()));
+        return stbd.toString();
+    }
+    private String mapToMoveMark(boolean moveSuccess) {
+        if (!moveSuccess) {
+            return MOVE_FAIL_MARK;
+        }
+        return MOVE_SUCCESS_MARK;
+    }
+}

리뷰: 다리 출력을 위한 메소드로 보입니다!
해당 메서드 혼자 19줄을 사용중인것을 보면 분리의 여지가 있을것 같습니다~😁
@@ -0,0 +1,37 @@
+package bridge.view;
+
+import bridge.dto.GameResultDto;
+import bridge.dto.RoundResultsDto;
+
+/**
+ * 사용자에게 게임 진행 상황과 결과를 출력하는 역할을 한다.
+ */
+public class OutputView {
+    private static final String GREETING_MESSAGE = "다리 건너기 게임을 시작합니다.";
+    private final OutputMessageResolver outputMessageResolver;
+
+    public OutputView(OutputMessageResolver outputMessageResolver) {
+        this.outputMessageResolver = outputMessageResolver;
+    }
+
+    public void printGreeting() {
+        System.out.println(GREETING_MESSAGE);
+    }
+    /**
+     * 현재까지 이동한 다리의 상태를 정해진 형식에 맞춰 출력한다.
+     * <p>
+     * 출력을 위해 필요한 메서드의 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다.
+     */
+    public void printMap(RoundResultsDto roundResultsDto) {
+        System.out.println(outputMessageResolver.resolveRoundResultMessage(roundResultsDto));
+    }
+
+    /**
+     * 게임의 최종 결과를 정해진 형식에 맞춰 출력한다.
+     * <p>
+     * 출력을 위해 필요한 메서드의 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다.
+     */
+    public void printResult(GameResultDto gameResultDto) {
+        System.out.println(outputMessageResolver.resolveGameResultMessage(gameResultDto));
+    }
+}

리뷰: Dto 를 통해 view에서는 도메인을 직접적으로 의존하지 않고 있네요! 👍
@@ -0,0 +1,52 @@
+# 다리 건너기 게임
+
+위아래 둘 중 하나의 칸만 건널 수 있는 다리를 끝까지 건너가는 게임이다.
+
+- 위아래 두 칸으로 이루어진 다리를 건너야 한다.
+    - 다리는 왼쪽에서 오른쪽으로 건너야 한다.
+    - 위아래 둘 중 하나의 칸만 건널 수 있다.
+- 다리의 길이를 숫자로 입력받고 생성한다.
+    - 다리를 생성할 때 위 칸과 아래 칸 중 건널 수 있는 칸은 0과 1 중 무작위 값을 이용해서 정한다.
+    - 위 칸을 건널 수 있는 경우 U, 아래 칸을 건널 수 있는 경우 D값으로 나타낸다.
+    - 무작위 값이 0인 경우 아래 칸, 1인 경우 위 칸이 건널 수 있는 칸이 된다.
+- 다리가 생성되면 플레이어가 이동할 칸을 선택한다.
+    - 이동할 때 위 칸은 대문자 U, 아래 칸은 대문자 D를 입력한다.
+    - 이동한 칸을 건널 수 있다면 O로 표시한다. 건널 수 없다면 X로 표시한다.
+- 다리를 끝까지 건너면 게임이 종료된다.
+- 다리를 건너다 실패하면 게임을 재시작하거나 종료할 수 있다.
+    - 재시작해도 처음에 만든 다리로 재사용한다.
+    - 게임 결과의 총 시도한 횟수는 첫 시도를 포함해 게임을 종료할 때까지 시도한 횟수를 나타낸다.
+- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
+    - `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다.
+
+# 구현할 기능 목록
+## 입력
+- [ ] 다리의 길이를 입력 받을 수 있다.
+- [ ] 위 아래 중 유저가 이동하고자 하는 방향을 입력 받을 수 있다.
+- [ ] 게임 종료 시 게임 재시도 여부를 입력 받을 수 있다.
+
+## 출력
+- [ ] 매 라운드 마다 이동한 결과를 보여줄 수 있다.
+- [ ] 최종 게임 결과를 출력할 수 있다.
+
+## 입력 검증
+- [ ] 다리 길이 입력 검증
+  - [ ] 숫자가 아닌 값으로 입력할 경우 예외 처리
+  - [ ] 3이상 20 이하의 값이 아닌 경우 예외 처리
+- [ ] 위 아래중 유저가 이동할 칸 입력 검증
+  - [ ] U와 D중 하나의 문자를 선택 하지 않은 경우 예외 처리
+- [ ] 게임 재시작/종료 여부 입력 검증
+  - [ ] R과 Q중 하나의 문자를 선택 하지 않은 경우 예외 처리
+
+## 비즈니스 로직
+- [ ] 다리
+  - [ ] 특정 길이의 다리를 생성할 수 있다. 
+  - [ ] 다리는 ```U,D,U,U,D```와 같이 건널 수 있는 길이 생성 시 정해진다.
+  - [ ] 다리의 길은 생성 시 0과 1의 무작위 값을 통해서 정해진다. (0:D , 1:U)
+- [ ] 게임
+  - [ ] 이동 방향을 정했을 때 이동할 수 있는지 없는지 판단할 수 있다.
+  - [ ] 게임에 실패하는 경우 게임을 재시작하거나 종료할 수 있다.
+  - [ ] 다리 건너기에 실패하여 재시작하는 경우 도전중인 다리로 도전한다. 
+  - [ ] 다리 건너기에 성공할 경우 게임은 종료된다. 
+
+

리뷰: 꼼꼼한 readme 작성 좋습니다! 👍 
다만 체크리스트를 사용했다면 완성된 기능은` - [x]` 로 체크를 하면 좋을것 같습니다!😎
@@ -0,0 +1,64 @@
+package bridge.domain;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 다리 건너기 게임을 관리하는 클래스
+ */
+public class BridgeGame {
+    private static final int MIN_LENGTH = 3;
+    private static final int MAX_LENGTH = 20;
+    private static final String INVALID_LENGTH_BRIDGE_MESSAGE = "[ERROR] 다리 길이는 3부터 20 사이의 숫자여야 합니다.";
+
+    private final List<Direction> escapeRoute;
+    private int userPosition = 0;
+
+    private BridgeGame(List<Direction> escapeRoute) {
+        validate(escapeRoute);
+        this.escapeRoute = escapeRoute;
+    }
+
+    public static BridgeGame from(List<String> directions) {
+        List<Direction> escapeRoute = directions.stream()
+                .map(Direction::from)
+                .collect(Collectors.toList());
+        return new BridgeGame(escapeRoute);
+    }
+    /**
+     * 사용자가 칸을 이동할 때 사용하는 메서드
+     * <p>
+     * 이동을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다.
+     */
+    public boolean move(Direction direction) {
+        if (escapeRoute.get(userPosition).equals(direction)) {
+            userPosition++;
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isGameEnd () {
+        return userPosition == escapeRoute.size();
+    }
+
+    /**
+     * 사용자가 게임을 다시 시도할 때 사용하는 메서드
+     * <p>
+     * 재시작을 위해 필요한 메서드의 반환 타입(return type), 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다.
+     */
+    public void retry() {
+        userPosition = 0;
+    }
+
+    private void validate(List<Direction> escapeRoute) {
+        validateLength(escapeRoute);
+    }
+
+    private void validateLength(List<Direction> escapeRoute) {
+        if (escapeRoute.size() < MIN_LENGTH || escapeRoute.size() > MAX_LENGTH) {
+            throw new IllegalArgumentException(INVALID_LENGTH_BRIDGE_MESSAGE);
+        }
+    }
+
+}

리뷰: 나도 똑같이 하고 있음😃 매핑이 필요한 경우 등 생성로직에 무언가 추가될 만한 것이 있다면 정적 팩토리 메서드를 사용하고 있다 🙂
@@ -1,4 +1,4 @@
-package bridge;
+package bridge.domain;
 
 @FunctionalInterface
 public interface BridgeNumberGenerator {

리뷰: 너 말대로 서비스는 행위에 집중하는 레이어라고 나도 생각함

그런데 행위와 속성을 묶은, 우리가 서비스하는 분야를 도메인으로 정의한다면 도메인으로의 분리도 문제없다고 개인적으로 생각함 🤔 즉 도메인이 좀 더 포괄적인 개념이라고 생각한다

도메인 레이어와 서비스레이어의 구분은 개발 패턴 (ex: DDD)과 같은 것들에 영향을 받는 부분이라고 생각하고 있다 😄
@@ -0,0 +1,17 @@
+package bridge.util;
+
+import java.util.function.Supplier;
+
+public class Repeater {
+
+    public static <T> T repeatUntilNoException(Supplier<T> inputSupplier) {
+        while (true) {
+            try {
+                return inputSupplier.get();
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+}

리뷰: 유틸리티 클래스로 적용했기에 범용성있게 사용가능하다 실제로 해당 클래스는 입력에서만 사용되고 있지 않고 생성에 실패하는 경우에도 사용되고 있어 🙂
@@ -0,0 +1,79 @@
+package christmas.controller;
+
+import christmas.domain.badge.Badge;
+import christmas.domain.discount.DiscountDetails;
+import christmas.domain.discount.TotalDiscountEvent;
+import christmas.domain.gift.Gift;
+import christmas.domain.plan.EventDate;
+import christmas.domain.plan.Menu;
+import christmas.domain.plan.Order;
+import christmas.domain.plan.Plan;
+import christmas.dto.PlanResultDto;
+import christmas.view.InputView;
+import christmas.view.OutputView;
+import java.util.Map;
+import java.util.function.Supplier;
+
+public class ChristmasPlanerController {
+
+    private final TotalDiscountEvent discountEvent = TotalDiscountEvent.createDecemberEvent();
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+
+    public void run() {
+        startApplication();
+        Plan plan = inputPlan();
+        PlanResultDto planResult = applyEvents(plan);
+        printEventResult(planResult);
+    }
+
+    private void startApplication() {
+        outputView.printApplicationTitle();
+    }
+
+    private Plan inputPlan() {
+        EventDate date = readRepeatedlyUntilNoException(this::readDate);
+        Order order = readRepeatedlyUntilNoException(this::readOrder);
+        return Plan.of(date, order);
+    }
+
+    private <T> T readRepeatedlyUntilNoException(Supplier<T> supplier) {
+        try {
+            return supplier.get();
+        } catch (IllegalArgumentException exception) {
+            outputView.printExceptionMessage(exception);
+            return readRepeatedlyUntilNoException(supplier);
+        }
+    }
+
+    private EventDate readDate() {
+        int date = inputView.readDate();
+        return EventDate.from(date);
+    }
+
+    private Order readOrder() {
+        Map<Menu, Integer> order = inputView.readOrder();
+        return Order.from(order);
+    }
+
+    private PlanResultDto applyEvents(Plan plan) {
+        DiscountDetails discountDetails = discountEvent.makeDiscountDetails(plan);
+        Gift gift = Gift.from(plan.calculateTotalPrice());
+        int totalBenefitPrice = calculateTotalBenefitPrice(discountDetails, gift);
+        Badge badge = Badge.from(totalBenefitPrice);
+
+        return PlanResultDto.builder()
+                .plan(plan).discountDetails(discountDetails)
+                .gift(gift).badge(badge).build();
+    }
+
+    private int calculateTotalBenefitPrice(DiscountDetails discountDetails, Gift gift) {
+        int totalDiscountPrice = discountDetails.calculateTotalDiscountPrice();
+        int giftBenefitPrice = gift.calculateBenefitPrice();
+        return totalDiscountPrice + giftBenefitPrice;
+    }
+
+    private void printEventResult(PlanResultDto planResult) {
+        outputView.printPlanResult(planResult);
+    }
+}

리뷰: Plan으로 객체화 시키신게 인상 깊네요!
@@ -0,0 +1,33 @@
+package christmas.domain.badge;
+
+import java.util.List;
+
+public enum Badge {
+
+    SANTA(20_000),
+    TREE(10_000),
+    STAR(5_000),
+    NOTHING(0);
+
+    private static final List<Badge> ORDER_BY_BENEFIT = List.of(SANTA, TREE, STAR, NOTHING);
+
+    private final int minBenefitAmount;
+
+    Badge(int minBenefitAmount) {
+        this.minBenefitAmount = minBenefitAmount;
+    }
+
+    public static Badge from(int benefitAmount) {
+        return ORDER_BY_BENEFIT.stream()
+                .filter(badge -> badge.isReach(benefitAmount))
+                .findFirst().orElseThrow(Badge::createIllegalBenefitAmountException);
+    }
+
+    private static IllegalArgumentException createIllegalBenefitAmountException() {
+        return new IllegalArgumentException("benefit amount must not be negative");
+    }
+
+    private boolean isReach(int benefitAmount) {
+        return benefitAmount >= this.minBenefitAmount;
+    }
+}

리뷰: Enum은 vlaues()를 통해 선언 순서대로 가져올 수 있으니 따로 정의하시는대신 사용하셔도 좋을 것 같습니다!