ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -4,8 +4,6 @@
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
-import reviewme.reviewgroup.controller.ReviewGroupSession;
-import reviewme.reviewgroup.domain.ReviewGroup;
 import reviewme.template.service.TemplateService;
 import reviewme.template.service.dto.response.SectionNamesResponse;
 
@@ -16,10 +14,8 @@ public class SectionController {
     private final TemplateService templateService;
 
     @GetMapping("/v2/sections")
-    public ResponseEntity<SectionNamesResponse> getSectionNames(
-            @ReviewGroupSession ReviewGroup reviewGroup
-    ) {
-        SectionNamesResponse sectionNames = templateService.getSectionNames(reviewGroup);
+    public ResponseEntity<SectionNamesResponse> getSectionNames() {
+        SectionNamesResponse sectionNames = templateService.getSectionNames();
         return ResponseEntity.ok(sectionNames);
     }
 }

리뷰: 이 부분은 사실 회의에서  `/templates/{templateId}/sections`로 하자고 했었는데, 
왜 테드가 기존 api 그대로 두고 고정 응답을 하자고 했는지 코드를 보면서 깨달았어요 ㅠ
templateId 를 받게 해봤자, templateId 를 지정해서 생성하는 곳도 없고, 응답으로 내려주는 곳도 없고...
지금 변경을 하더라도 맥락에 안맞는 변경 같더라고요😅

그래서 기존 내용대로 그대로 뒀습니다! 
대신 defaultTemplate 관련된 내용이 응집도있게 존재할 수 있도록 
DefaultTemplateService 를 만들어봤어요.
@@ -22,6 +22,7 @@
 public class AdminController {
     private final AdminService adminService;
 
+    // 자원에 대한 복수형 표기
     @PostMapping("/artist")
     public ResponseEntity<ArtistDTO> createArtist(@Validated @RequestBody NewArtistDTO newArtistDTO) {
         return ResponseEntity.ok(adminService.createArtist(newArtistDTO));

리뷰: 다른 곳은 url 상의 자원(ex. artist)을 복수형으로 표기했는데 없는 곳도 있네요
통일성을 가지면 좋을 거 같아요~
@@ -84,6 +84,7 @@ public ResponseEntity<PageResponse<MusicContentDTO>> searchMusic(
         return ResponseEntity.ok(musicService.searchMusic(query, mcType, pageRequest));
     }
 
+    // 단어 구분 통일시키기 auto-complete
     @GetMapping("/search/autocomplete")
     public ResponseEntity<List<String>> autoCompleteSearchKeyword(
         @RequestParam(name = "q") String query,

리뷰: 메소드명은 autoComplete 인데, url 명은 autocomplete 이네요
단어를 구분지어서 auto-complete 으로 통일성있게 네이밍 하시면 좋을 거 같아요.
@@ -4,6 +4,7 @@
 import com.anarchyadventure.music_dabang_api.entity.user.User;
 import lombok.Data;
 
+// RequestDto, ResponseDto 구분해주기 (패키지명 or 클래스명)
 @Data
 public class NewPlaylistDTO {
     private String name;

리뷰: 요청 dto 인지 응답 dto 인지를 패키지로 분리해주거나, 클래스명으로 구체적으로 명시해주면 다른 사람이 봤을 때 좀 더 편하게 구분할 수 있을 거 같아요
@@ -8,6 +8,8 @@
 @Data
 public class KakaoUserDTO {
     private Long id;
+
+    // 외부 API 필드명에 의존하지 않도록 @JsonProperty 사용
     private KakaoUserInfo kakao_account;
 
     @Data
@@ -20,6 +22,7 @@ public static class KakaoUserInfo {
         private Profile profile;
 
         public LocalDate getBirth() {
+            // StringUtils 사용
             if (birthday == null || birthyear == null) {
                 return null;
             }
@@ -34,6 +37,7 @@ public LocalDate getBirth() {
         }
 
         public Gender getGender() {
+            // null 보다는 UNKNOWN 고려
             if (gender == null) {
                 return null;
             }

리뷰: 외부 API 응답이 snake case 로 내려온다고 해서 필드명을 그대로 맞출 필욘 없을 거 같아요. 외부 API 응답은 snake case 로 받고 java 코드 내에서는 camel case 를 사용하도록 @JsonProperty, @JsonNaming 등을 활용해보면 좋을 거 같아요

https://lvolz.tistory.com/77
@@ -8,6 +8,8 @@
 @Data
 public class KakaoUserDTO {
     private Long id;
+
+    // 외부 API 필드명에 의존하지 않도록 @JsonProperty 사용
     private KakaoUserInfo kakao_account;
 
     @Data
@@ -20,6 +22,7 @@ public static class KakaoUserInfo {
         private Profile profile;
 
         public LocalDate getBirth() {
+            // StringUtils 사용
             if (birthday == null || birthyear == null) {
                 return null;
             }
@@ -34,6 +37,7 @@ public LocalDate getBirth() {
         }
 
         public Gender getGender() {
+            // null 보다는 UNKNOWN 고려
             if (gender == null) {
                 return null;
             }

리뷰: StringUtils 메소드를 사용하면 null 체크와 blank 체크를 한번에 하실 수 있어요

ex)
```java
if (StringUtils.isBlank(birthday) || StringUtils.isBlank(birthyear)) {
    return null;
}
```
@@ -8,6 +8,8 @@
 @Data
 public class KakaoUserDTO {
     private Long id;
+
+    // 외부 API 필드명에 의존하지 않도록 @JsonProperty 사용
     private KakaoUserInfo kakao_account;
 
     @Data
@@ -20,6 +22,7 @@ public static class KakaoUserInfo {
         private Profile profile;
 
         public LocalDate getBirth() {
+            // StringUtils 사용
             if (birthday == null || birthyear == null) {
                 return null;
             }
@@ -34,6 +37,7 @@ public LocalDate getBirth() {
         }
 
         public Gender getGender() {
+            // null 보다는 UNKNOWN 고려
             if (gender == null) {
                 return null;
             }

리뷰: 저는 enum 값을 null 로 반환하는 것을 지양하는 편인데요. null 은 npe 등의 이슈가 있으니 '알수없음(ex. UNKNOWN)'을 나타내는 enum 을 정의하시는 걸 추천드립니다
@@ -38,6 +38,7 @@ public class MusicContent extends BaseEntity {
     private MusicContentType musicContentType = MusicContentType.MUSIC;
 
     public MusicContent(String thumbnailUrl, String musicContentUrl, String videoContentUrl, String title, Artist artist) {
+        // 필수값, 길이 제한 등에 대한 validation (+ dto 보다는 생성자에 기록)
         this.thumbnailUrl = thumbnailUrl;
         this.musicContentUrl = musicContentUrl;
         this.videoContentUrl = videoContentUrl;

리뷰: 필수값이나 길이 제한 등에 대해 생성자에 validation 코드를 넣어도 좋을 거 같아요
@@ -8,6 +8,7 @@ public enum MusicContentType {
 
     @JsonCreator
     public static MusicContentType from(String value) {
+        // java 8 stream 사용하기
         for (MusicContentType type : MusicContentType.values()) {
             if (type.name().equalsIgnoreCase(value)) {
                 return type;

리뷰: java 8 의 stream 을 사용하시는 걸 추천드립니다. 가독성을 위해 최대한 들여쓰기 depth 가 2가 넘어가지 않게 코딩해보시는 연습을 해보시는 게 좋습니다.

ex)

```java
    public static MusicContentType from(String value) {
        return Arrays.stream(MusicContentType.values())
            .filter(type -> type.name().equalsIgnoreCase(value))
            .findFirst()
            .orElse(null);
    }
```
@@ -70,6 +70,8 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
         try {
             Long userId = Long.parseLong(jwt.getSubject());
             Optional<User> userOpt = userRepository.findById(userId);
+
+            // early return 고려
             if (userOpt.isPresent()) {
                 principalDetails = new PrincipalDetails(userOpt.get());
                 if (tokenType.equals(ACCESS_TOKEN_NAME)) {

리뷰: depth 가 깊어지면 기본적으로 가독성이 떨어지게 되어있어요. early return 할 수 있는 부분은 early return 하는 것도 방법이에요. (절대적인 방법은 아닙니다!)

https://woonys.tistory.com/209
@@ -29,6 +29,7 @@ public static User getUser() {
         return null;
     }
 
+    // service 에서 매번 해당 메소드를 사용하는 것이 아니라, ArgumentResolver 로 편하게 처리하기
     public static User getUserAuth(UserRole... userRoles) {
         User user = getUser();
         if (user == null) throw new UnauthorizedException();

리뷰: service 에서 매번 `SecurityHandler.getUserAuth()` 를 호출하는데 User 정보를 가져오기 위함으로 보여요. 나중에 ArgumentResolver 를 사용해서 처리해보는 것도 좋을 거 같아요

https://velog.io/@uiurihappy/Spring-Argument-Resolver-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9C%A0%EC%A0%80-%EC%A0%95%EB%B3%B4-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B8%B0
@@ -68,6 +68,7 @@ public MusicContentDTO editMusic(Long musicId, NewMusicContentDTO newMusicConten
             newMusicContentDTO.getVideoContentUrl(),
             newMusicContentDTO.getTitle()
         );
+        // jpa 더티 체킹을 이용하여 update
         musicContentRepository.save(musicContent);
         return MusicContentDTO.from(musicContent);
     }

리뷰: jpa 를 사용하고 계신데 영속성 컨텍스트에서 관리하게끔 @Transactional 을 붙여주면 save 메소드는 별도로 호출할 필요가 없겠네요
@@ -67,7 +67,11 @@ public PlaylistItemDTO addMyMusicListItem(Long musicId) {
         User user = SecurityHandler.getUserAuth();
         PlaylistItem item = new PlaylistItem(user, music, null);
         Long lastOrderingNum = playlistItemRepository.lastOrderingNumInMyMusicList(user.getId());
+        // addPlaylistItem 과 중복 로직
         if (lastOrderingNum != null) {
+            // 매직 넘버보다는 상수를 사용
+            // 천 단위는 언더바 사용하는 습관
+            // setter 사용은 지양. 생성자로 처리할 수 있을 거 같은데 시도해보기.
             item.setOrderingNum(lastOrderingNum + 1000L);
         }
         playlistItemRepository.save(item);

리뷰: `addPlaylistItem` 에서 중복 로직이 좀 보이는데 별도 메소드로 빼는 걸 고려해보셔도 좋을 거 같아요
@@ -67,7 +67,11 @@ public PlaylistItemDTO addMyMusicListItem(Long musicId) {
         User user = SecurityHandler.getUserAuth();
         PlaylistItem item = new PlaylistItem(user, music, null);
         Long lastOrderingNum = playlistItemRepository.lastOrderingNumInMyMusicList(user.getId());
+        // addPlaylistItem 과 중복 로직
         if (lastOrderingNum != null) {
+            // 매직 넘버보다는 상수를 사용
+            // 천 단위는 언더바 사용하는 습관
+            // setter 사용은 지양. 생성자로 처리할 수 있을 거 같은데 시도해보기.
             item.setOrderingNum(lastOrderingNum + 1000L);
         }
         playlistItemRepository.save(item);

리뷰: 1. 1000L 이 의미하는 부분을 별도 상수로 정의하는게 가독성을 위해 좋을 거 같아요.
2. 천 단위는 언더바를 사용해 구분하는 습관을 가지면 좋아요

ex) `private static final DEFAULT_ADDITIONAL_ORDERING_NUM = 1_000L;`

3. setter 사용은 최대한 지양하는게 좋아요. item 을 생성자를 만들어 파라미터를 넘겨 처리 가능해보이는데 시도해보면 어떨까요?
@@ -37,6 +37,7 @@ public TokenDTO loginWithKakao(String accessToken) {
             user = new User(
                 OAuthProvider.KAKAO,
                 kakaoUser.getId().toString(),
+                // 디미터 법칙 고려
                 kakaoUser.getKakao_account().getProfile().getNickname(),
                 kakaoUser.getKakao_account().getProfile().getProfile_image_url(),
                 kakaoUser.getKakao_account().getPhone_number(),

리뷰: 객체의 getter 를 줄줄이 호출하는 구조네요. 디미터 법칙에 대해 알아보셔도 좋을 거 같아요.

https://mangkyu.tistory.com/147
@@ -0,0 +1,32 @@
+package racingcar.controller.Validation
+
+class RaceCountValidation(
+    private val raceCount: String,
+) {
+    fun validateRaceCount() {
+        isNotEmpty()
+        isLong()
+        isInteger()
+        over1()
+    }
+
+    private fun isNotEmpty() {
+        require(raceCount.isNotEmpty()) { RaceCountErrorType.EMPTY_INPUT.errorMessage }
+    }
+
+    private fun isLong() {
+        require(raceCount.toLongOrNull() != null) { RaceCountErrorType.NO_LONG.errorMessage }
+    }
+
+    private fun isInteger() {
+        require(raceCount.toIntOrNull() != null) { RaceCountErrorType.INTEGER.errorMessage }
+    }
+
+    private fun over1() {
+        require(raceCount.toInt() >= RACE_COUNT_OK) { RaceCountErrorType.UP_1.errorMessage }
+    }
+
+    companion object {
+        private const val RACE_COUNT_OK = 1
+    }
+}
\ No newline at end of file

리뷰: Int형 검사를 하기 전에 Long Type 으로 검사하신 이유가 궁금합니다 ㅎㅎ
Int로 표현 가능한 범위를 초과한 경우를 위해서 하신걸까용?
@@ -0,0 +1,19 @@
+package racingcar.model
+
+import racingcar.util.RandomNumber
+import javax.swing.text.Position
+
+data class Car(
+    val name: String,
+    private var _position: Int = 0,
+) {
+    val position: Int
+        get() = _position
+
+    fun move() {
+        val randomNumber = RandomNumber().getRandomNumber()
+        if (randomNumber >= 4) {
+            _position++
+        }
+    }
+}

리뷰: 외부에서 Position을 변경할 수 없도록 하신게 좋네요 !
@@ -0,0 +1,27 @@
+package racingcar.view
+
+import racingcar.model.Car
+
+class OutputView {
+    fun showStartMessage() {
+        println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)")
+    }
+
+    fun showRaceCount() {
+        println("시도할 횟수는 몇 회인가요?")
+    }
+
+    fun showPlayStart() {
+        println("실행결과")
+    }
+
+    fun showEachRoundResult(cars: List<Car>) {
+        for (car in cars) {
+            println("${car.name} : ${"-".repeat(car.position)}")
+        }
+    }
+
+    fun showWinner(winners: List<String>) {
+        println("최종 우승자 : ${winners.joinToString(", ")}")
+    }
+}
\ No newline at end of file

리뷰: 저는 사용자에게 메시지를 보여줄 메시지를 만드는 것두
하나의 비즈니스 로직이라고 생각해서 메시지를 가공후 OutputView에 전달하도록 구현한 터라
채채님두 OutputView는 출력에 대한 역할만 하도록 하는건 어떨까 싶어요 !
@@ -0,0 +1,10 @@
+package racingcar.view
+
+import camp.nextstep.edu.missionutils.Console
+
+class InputView {
+    fun getInput(): String {
+        val userInput = Console.readLine().trim()
+        return userInput
+    }
+}
\ No newline at end of file

리뷰: 오! 여기서 trim() 을 써도 깔끔하겠군요 👍
@@ -0,0 +1,27 @@
+package racingcar.view
+
+import racingcar.model.Car
+
+class OutputView {
+    fun showStartMessage() {
+        println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)")
+    }
+
+    fun showRaceCount() {
+        println("시도할 횟수는 몇 회인가요?")
+    }
+
+    fun showPlayStart() {
+        println("실행결과")
+    }
+
+    fun showEachRoundResult(cars: List<Car>) {
+        for (car in cars) {
+            println("${car.name} : ${"-".repeat(car.position)}")
+        }
+    }
+
+    fun showWinner(winners: List<String>) {
+        println("최종 우승자 : ${winners.joinToString(", ")}")
+    }
+}
\ No newline at end of file

리뷰: 맞아요! 찬호님 코드 보면서 그 부분이 너무 좋았어요! outputview은 출력만 하는 게 좋았습니다! 저도 찬호님 코드 다시보면서 제 코드에 어떻게 적용할 수 있는 지 고민해봐야겠어요! 의견 감사합니다!
@@ -0,0 +1,32 @@
+package racingcar.controller.Validation
+
+class RaceCountValidation(
+    private val raceCount: String,
+) {
+    fun validateRaceCount() {
+        isNotEmpty()
+        isLong()
+        isInteger()
+        over1()
+    }
+
+    private fun isNotEmpty() {
+        require(raceCount.isNotEmpty()) { RaceCountErrorType.EMPTY_INPUT.errorMessage }
+    }
+
+    private fun isLong() {
+        require(raceCount.toLongOrNull() != null) { RaceCountErrorType.NO_LONG.errorMessage }
+    }
+
+    private fun isInteger() {
+        require(raceCount.toIntOrNull() != null) { RaceCountErrorType.INTEGER.errorMessage }
+    }
+
+    private fun over1() {
+        require(raceCount.toInt() >= RACE_COUNT_OK) { RaceCountErrorType.UP_1.errorMessage }
+    }
+
+    companion object {
+        private const val RACE_COUNT_OK = 1
+    }
+}
\ No newline at end of file

리뷰: 네 맞아요!!! long 타입 숫자는 에러로 내야할거 같아요! long 타입도 허용하면 고려할 타입이 2개나 생겨서 최대한 고려할 요소를 줄이자라는 게 시험에 임하는 저의 자세입니다... 하하하 실제로 프로덕트 만들 때는 이러지 말자..... 나 자신아...
@@ -0,0 +1,86 @@
+package racingcar.controller
+
+import racingcar.controller.Validation.CarsNameValidation
+import racingcar.controller.Validation.RaceCountValidation
+import racingcar.controller.domain.UserInteractionController
+import racingcar.model.Car
+
+class RacingCar(
+    private val userInteractionController: UserInteractionController = UserInteractionController()
+) {
+    fun run() {
+        val cars = startCarsName()
+        val playCount = startRaceCount()
+        val doneCars = startGame(playCount, cars)
+        val winnerNames = getWinner(doneCars)
+        showWinners(winnerNames)
+    }
+
+    private fun getCarsName(): String {
+        val carsName = userInteractionController.handleCarsName()
+        return carsName
+    }
+
+    private fun getRaceCount(): String {
+        val raceCount = userInteractionController.handleRaceCount()
+        return raceCount
+    }
+
+    private fun validateCarName(carsName: String) {
+        val carsNameValidation = CarsNameValidation(carsName)
+        carsNameValidation.validateCarsName()
+    }
+
+    private fun validateRaceCount(raceCount: String) {
+        val raceCountValidation = RaceCountValidation(raceCount)
+        raceCountValidation.validateRaceCount()
+    }
+
+    private fun startCarsName(): List<Car> {
+        val carsName = getCarsName()
+        validateCarName(carsName)
+        val cars = adapterCarsName(carsName)
+        return cars
+    }
+
+    private fun startRaceCount(): Int {
+        val raceCount = getRaceCount()
+        validateRaceCount(raceCount)
+        val playCount = adapterRaceCount(raceCount)
+        return playCount
+    }
+
+    private fun adapterCarsName(carsName: String): List<Car> {
+        val cars = carsName.split(COMMA).map { Car(it.trim()) }
+        return cars
+    }
+
+    private fun adapterRaceCount(raceCount: String): Int {
+        return raceCount.toInt()
+    }
+
+    private fun startGame(playCount: Int, cars: List<Car>): List<Car> {
+        userInteractionController.handlePlayStart()
+        for (count in START_GAME..playCount) {
+            cars.forEach { it.move() }
+            userInteractionController.handleEachRoundResult(cars)
+        }
+        return cars
+    }
+
+    private fun getWinner(cars: List<Car>): List<String> {
+        val winnerPosition = cars.map { it.position }.max()
+        val winner = cars.filter { winnerPosition == it.position }
+        val winnerName = winner.map { it.name }
+        return winnerName
+    }
+
+    private fun showWinners(winnersName: List<String>) {
+        userInteractionController.handleWinners(winnersName)
+    }
+
+    companion object {
+        private const val COMMA = ","
+        private const val START_GAME = 1
+    }
+}
\ No newline at end of file

리뷰: controller를 이렇게 쓰는 거군요! 함수이름들이 깔끔하고 정렬도 잘되어있어서 한눈에 알아보기 쉽네요 :)
@@ -0,0 +1,44 @@
+package racingcar.controller.Validation
+
+class CarsNameValidation(
+    private val carsName: String,
+) {
+    fun validateCarsName() {
+        isNotEmpty()
+        checkCarsCount()
+        checkCarNameEmpty()
+        checkCarName5()
+        checkSameName()
+    }
+
+    private fun isNotEmpty() {
+        require(carsName.isNotEmpty()) { CarsNameErrorType.EMPTY_INPUT.errorMessage }
+    }
+
+    private fun checkCarsCount() {
+        val carsCount = carsName.split(COMMA).size
+        require(carsCount >= RACING_OK_COUNT) { CarsNameErrorType.UP_1.errorMessage }
+    }
+
+    private fun checkCarNameEmpty() {
+        val carsName = carsName.split(COMMA)
+        require(carsName.all { it.isNotEmpty() }) { CarsNameErrorType.EMPTY_INPUT.errorMessage }
+    }
+
+    private fun checkCarName5() {
+        val carsName = carsName.split(COMMA)
+        require(carsName.all { it.length <= CAR_NAME_OK_LENGTH }) { CarsNameErrorType.UNDER_5.errorMessage }
+    }
+
+    private fun checkSameName() {
+        val carsName = carsName.split(COMMA)
+        require(carsName.size == carsName.toSet().size) { CarsNameErrorType.SAME_NAME.errorMessage }
+    }
+
+    companion object {
+        private const val COMMA = ","
+        private const val RACING_OK_COUNT = 2
+        private const val CAR_NAME_OK_LENGTH = 5
+
+    }
+}
\ No newline at end of file

리뷰: 콤마를 상수화 하신 게 정말 좋네요!!
@@ -0,0 +1,19 @@
+package racingcar.model
+
+import racingcar.util.RandomNumber
+import javax.swing.text.Position
+
+data class Car(
+    val name: String,
+    private var _position: Int = 0,
+) {
+    val position: Int
+        get() = _position
+
+    fun move() {
+        val randomNumber = RandomNumber().getRandomNumber()
+        if (randomNumber >= 4) {
+            _position++
+        }
+    }
+}

리뷰: 4도 상수화를 해서 변경이 쉽게 하면 좋을 것 같습니다
@@ -0,0 +1,15 @@
+package racingcar.controller.Validation
+
+enum class RaceCountErrorType(private val _errorMessage: String) {
+    EMPTY_INPUT("경주 횟수에 빈 문자를 입력했습니다."),
+    NO_LONG("경주 횟수에는 21억보다 큰 수는 입력하지 못합니다."),
+    INTEGER("경주 횟수에는 숫자만 입력이 가능합니다."),
+    UP_1("경주 횟수는 1보다 큰 수만 입력이 가능합니다.");
+
+    val errorMessage: String
+        get() = ERROR_MESSAGE + _errorMessage
+
+    companion object {
+        private const val ERROR_MESSAGE = "[ERROR] "
+    }
+}
\ No newline at end of file

리뷰: enum도 controller 폴더안에 들어가는군요 배워갑니다
@@ -0,0 +1,19 @@
+package racingcar.model
+
+import racingcar.util.RandomNumber
+import javax.swing.text.Position
+
+data class Car(
+    val name: String,
+    private var _position: Int = 0,
+) {
+    val position: Int
+        get() = _position
+
+    fun move() {
+        val randomNumber = RandomNumber().getRandomNumber()
+        if (randomNumber >= 4) {
+            _position++
+        }
+    }
+}

리뷰: 넷! 알겠습니다!!
@@ -1 +1,44 @@
-# java-convenience-store-precourse
+우아한테크코스 7기 프리코스 4주차 과제 : 편의점(java-convenience-store-precourse)
+===============================================================================
+
+기능 목록
+---------
+* AppConfig: Controller, Service, Repository, View 객체 구현체를 설정하여 객체를 생성한다.
+* InputView: 입력과 관련된 안내 메시지를 출력하고 입력을 받아서 StoreController에 전달한다.
+  * 입력값이 Y 또는 N이어야 하는 경우 입력받은 문자열을 검증해서 잘못된 형식이면 IllegalArgumentException를 호출하고, 예외 메시지를 출력하고 다시 입력을 받는다.
+* OutputView: StoreController로부터 전달받은 결과값을 출력한다.
+* StoreController: 프로그램의 메인 흐름을 제어하고, View와 Service 계층을 매개하는 역할을 한다. 
+  * InputView로부터 모델 생성에 필요한 값을 전달받아서 모델 객체를 생성하고, StoreServcie에 전달한다.
+  * StoreService에서 생성한 결과 값을 요청해서 전달받고 이를 OutputView에 전달한다.
+* StoreService: controller에서 전달 받은 모델을 가지고 주요 비즈니스 로직을 실행하며, 결과값을 Repository에 저장하거나, Controller에 반환한다.
+* Repository: 주요 모델를 저장한다.
+* Inventory: 편의점 상품 재고 상황을 보관한다.
+* Stock: 상품 재고 상황(프로모션 재고와 일반재고)을 저장한다.
+* Product: 편의점에서 파는 상품 정보(이름, 가격, 적용되는 프로모션 정보)를 담는다.
+* Promotion: 프로모션 할인 정보(이름, 보너스 수량, 행사 시작일, 종료일)를 담는다.
+* Promotions: Promotion 객체를 컬렉션으로 보관한다.
+* OrderDetails: 주문 정보(주문 상품, 수량)를 저장한다.
+* OrderDetailsFactory: 주문 정보 객체(OrderDetails)를 만드는 팩토리 클래스이다. 주문 정보 문자열을 받아서 검증하며 형식이 맞지 않으면 IllegalException를 호출한다. 
+* OrderQuantityOption: 주문한 상품 프로모션 할인과 관련된 수량 조절 선택지를 담는다. 옵션 종류(OptionCase)는 주문한 상품이 프로모션 재고가 없는 경우, 프로모션 적용 보너스 상품을 추가로 받을 수 있는 주문인 경우 두 가지이다.
+  * 상품별 경우와, 이를 안내할 메시지, 고객의 선택을 저장한다.
+* OrderQuantityOptions: OrderQuantityOption 객체를 컬렉션으로 보관한다.
+* QuantityOptionsFactory: 주문 정보(OrderDetails)와 재고 현황(Inventory)과 주문시간을 바탕으로 비교해서 주문 중에 수량을 조절해야 하는 경우(OptionCase)가 있는지 확인하고 OrderQuantityOptions 객체를 생성한다.
+  * 먼저 프로모션을 적용할 수 있는지 확인한다.
+    * 프로모션을 적용할 수 없다면 넘어간다.
+    * 프로모션을 적용할 수 있다면 주문한 수량이 프로모션 재고를 초과하는지 확인한다.
+      * 프로모션 재고를 초과하면 일반재고로 주문할 선택지를 저장한다.
+      * 재고를 넘어가지 않는다면 주문한 수량이 모든 증정품을 포함하는지 확인한다.
+        * 모든 증정품을 포함하지 않는다면 증정품을 포함한 수량만큼 재고가 있는지 확인하고 재고가 있으면 주문 수량 추가 선택지를 저장한다.
+        * 만약 재고가 없다면 넘어간다.
+* OrderProductQuantity: 주문한 상품 수량을 저장한다. 수량 정보는 총 구매수량, 프로모션 재고 차감 수량, 일반 재고 차감 수량으로 이루어진다.
+* OrderQuantityAdjuster: 주문한 상품 수량 조절 선택지(OrderQuantityOptions)에 대한 주문자의 선택을 바탕으로 수량을 조절하며, 주문 수량(OrderProductQuantity)과 재고 현황(Inventory)를 바탕으로 주문 수량에서 프로모션 재고 차감분과 일반 재고 차감분을 갱신한다.
+* PromotionDiscountDetail: 주문 상품 별 프로모션 할인 적용 정보(프로모션 이름, 증정품 수량, 증정품 가격 합)를 저장한다. 
+* PromotionDiscountDetails: PromotionDiscountDetail를 컬렉션으로 보관한다. 주문 정보, 편의점 재고 현황, 주문 시간을 가지고 상품 별 프로모션 할인 정보를 만들어서 보관한다.
+* MembershipDiscountDetail: 주문에 대한 멤버십할인 금액를 저장한다.
+* Receipt: 고객에게 보여줄 영수증을 담당하는 클래스, 주문 정보와 할인 내역을 표시한다. 주문 정보, 프로모션 할인 정보, 멤버십 할인 정보를 바탕으로 영수증 객체를 생성한다.
+* InventoryInitializer: 재고 파일에서 재고 현황을 읽어서 초기화한다.
+* PromotionInitializer: 프로모션 파일에서 상품별 프로모션 정보를 읽어서 초기화한다.
+* StringValidator: 여러 클래스에서 공통으로 쓰는 문자열 검증 기능을 담당한다.
+* Constants: 여러 클래스에서 공통으로 쓰는 상수를 저장한다.
+* ExceptionMessage: 예외 메시지를 저장한다.
+* KoreanStringFormatter: 자리수에 맞추어 정렬된 문자열을 만든다.
\ No newline at end of file

리뷰: 클래스별 역할을 정리하는 것보다, 전체적인 흐름과 역할별 기능 목록을 정리하는게 어떨까요?
@@ -0,0 +1,34 @@
+package store.config;
+
+import store.controller.StoreController;
+import store.repository.MemoryStoreRepository;
+import store.repository.StoreRepository;
+import store.service.StoreService;
+import store.service.StoreServiceImpl;
+import store.view.InputView;
+import store.view.OutputView;
+
+/** Controller, Service, Repository, View 객체 구현체를 설정하여 객체를 생성하는 클래스 */
+public class AppConfig {
+    
+    private StoreRepository getRepository() {
+        return new MemoryStoreRepository();
+    }
+    
+    private InputView getInputView() {
+        return new InputView();
+    }
+    
+    private OutputView getOutputView() {
+        return new OutputView();
+    }
+    
+    private StoreService getService() {
+        return new StoreServiceImpl(getRepository());
+    }
+    
+    public StoreController getStoreController() {
+        return new StoreController(getInputView(), getOutputView(), getService());
+    }
+    
+}

리뷰: 클래스명으로 추측 가능한 내용이라 주석은 없어도 될 것 같습니다!