Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2c7ac0b
bridgeInfo 생성
Oct 28, 2025
601989e
feat : application
Oct 30, 2025
45188f7
feat : controller
Oct 30, 2025
6bc5102
feat : Bridge
Oct 30, 2025
25114d9
feat : BridgeStep
Oct 30, 2025
2d98c95
feat : 사다리 하강 로직
Oct 30, 2025
4ae4ed7
feat : 플레이어 관리 클래스
Oct 30, 2025
e2f073b
feat : 도착지에 있는 보상 클래스
Oct 30, 2025
e46be8a
feat : 문자열 분리 메소드
Oct 30, 2025
c29c860
feat : view
Oct 30, 2025
5aeab91
refactor : 스트림 이용
Oct 30, 2025
d6ac423
refactor : 들여쓰기 적용 및 라인 정리
Oct 30, 2025
c90102f
feat : READ.ME 작성
Oct 30, 2025
53b8d01
feat : READ.ME 작성
Oct 30, 2025
00f8644
feat : 테스트 코드 작성
Oct 30, 2025
35bc4d1
refactor(controller) : 한 가지 목적을 가진 코드들 메소드로 분리
Nov 2, 2025
9eff889
refactor(controller) : isAll과 while문의 break문
Nov 2, 2025
4b32197
refactor(controller) : split 메소드 적용
Nov 2, 2025
1007500
refactor(controller) : split 메소드 적용2
Nov 2, 2025
e6710c5
refactor(outputView) : 메소드 이름 수정
Nov 2, 2025
af2e6a8
refactor(outputView) : printAll과 printSpecificPlayer 리팩토링
Nov 2, 2025
7ed8f0a
refactor(outputView) : Bridge와 BridgeRow로 리팩토링
Nov 2, 2025
04b80c8
refactor(outputView) : Bridge와 BridgeRow로 리팩토링2
Nov 2, 2025
4db07ab
refactor : Rewards 수정
Nov 2, 2025
4fe7fca
refactor : 종속성 수정
Nov 2, 2025
cf2700c
feat : LadderResult 와 테스트 코드
Nov 3, 2025
82afadc
refactor(controller) : height 선언 시점 수정
Nov 5, 2025
88d3fc3
refactor(OutputView,controller) : getWidthAndHeight 리팩토링
Nov 5, 2025
86db6b1
refactor(Bridge) : 정적 팩토리 메소드 적용
Nov 5, 2025
c60e922
refactor(Test) : 테스트 코드 클래스 별 분리
Nov 5, 2025
cee5e54
refactor(Test) : 람다식을 이용한 soft assertion 적용
Nov 5, 2025
3c3f2e2
refactor(Test) : 람다식을 이용한 soft assertion 적용
Nov 5, 2025
12dcf0d
refactor : 전략패턴을 이용한 다리 생성 로직 수정
Nov 5, 2025
3dac03c
refactor : formatter를 이용한 뷰 클래스 리팩토링
Nov 5, 2025
7b421a9
refactor : formatter를 이용한 controller 리팩토링
Nov 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 자바 사다리 게임

## 주요 기능

* 쉼표로 구분하여 여러 플레이어 이름 입력
* 사다리 끝에 위치할 보상 입력
* 사다리 높이 설정
* 가로 라인이 무작위로 생성되는 사다리
* 최종 생성된 사다리 모양 출력
* 각 플레이어의 최종 결과(보상) 표시
* 전체 플레이어 또는 특정 플레이어의 결과 조회 기능

## 코드 설명
* **`Application.java`**: 프로그램의 시작점입니다. 전체 게임의 흐름을 제어합니다.

* **`controller` 패키지**:
* `Controller.java`: 사용자의 입력을 받아 Model을 업데이트하고, 결과를 View에 전달하는 등 게임의 핵심 로직을 관리합니다.

* **`model` 패키지**: 게임의 데이터와 비즈니스 로직을 담당합니다.
* `Player.java`: 게임에 참여하는 플레이어들의 정보를 관리합니다.
* `Rewards.java`: 사다리 게임의 결과로 주어지는 보상 정보를 관리합니다.
* `Bridge.java`: 전체 사다리의 구조를 나타냅니다. 가로 줄(`BridgeStep`)들의 리스트로 구성됩니다.
* `BridgeStep.java`: 사다리의 한 칸이 가로 줄을 가지고 있는지 여부(`EXIST` 또는 `EMPTY`)를 나타내는 Enum입니다.
* `LadderDescentService.java`: 플레이어가 사다리를 내려가는 과정을 처리하는 서비스 클래스입니다.
* `SplitMethod.java`: 사용자 입력을 쉼표 기준으로 분리하는 유틸리티 클래스입니다.

* **`view` 패키지**: 사용자 인터페이스(입력/출력)를 담당합니다.
* `InputView.java`: 사용자로부터 플레이어 이름, 보상, 사다리 높이 등의 입력을 받습니다.
* `OutputView.java`: 게임 진행 상황, 사다리 모양, 최종 결과 등을 콘솔에 출력합니다.
8 changes: 8 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Controller.Controller;

public class Application {
public static void main(String[] args) {
Controller controller = new Controller();
controller.startLadder();
}
}
43 changes: 43 additions & 0 deletions src/main/java/Controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package Controller;

import Model.Bridge;
import Model.Player;
import Model.Rewards;
import view.InputView;
import view.OutputView;

public class Controller {
InputView inputView = new InputView();
OutputView outputView = new OutputView();

public void startLadder() {
int height;
Copy link

Choose a reason for hiding this comment

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

height와 isAll이 가장 위에 있는데 먼저 선언한 이유가 있나요??

Copy link
Author

Choose a reason for hiding this comment

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

따로 의미를 두고 필드를 위에 선언 한 이유는 없습니다!

Copy link

Choose a reason for hiding this comment

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

아하 보통 변수는 선언과 동시에 초기화하는게 좋아요

boolean isAll = false;
outputView.askPlayers();
String inputs = inputView.getScanner().nextLine();
Player players = new Player(inputs);
Copy link

Choose a reason for hiding this comment

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

지금 줄바꿈으로 구역을 나눠주어서 쉽게 변경 가능할만한 것 같은데요
한가지 목적을 위해 생성되는 코드들은 private 메서드로 하나로 합쳐주면 좀 더 역할 분담이 잘된 코드가 생길 것 같아요

    private Player getPlayers() {
        outputView.askPlayers();
        String inputs = inputView.getScanner().nextLine();
        return new Player(inputs);
    }

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정하겠습니다!


outputView.askRewards();
inputs = inputView.getScanner().nextLine();
Copy link

Choose a reason for hiding this comment

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

변수명에 의미를 담아주시면 훨씬 좋아보여요
ex) inputs: 무엇을 하는 inputs일까?? >> 윗줄을 보고 askRewards임을 알 수 있지만, rewardInputs로 변수명을 사용하면 굳이 윗줄 코드에 대해 생각하지 않아도 됨

그리고 변수는 기본적으로 한 번 초기화된 상태로 쭉 사용하는 용도이고, 같은 변수가 여러번 달라지는건 특별한 경우라고 생각하시면 편해요. 일종의 국룰? 같은거로 생각하시면 됩니다. 왜냐하면 코드 읽는 입장에서는 코드 읽으면서 특정 변수도 계속 기억해야하는 상황이기 때문에 코드 이해하는 속도가 느려진다고 생각하시면 좋아요

Copy link
Author

Choose a reason for hiding this comment

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

아아 저는 다시 변수를 선언하는 것이 오히려 불필요한 과정이라고 생각했었는데 오히려 가독성 부분에선 리뷰어님의 말씀이 맞는 거 같습니다. 기억해 놓겠습니다!

Copy link

Choose a reason for hiding this comment

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

개발은 읽기 쉬운 코드, 관리하기 쉬운 코드가 1순위라고 생각하시면 좋습니다요

Rewards rewards = new Rewards(inputs);

outputView.askLadderHeight();
height = inputView.getWidthAndHeight();
Copy link

Choose a reason for hiding this comment

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

맨위에 있는 height는 여기랑 합치면 좋아보여요~

Suggested change
height = inputView.getWidthAndHeight();
int height = inputView.getWidthAndHeight();

Copy link
Author

Choose a reason for hiding this comment

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

수정하겠습니다!


outputView.printPlayersAndRewards(players.getPlayers());
Bridge bridge = new Bridge(height, players.getPlayersNumber() - 1);
outputView.print(bridge);

outputView.printPlayersAndRewards(rewards.getRewards());

players.playerOnDestination(bridge);
inputView.getScanner().nextLine();

while (!isAll) {
outputView.askResults();
String string = inputView.getScanner().nextLine();

isAll = outputView.printRewards(rewards, players, string);
}
Copy link

Choose a reason for hiding this comment

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

isAll을 계속 가지고 있는 상태로 while문을 돌리는 대신, 차라리 무한루프문을 만들어서 all을 입력할 때 break문으로 탈출하는건 어떨까요??

아래에 적어둔 OutputView 코멘트도 그렇고, 위에 isAll 선언도 그렇고 isAll이 범인이네요 ㅋㅋ

Copy link
Author

Choose a reason for hiding this comment

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

저 혹시 무한루프와 isAll을 가지고 있는 while문에 어떤 차이가 존재하는지 궁금합니다.
printRewards가 all이라면 true를 리턴하는 메소드여서 while문의 조건으로 구조했었는데 저는 무한루프와 큰차이를 모르겠어서 질문드립니다!!

Copy link

Choose a reason for hiding this comment

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

코드를 읽을 때 변수를 하나 더 기억하고, 관련 메서드를 확인해야하는지의 유무라고 생각하시면 됩니다

앞으로 개발하면서 우리는 새로 작성하는 코드보다 같은 코드를 반복해서 읽는 시간이 많다는 점을 알고 있어야 하는데요
(읽어보면 좋은 글)

이런 맥락으로 생각하면 코드 자체는 isAll이든 무한루프문이든 큰 차이는 없지만, 무한루프문이 코드 읽고 수정하는 입장에서는 하나의 메서드만 봐도 되니 좀 더 관리하기 편한 코드라고 생각하시면 편해요

  • isAll을 가지고 있는 while문: isAll을 결정하는 outputView.printRewards(...) 내부 코드까지 확인해야함
  • 무한루프문: string == "all"을 통해 break문을 사용하면 outputView.printRewards(...)를 선택적으로 볼 수 있는 기회가 주어짐

Copy link
Author

Choose a reason for hiding this comment

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

아아 넵 감사합니다!! 메소드를 실행하기 전에 if문으로 메소드의 실행여부를 한번 확인하는 로직이 관리하기 편한 코드가 된다는 걸 이해했습니다!

}
}
66 changes: 66 additions & 0 deletions src/main/java/Model/Bridge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package Model;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Collections;

public class Bridge {

private final List<List<BridgeStep>> rows;
Copy link

Choose a reason for hiding this comment

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

2차원 배열보다는 List<BridgeStep>을 한 번 더 감싼 BridgeLine 같은 객체가 있으면 좋아보여요

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정하겠습니다!

private final Random random = new Random();

public Bridge(int height, int width) {
this.rows = new ArrayList<>();
initializeBridgeRows(height, width);
}
Copy link

Choose a reason for hiding this comment

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

이거는 생성자 방식으로 만들기보다 정적 팩토리 메서드 방식으로 만드는게 좋아보여요~

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정하겠습니다!!


private void initializeBridgeRows(int height, int width) {
for (int i = 0; i < height; i++) {
this.rows.add(createOneRow(width));
}
}

private List<BridgeStep> createOneRow(int width) {
List<BridgeStep> row = generateRandomRow(width);
ensureNoSideBySideSteps(row);
return row;
}

private List<BridgeStep> generateRandomRow(int width) {
List<BridgeStep> row = new ArrayList<>();
for (int i = 0; i < width; i++) {
row.add(BridgeStep.createRandomStep(this.random));
}
return row;
}

private void ensureNoSideBySideSteps(List<BridgeStep> row) {
for (int i = 0; i < row.size() - 1; i++) {
fixSideBySideStepIfNeeded(row, i);
}
}

private void fixSideBySideStepIfNeeded(List<BridgeStep> row, int index) {
if (areBothStepsExisting(row, index)) {
setOneToNoneRandomly(row, index);
}
}


private boolean areBothStepsExisting(List<BridgeStep> row, int index) {
return row.get(index).isExist() && row.get(index + 1).isExist();
}

private void setOneToNoneRandomly(List<BridgeStep> row, int index) {
if (this.random.nextBoolean()) {
row.set(index, BridgeStep.NONE);
return;
}
row.set(index + 1, BridgeStep.NONE);
}

public List<List<BridgeStep>> getRows() {
return Collections.unmodifiableList(this.rows);
}
}
26 changes: 26 additions & 0 deletions src/main/java/Model/BridgeStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// BridgeStep.java
package Model;

import java.util.Random;

public enum BridgeStep {
NONE(0), // 다리 없음
EXIST(1); // 다리 있음

private final int value;

BridgeStep(int value) {
this.value = value;
}

public static BridgeStep createRandomStep(Random random) {
if (random.nextBoolean()) {
return EXIST;
}
return NONE;
}

public boolean isExist() {
return this == EXIST;
}
}
42 changes: 42 additions & 0 deletions src/main/java/Model/LadderDescentService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package Model;

import java.util.List;

public class LadderDescentService {
Copy link

Choose a reason for hiding this comment

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

로직 깔끔해서 좋네요 👍👍

private Bridge bridge;

public LadderDescentService(Bridge bridge) {
this.bridge = bridge;
}

public int descent(int curPos){
int pos = curPos;
for (List<BridgeStep> row : bridge.getRows()) {
pos += checkRightBridge(row,pos) + checkLeftBridge(row,pos);
}
return pos;
}


private int checkRightBridge(List<BridgeStep> row, int pos) {
if (pos >= row.size()) {
return 0;
}
if (row.get(pos) == BridgeStep.EXIST) {
return 1;
}
return 0;
}

private int checkLeftBridge(List<BridgeStep> row, int pos) {
if (pos == 0) {
return 0;
}
if (row.get(pos - 1) == BridgeStep.EXIST) {
return -1;
}
return 0;
}


}
34 changes: 34 additions & 0 deletions src/main/java/Model/Player.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package Model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Player extends SplitMethod {
List<String> players;
List<Integer> destinatioinPos = new ArrayList<>();

public Player(String players) {
this.players = split(players);
}

public List<String> getPlayers() {
return Collections.unmodifiableList(players);
}

public int getDestinatioinPos(String string) {
return destinatioinPos.get(players.indexOf(string));
}

public int getPlayersNumber() {
return players.size();
}

public void playerOnDestination(Bridge bridge) {
LadderDescentService descentService = new LadderDescentService(bridge);

for (String player : players) {
destinatioinPos.add(descentService.descent(players.indexOf(player)));
}
}
Copy link

Choose a reason for hiding this comment

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

여기도 Rewards랑 비슷하게 사다리가 플레이어에 의존하고 있네요

스크린샷 2025-11-01 오후 10 49 52

만약 플레이어 이름 대신 팀으로 하게 된다면 또 거기에 맞는 코드를 추가해야하는데요

어디에도 종속 받지 않도록 하려면 어떤 인덱스가 어떤 인덱스로 도착하는지만 가지고 있으면 될 것 같아요
나중에 필요하면 플레이어 + 사다리게임 결과를 조합하면 되니 독립적인 객체가 될 수 있어보이네요

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정하겠습니다!!

Copy link
Author

Choose a reason for hiding this comment

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

}
20 changes: 20 additions & 0 deletions src/main/java/Model/Rewards.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package Model;

import java.util.Collections;
import java.util.List;

public class Rewards extends SplitMethod {
List<String> rewards;

public Rewards(String rewards) {
this.rewards = split(rewards);
}

public String getRewardskey(int index) {
return rewards.get(index);
}
Copy link

Choose a reason for hiding this comment

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

Rewards는 Players의 이름 순서와 Rewards의 보상 순서가 서로 매칭된다는 내용을 알고 있어야 이해할 수 있는 코드인데요
Rewards가 이름과 보상 모든 정보를 가지고 있으면 위 히스토리를 몰라도 독립적으로 사용할 수 있어보여요

Copy link
Author

Choose a reason for hiding this comment

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


public List<String> getRewards() {
return Collections.unmodifiableList(rewards);
}
}
22 changes: 22 additions & 0 deletions src/main/java/Model/SplitMethod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package Model;

import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class SplitMethod {
Copy link

Choose a reason for hiding this comment

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

입력값을 모델에서 처리하기 위해 SplitMethod로 만들고, Player, Rewards가 상속하도록 만들었군요?!
그런데 상속의 경우에는 단 하나의 클래스만 상속이 가능하기 때문에 최대한 사용을 지양하는게 좋은 편입니다
(ex. 나중에 여러 종류의 Rewards와 Player가 나와서 최상위 클래스가 필요하다면?)

Player, Rewards 코드는 최대한 유지하면서 extends를 사용 안하게끔하는게 뭐가 있을까 고민해봤는데, splitMethod의 역할은 입력값을 정상적인 값들만 리스트 형태로 변환해서 반환하는 메서드이므로 여러 곳에 사용이 가능하니 유틸 클래스라고 생각할 수 있어보여요

그리고 SplitMethod를 Model에서 처리하는게 아니라 Controller에서 사용해서 Player와 Rewards에서는 배열로 파라미터를 받아서 처리하면 model은 깔끔한 코드가 유지될 것 같습니다

Copy link
Author

Choose a reason for hiding this comment

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

넵 반영해보겠습니다!

Copy link
Author

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

protected static List<String> split(String input) {

final Pattern DELIMITER = Pattern.compile(",");

if (input == null || input.isEmpty()) {
return Collections.emptyList();
}

return DELIMITER.splitAsStream(input)
.map(String::trim) // 양쪽 공백 제거
.filter(name -> !name.isEmpty()) // 빈 값 제거 (예: "neo,,brie")
.collect(Collectors.toList());
}
}
15 changes: 15 additions & 0 deletions src/main/java/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package view;

import java.util.Scanner;

public class InputView {
Scanner scanner = new Scanner(System.in);

public int getWidthAndHeight() {
return scanner.nextInt();
}

public Scanner getScanner() {
return scanner;
}
}
78 changes: 78 additions & 0 deletions src/main/java/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package view;


import Model.Bridge;
import Model.BridgeStep;
import Model.Player;
import Model.Rewards;

import java.util.Arrays;
import java.util.List;

public class OutputView {
public void print(Bridge bridge) {
for (List<BridgeStep> row : bridge.getRows()) {
printRow(row);
System.out.print("|");
System.out.println();
}

}

private void printRow(List<BridgeStep> row) {
row.forEach(bridgeStep -> {
System.out.print("|");
printBridgeStep(bridgeStep);
});
}
Copy link

Choose a reason for hiding this comment

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

여기도 변수명처럼 함수명에 무엇을 출력하는 목적의 메서드인지 이름을 부여하는게 좋아요~

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정하겠습니다!!

Copy link
Author

Choose a reason for hiding this comment

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


public void printPlayersAndRewards(List<String> strings) {
strings.forEach(string -> System.out.printf("%-6s", string));
System.out.println();
}

private void printBridgeStep(BridgeStep bridgeStep) {
if (bridgeStep == BridgeStep.EXIST) {
System.out.print("-----");
} else {
System.out.print(" ");
}
}

public void askResults() {
System.out.println("결과를 보고 싶은 사람은?");
}

public boolean printRewards(Rewards rewards, Player players, String inputNames) {
Copy link

Choose a reason for hiding this comment

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

OutputView는 단순히 출력해야하는 목적만 가지고 있으면 베스트인데, 여기서 all을 입력했는지 or 안했는지도 판단해서 boolean을 반환하고 있네요. 요거는 위에 isAll 관련 코멘트 해결하면 같이 해결될 것으로 보이네요

추가로 단순 출력 목적만 생각하면 지금처럼 if문으로 나누기보다는 all을 출력하는 메서드와 사용자를 출력하는 메서드 두가지로 나눠서 만들 수도 있을 것 같네요

Copy link
Author

Choose a reason for hiding this comment

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

넵 수정하겠습니다!

Copy link
Author

Choose a reason for hiding this comment

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

System.out.println("실행 결과");
String trimmedInput = inputNames.trim();
if (trimmedInput.equals("all")) {
players.getPlayers().stream()
.forEach(player -> System.out.println(player + " : " + rewards.getRewardskey(players.getDestinatioinPos(player))));
return true;
}

Arrays.stream(trimmedInput.split(","))
.map(String::trim)
.forEach(trimmedName -> {
try {
System.out.println(trimmedName + " : " + rewards.getRewardskey(players.getDestinatioinPos(trimmedName)));
} catch (Exception e) {
throw new IllegalArgumentException("[ERROR] 존재하지 않는 플레이어 이름입니다: " + trimmedName);
}
});
return false;
}

public void askPlayers() {
System.out.println("참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)");
}

public void askRewards() {
System.out.println("실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)");
}

public void askLadderHeight() {
System.out.println("사다리의 높이는 몇 개인가요?");
}
}
Loading