프로젝트 개발과 함께 실시간으로 수정하며 작성하였습니다.
! 피드백 언제나 환영합니다 !
[깃허브] 레포지토리 바로가기
[트러블슈팅] 포스팅 바로가기
1. 요구사항 정리 & 클래스 설계
2025-01-13
- 필수 기능과 도전 기능을 정리하고 기능 설계
- 필수 기능
Lv. 1 | 햄버거 선택 출력 화면 구성 | |
Lv. 2 | 햄버거 메뉴를 MenuItem과 List를 통해 관리 | MeunItem |
Lv. 3 | main에서 관리하던 전체 순서를 제어하는 Kiosk 생성 유효하지 않은 입력에 대해 오류 메세지 출력 0을 입력하면 '뒤로가기' 혹은 '종료' |
Kiosk |
Lv. 4 | MenuItem을 관리하는 Menu생성 | Menu |
Lv. 5 | 캡슐화 적용 |
Lv.3 Kisok
- MenuItem을 관리하던 List를 필드로
- main에서 관리하던 입력은 start메서드로
- List<MenuItem>은 Kiosk 생성자로
Lv.4 Menu
- 상위 개념 '버거' 같은 카테고리 이름은 필드로 생성
- 버거(Burgers), 음료(Drinks), 디저트(Desserts)
- 도전 기능
Lv. 1 | 장바구니 · 구매하기 기능 추가 | ShoppingCart |
Lv. 2 | Enum, Lambda&Stream 활용 → 주문 · 장바구니 관리 |
- 클래스 설계
클래스 | Menu | MenuItem | ShoppingCart | BuyItem | Kiosk |
필드 | List<MeunItem> | String name Double price String explanation |
List<> Choose | ||
기능 | - 메뉴판 출력 - 메뉴 선택 |
- is empty? - 장바구니 출력 - 장바구니 메뉴 선택 - 총 금액 출력 - 내역 취소 (제거) |
- 할인 정보 출력 - 할인 선택 - 할인율 계산 |
- 전체 흐름 관리 |
- 예외 처리는 각 선택 메뉴마다 존재, 바른 번호를 입력하지 않았을 경우 예외 메시지 출력 → 다시 입력
2. 필수 기능 구현
2025-01-14
2025-01-14 09:50
계획
- 1레벨부터 3레벨 까지 완성하는 것이 목표.
- 16시 이전까지 3레벨을 완성했으면, 최대한 많이 필수과제 구현하기
2025-01-14 14:56
진행
필수 기능 1레벨부터 5레벨까지 완료
- 1레벨: 기본적인 키오스크 프로그래밍
Main
클래스 하나만 가지고 입출력 진행
- 2레벨: 햄버거 메뉴 클래스로 관리하기 → MenuItem
MenuItem
클래스에서name, price, explanation
관리Main
에서List<MenuItem>
을 만들어add
로 메뉴 추가 후 반복문을 통해 리스트 출력- 메뉴를 선택하면 선택한 메뉴에 대한 정보(이름, 가격, 설명) 출력
- 3레벨: 순서 제어를 클래스로 관리하기 → Kiosk
- Main에서
MenuItem
에 직접 접근하지 않고,Kiosk
를 통해서 접근 - Main에서 보여주던 입출력 내용을
Kiosk의 start()
메서드로 복사 → Main에서는 Kiosk 객체를 만들어 start() 호출 - 주어진 번호 외에 잘못된 번호를 입력했을 때 예외처리
- Main에서
- 4레벨&5레벨: 음식 메뉴와 주문 내역을 클래스 기반으로 관리하기 → Menu
- 음식(메뉴)에 관한 정보는 Menu클래스에, 주문에 관한 정보는 Kiosk로 관리
List<MenuItem>
은 Kiosk에서Menu
로 이동
< 주요 기능 접은글 확인 >
1. 카테고리 메뉴 출력
// 카테고리 메뉴 출력
private void printMenu(List<MenuItem> category) {
for (int i = 0; i < category.size(); i++) {
MenuItem item = category.get(i);
System.out.println((i+1)+". "+item.getName()+"\t| W "+ item.getPrice()+" | "+item.getExplanation());
}
System.out.println("0. 뒤로가기");
}
public void printBurgersMenu(){
System.out.println("[ BURGERS MENU ]");
printMenu(burgers);
}
public void printDrinksMenu(){
System.out.println("[ DRINKS MENU ]");
printMenu(drinks);
}
public void printDessertsMenu(){
System.out.println("[ DESSERTS MENU ]");
printMenu(desserts);
}
- 각 카테고리(버거, 음료, 디저트)를 각각의 List로 생성하여 필드로 관리하고 있다.
- 메뉴를 출력하는 메서드를 생성하여 파라미터로
List<MenuItem>
을 전달한다. - 선택한 카테고리의 내용을 출력한다.
- 이 메서드를 각 카테고리의 메뉴에서 호출하여, 카테고리 이름과 함께 전체 출력을 한다.
2. Kiosk에서 메뉴를 활용하기 위한 메서드
public int selectSize(List<MenuItem> select){
return select.size();
}
public MenuItem getSelectMenu(List<MenuItem> select, int idx){
return select.get(idx);
}
selectSize
는 각 카테고리 별로 저장된 메뉴의 수를 반환한다.getSelectMenu
는 소메뉴에서 선택한 제품(카테고리 안에서 제품 선택)을 반환한다.
3. Kiosk에서 선택한 카테고리 활용 방법
public void start(){
Scanner scanner = new Scanner(System.in);
int selectMenu = 0;
while(true){
List<MenuItem> category = new ArrayList<>();
// 메뉴 출력
...
//소메뉴 출력
try{
switch (selectMenu){
case 1:
category = menu.getBurgers();
menu.printBurgersMenu();
break;
case 2:
category = menu.getDrinks();
menu.printDrinksMenu();
break;
case 3:
category = menu.getDesserts();
menu.printDessertsMenu();
break;
}
...
- 전체 반복문을 시작할 때, 선택한 카테고리를 저장할
List<MenuItem> category
을 미리 생성한다. - 전체 메뉴가 출력되고, 그 중 특정 카테고리를 선택하면(selecMenu에 입력값 저장) 소메뉴로 넘어간다.
switch
를 이용해selectMenu
를 골라 처음 만들어둔category
에 각각의 리스트 대입- 이렇게 저장된 category를 이용해,
selectSize(List<MenuItem>)
와getSelectMenu(List<MenuItem>, int)
사용
해당 내용 커밋 ID: da3877e
계획
- Kiosk가 복잡해 보인다. 간결하게 만들 수 있을까?
- 도전 1레벨 장바구니·구매하기 기능 추가
- 단순 Getter보다 더 나은 캡슐화 방법 찾아보기
3. 객체 지향 프로그래밍 설계 및 도전 기능
2025-01-15
2025-01-15 09:40
계획
- 도전 기능에 들어가기 전, 기존에 구성했던 설계를 객체 지향적으로 변경하기.
- 처음 설계한 내용 → 도전 기능까지 포함하여 작성
클래스 | Menu | MenuItem | ShoppingCart | BuyItem | Kiosk |
필드 | List<MeunItem> | String name Double price String explanation |
List<> Choose | ||
기능 | - 메뉴판 출력 - 메뉴 선택 |
- is empty? - 장바구니 출력 - 장바구니 메뉴 선택 - 총 금액 출력 - 내역 취소 (제거) |
- 할인 정보 출력 - 할인 선택 - 할인율 계산 |
- 전체 흐름 관리 |
- 일단
Menu
를 기준으로 변경- 메뉴 출력
printCategory()
,printMenu(List<MenuItem>)
-print~Menu()
- 선택한 메뉴 반환
get~() : List<MenuItem>
- 선택한 메뉴 크기
selectSize(List<MenuItem> : int
- 선택한 제품 반환
getSelectMenu(List<MenuItem>, int) : MenuItem
- 메뉴 출력
- Menu는 메뉴를 관리하는 클래스 → 메뉴를 출력하는 부분과 떨어져도 되지 않을까?
- Menu의 할 일: 카테고리(버거, 음료, 디저트) 관리
- 카테고리 별로 클래스가 생성되어도 되지 않을까? → Menu를 인터페이스로 만들어 보자.
- Menu는 인터페이스로, 이를 구현하는 Burgers, Drinks, Desserts 클래스 생성
- Kiosk는 Menu와 접근 → 외부 접근은 인터페이스로 (DIP)
- 메뉴 출력은 어떻게? 일단 하나 안에 넣어 합치자
- 변경 내용 → 도전 기능(ShoppingCart, BuyItem) 제외
클래스 | MenuItem | Menu <I> | BurgersMenu | DrinksMenu | DessertsMenu | Kiosk |
역할 | 제품 구성 요소 | 메뉴 관리 | 버거 관리 | 음료 관리 | 디저트 관리 | 전체 흐름 제어 |
필드 | - name:String - price: Double - explanation: String |
List<MenuItem> | Menu를 implements한 클래스 인터페이스를 Overriding |
Menu menu | ||
기능 | + getName:String + getPrice:Double + getExplanation: String |
+ printMenu() + menuSize():int + getChoice(int): MenuItem |
+ start() |
- 인터페이스 내부에서 메서드가 아닌 필드를 만들어도 될까?
- 인터페이스에서 만든 이 필드를 구현체가 어떻게 사용할까?
public interface Menu2 {
List<MenuItem> category = new ArrayList<>();
public void printMenu();
}
public class BurgersMenu implements Menu2{
public BurgersMenu(){
category.clear();
category.add(new MenuItem("ShakeBuger",6.9,"토마토, 양상추, 쉑소스가 토핑된 치즈버거"));
category.add(new MenuItem("SmokeShack",8.9,"베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"));
category.add(new MenuItem("Cheeseburger",6.9,"포테이토 번과 비프패티, 치즈가 토핑된 치즈버거"));
category.add(new MenuItem("Hamburger",5.4,"비프패티를 기반으로 야채가 들어간 기본버거"));
}
@Override
public void printMenu() {
for (int i = 0; i < category.size(); i++) {
System.out.println(category.get(i).getName());
}
}
}
- 확인 용도로 만든 임의 인터페이스 Menu2
- 인터페이스 자체에서
List<MenuItem>
타입의 category를 인스턴스 - 구현클래스에서 category 이름을 이용해 접근 가능
- 시작 전에
clear()
를 해주지 않으면, 다른 객체를 만들어도 그 뒤에 계속 추가된다. - 인터페이스 자체의 category를 참조하는 것 같다.
- 버거클래스를 먼저 만들고, 그 뒤에 다른 객체로 음료클래스를 인스턴스해도 두 객체는 category에 추가
- 시작 전에
- 이 방법이 옳은 방법인지 모르겠지만 일단 이 방법으로 구현해보자.
2025-01-15 12:00
Menu를 인터페이스가 아닌 추상클래스로 설계하는 것으로 변경
각 클래스의 목적에 맞게 구현하는 인터페이스보다, 중복되는 멤버를 확장하는 추상클래스가 더 적합해보인다.
2025-01-15 12:35
진행
- Menu를 추상클래스로 변경하여 중복되는 코드를 하나로 사용
- Menu를 상속받는 BurgersMenu, DrinksMenu, DessertsMenu 생성
- 각 클래스의 생성자에서 Menu의 List<MenuItem>에 제품 추가
- 해당 카테고리에 소속된 제품을 출력하는 printCategoryMenu()만 오버라이딩, 나머지는 그대로 사용
public abstract class Menu {
public void printCategoryMenu() {
for (int i = 0; i < category.size(); i++) {
MenuItem item = category.get(i);
System.out.println((i+1)+". "+item.getName()+"\t| W "+ item.getPrice()+" | "+item.getExplanation());
}
System.out.println("0. 뒤로가기");
}
}
// 버거메뉴, 나머지도 동일하게 작성
public class BurgersMenu extends Menu {
public BurgersMenu(){
category.add(new MenuItem("ShakeBuger",6.9,"토마토, 양상추, 쉑소스가 토핑된 치즈버거"));
category.add(new MenuItem("SmokeShack",8.9,"베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"));
category.add(new MenuItem("Cheeseburger",6.9,"포테이토 번과 비프패티, 치즈가 토핑된 치즈버거"));
category.add(new MenuItem("Hamburger",5.4,"비프패티를 기반으로 야채가 들어간 기본버거"));
}
@Override
public void printCategoryMenu(){
System.out.println("[ BURGERS MENU ]");
super.printCategoryMenu();
}
}
- 반복문을 통해 카테고리를 모두 돌면서 제품을 출력하는 방식은 그대로 적용 → super
- 제품 출력 전, 해당 카테고리의 메뉴를 알리기 위한 출력문 추가
계획
- Kiosk를 리팩터링 하는 방법 생각하기
- 도전 기능 Lv.1 ~ Lv.2 구현 하기 → 장바구니
2025-01-15 17:20
진행
- 장바구니를 관리하는
ShoppingCart
추가- 메뉴를 선택하면 장바구니에 담을 것인지 질문하고, 추가한다.
- 장바구니에는 제품명, 수량, 가격 정보가 저장된다.
- 장바구니 항목을 추가, 조회할 수 있다.
- 결제 하기 전, 장바구니 내역을 조회하고 총 금액을 출력한다.
- 메뉴는 한번에 1개만 추가할 수 있다. → 장바구니에 같은 품목이 존재하는지 확인해야함
- 결제창에서 마지막 주문하기를 누르면 장바구니 내역이 초기화 된다.
- Kiosk에서 나타낼 것과 ShoppingCart에서 나타낼 것을 정리한다.
- Kiosk: 장바구니에 담을 것인지 질문하는 등 안내문
- ShoppingCart: 정보 관리, 메뉴 추가, 조회, 총 금액 계산, 중복 확인, 수량 계산, 초기화
- 장바구니에 들어갈 수량을 계산할 때 장바구니를 조회하여 중복된 품목이 있는지 검사하기로 했다. 조회는 단순하게 For문을 이용해 반복하며 비교했고, Boolean으로 반환하도록 만들었다.
- 장바구니에 같은 제품이 없으면 False를 반환하고, 장바구니에 같은 제품이 있으면 True를 반환한다. 같은 품목이 없을 경우에는 List에 새로운 품목을 인스턴스해 추가했고, 같은 제품이 있으면 수량을 1 증가 시켰다.
- 장바구니는 MenuItem과 같이 CartItem을 만들어서 관리했다. →
String name
,int quantity
,double price
처음에는 장바구니를 List<Class>
로 할지, List<Map<String, Object>>
로 할지 고민했다. map은 중복이 허용되지 않기 때문에 내장함수를 이용해 중복 여부를 판단하려고 했다. 품목마다 key로 name, quantity, price를 가지고 있으면 되지 않을까? 하지만 MenuItem처럼 필드를 가지고 있는 클래스 CartItem
을 만들었다. 어차피 기능을 사용하는 면에서는 같을테지만, 그 내부가 다르다고 생각되었다.
직접 CartItem을 생성하면, 어떤 리스트에 넣어도 CartItem은 name, quantity, price를 가지고 있다. 하지만 Map
을 통해 구현하면 key
값을 직접 입력하는 것이기 때문에, "이 내용을 입력해야 한다"는 조건이 사라진다고 생각되었다.
name, quantity, price말고도 rating 등 다른 값을 임의로 추가하게 될 수도 있어, 모든 요소가 다른 key를 가지게 될 가능성이 있다. 때문에 아예 그 값이 지정되도록 클래스를 생성하는 걸 선택했다.
클래스 | CartItem | ShoppingCart | Kiosk |
필드 | - name:String - quantity:int - price:double |
- cart:List<CartItem> | cart:ShoppingCart menu:Menu |
메서드 | + getPrice():double + setQuantity(int) + getQuantity():int + getName():String |
- priceCalculation(CartItem):double + clearAll() + getCart():List<CartItem> + addItem(String, double) + isOrder():Boolean - existProduct(CartItem) + printCart() + totalPrice() - isExist(String):Boolean |
+ start() - cartMode():int - orderMode(int) - checkCart() - printMenu() - printOrder() |
내부에서 사용하는 메서드는 바로바로 private
로 바꿔주었다.
Kiosk가 좋지 않아보인다(start
) 어떻게 분리하면 좋을까
계획
- 내일은 도전 기능 Lv.2 완료
Lambda & Stream
을 활용해 장바구니를 조회하고,stream.filter
를 이용해 특정 메뉴 제거하기Enum
을 활용해 사용자 유형별 할인율 관리하기
- 추가로 Kiosk에서 장바구니에 제품을 담았을때 뜨는 '5번 취소'버튼을 다시 구현해야 할 것 같다. 요구사항에는 그저 취소한다고만 나와있는데 저 위치가 메뉴를 제거하는 위치가 아닐까?
- 기능을 조금 더 분리하고 캡슐화할 수 있는 방법을 찾아보자!
2025-01-16
2025-01-16 10:57
도전 기능 2레벨 정리
- Enum을 사용해 할인 적용을 위한 사용자 유형 관리
- Lambda&Stream을 활용해 장바구니를 조회하고, 메뉴 제거
계획
- 먼저 할인을 위한 Enum타입의 Discount를 생성할 예정
- 국가유공자 PATRIOTS, 군인 SOLDIER, 학생 STUDENT, 일반 COMMON
- 계산은 quantity * 할인율 => 결과 반환
- 할인율 = (100 - 할인퍼센트) * 0.01
- Enum에서
추상메서드를 만들어, 각 상수에서 메서드를 구현하는 방식을 이용 - 상수에 할인퍼센트를 넣어주고, 그 값을 출력해서 사용자에게 보여주자.
※ 추상 메서드 사용 안 함. 바로 다음에 수정 내용 작성
2025-01-16 12:15
진행
- 추상메서드가 아닌 일반 메서드를 만들어주었다
- Enum 상수에 매개변수로 넣은 rate에 따라서 할인이 적용되고, 그 계산은 동일 → 동일 함수, 파라미터로 조절
- Enum의 각 요소에 저장된 값을 가져오는 getter를 생성
- public이 아닌 private로 생성
- 무작정 getter를 이용하는 건 오히려 캡슐화를 위반하게 될 수 있다. getter를 생성하되, 외부에서 getter로 직접 접근하지 않고 다른 메서드를 통해 우회하여 값을 가져오도록 했다.
printDiscountInformation()
에서 getter 사용
- Kiosk에 추가된 메서드는
printDiscount()
와applyDiscount()
- printDiscount():int - Discount에 있는
printDiscountInformation()
을 호출하고, Scanner로 유형 입력 - applyDiscount():Discount - printDiscount()를 호출하는 메서드, 여기서 값을 받아 Enum요소 적용
- 두 메서드 모두
throws
로 예외를 던져준다. printDiscount()에서 입력받은 값에 대한 예외 처리
- printDiscount():int - Discount에 있는
신경 쓴 부분
- Kiosk의 할인 정보를 출력하는 printDiscount()
// Discount <E>
public void printDiscountInformation(){
Discount[] discount = Discount.values();
for (int i = 0; i < discount.length; i++) {
System.out.println((i+1)+". "+discount[i].getType()+" : "+discount[i].getRate()+"%");
}
}
// Kiosk
private int printDiscount() throws BadInputException {
System.out.println("할인 정보를 입력해주세요.");
Discount discount = Discount.COMMON;
discount.printDiscountInformation();
...
}
- 전에 작성해둔 메인 메뉴를 출력하는 Kiosk의 printMenu()
private void printMenu(){
System.out.println("[ MAIN MENU ]");
System.out.println("1. Burgers\n2. Drinks\n3. Desserts");
System.out.println("0. 종료");
}
위 두 메서드의 차이가 있다.
먼저 작성한 printMenu()
는 그 메뉴를 직접 출력하고 있다.
만약 새로운 카테고리 Chicken이 생기면, 카테고리도 추가하고, 메뉴 출력에 관한 모든 메서드도 수정해야 한다.
하지만 오늘 생성한 할인 정보를 출력하는 printDiscount()
는 직접 출력하고 있지 않다.
저장된 데이터를 배열로 가져와 순회하면서 배열의 요소를 하나씩 참조한다.
이 경우 새로운 유형 elderly가 추가되면, Enum에서 새로운 ELDERLY("노인", 7)만 추가하면 된다.
출력문을 따로 수정하지 않아도 자연스럽게 추가되며, 입력값에 따라 Discount를 선택하는 조건문만 추가하면 할인율 계산도 가능하다.
- Enum에서 요소 추가, Kiosk의 applyDiscount() 내부 switch에 case 5 추가

계획
- 위 접은글을 보면 새로운 할인 유형을 추가했을 때, Enum이 아닌 Kiosk에서도 수정이 필요했다.
외부 모듈인 Kiosk에 영향을 주지 않고 Enum에서만 수정이 일어날 수 있도록 수정해야 할 것 같다. - 도전 2레벨의 두번째 기능 - 장바구니 조회, 삭제 기능 추가
- 오늘은 도전 기능을 제대로 구현하는 것을 목표로 한다. 그리고 내일은 SOLID를 지켜 프로그램을 수정하고 싶다.
2025-01-16 15:15
진행
- 조회 메서드 Stream을 사용하도록 변경
- 제품의 이름을 비교해 같은 제품의 CartItem을 반환하는
find(String)
생성- 기존에 장바구니에 제품을 추가하는 add메서드 변경
- 이전: isExist()에서 제품 수량을 직접 추가 → 기능 분리 안 됨
개선: isExist(String)이 일 경우find(String):CartItem
으로 받아, 해당 CartItem의 수량 +1 - 해당 제품을 삭제하는 delete메서드에서도 find 활용
- deleteItem(String)을 생성해 선택한 품목을 삭제한다.
- 만약 장바구니에 담긴 수량이 2개 이상일 경우 수량 -1
1개만 담겨있으면 remove로 제품을 삭제한다.
- 만약 장바구니에 담긴 수량이 2개 이상일 경우 수량 -1
- ShoppingCart와 Kiosk가 제품의 이름과 수량을 주고 받기 위한
nameToQuantity()
생성- Map<String, Integer>로 반환하여 name을 key로, quantity를 찾는다.
- Map<String, Integer>로 반환하여 name을 key로, quantity를 찾는다.
- Kiosk에서 삭제를 위한 deleteCart()생성
- nameToQuantity()를 통해 가져온 map을 화면에 출력하며, 사용자에게 삭제할 제품을 질문한다.
- 출력을 할 때 사용한 반복변수를 이용해 Map<Integer, String>으로 새로운 map 생성
- 1.A / 2.B ← 출력되면 사용자는 번호 1을 입력
입력받은 input을 key로 이용하여 value A를 가져온다. - 가져온 value는 String → ShoppingCart의 deleteItem(String)을 호출하여 삭제
※ 파란색, 보라색은 데이터 이동 흐름
현재까지 클래스 구성도
- 제품 메뉴 관련
클래스 | MenuItem | Menu | BurgersMenu | DrinksMenu | DessertsMenu |
필드 | - name:String - price:Double - explanation:String |
category: List<MenuItem> |
|||
메서드 | + getName():String + getPrice():Double + getExplan:String |
+ printCategoryMenu + menuSize():int + getChoice(int):MenuItem |
@Override + printCategoryMenu() |
- 장바구니 관련
클래스 | CartItem | ShoppingCart | Discount <E> |
필드 | - name:String - quantity:int - price:double |
- cart:List<CartItem> | + PATRIOTS + STUDENT + SOLDIER + COMMON - type:String - rate:int |
메서드 | + getName():String + getPrice():double + getQuantity():int + setQuantity(int) |
+ printCart() + isOrder():Boolean + clearAll() + addItem(String, double) + deleteItem(String) + nameToQuantity():Map<String, Integer> + totalPrice():double - priceCalculation(CartItem):double - isExist(String):Boolean - find(String):CartItem |
+ calculateDiscount(double):double + valueOf(String):Discount + printDiscountInformation() + values():Discount[] - getType():String - getRate():int |
- 클래스 다이어그램
추가 개선점
- Kiosk의 출력문 수정 → 다형성을 위한 수정 필요 → 직접 출력 x 참조 o
- 캡슐화가 제대로 지켜졌는지 확인 → 도트 연산자로 내부 데이터에 접근할 수 있으면 캡슐화x
- 카테고리, 제품, 할인 유형을 새로 추가하며 확장성 확인 → OCP, 캡슐화, 결합도, 응집도
- 기능을 더 쪼갤 수 있는 것은 분리하고, 인터페이스로 구현할 수 있는 것 나누기
이후, 개선안은 새 포스팅으로 [트러블슈팅] 작성
'내일배움캠프' 카테고리의 다른 글
일정 관리를 위한 서버를 간단하게 만들어보자 (0) | 2025.01.24 |
---|---|
키오스크 프로젝트 개발 트러블 슈팅 정리 (0) | 2025.01.17 |
[TIL/WIL] 2-3주차 과제: 계산기 만들기 (0) | 2025.01.07 |
스타터 노트 (1) | 2024.11.25 |
STEP 1. 시작하기 (0) | 2024.11.25 |