본문 바로가기
TIL

메뉴판 프로그램

by 스니펫 2023. 10. 23.

※ 전체 코드 : 깃허브

 

프로젝트 소개

  1. 뉴판을 보고 주문할 수 있는 Java 프로그램
  2. 화면은 System.out.println() 메소드를 사용해서 심플하게 출력
  3. 메뉴 클래스와 주문 클래스를 사용하여 Java 의 핵심 기능인 상속을 최대한 사용

 

요구사항 및 출력 예시

Java 클래스 설계 시 필수 요구사항

  • 메뉴 클래스는 이름, 설명 필드를 가짐
  • 상품 클래스는 이름, 가격, 설명 필드를 가짐
  • 상품 클래스의 이름, 설명 필드는 메뉴 클래스를 상속받아 사용하는 구조
  • 주문 클래스에서 상품 객체를 담을 수 있도록 함

1. 메인 메뉴판 화면

  • 메인 메뉴판이 출력되며 메뉴판에는 상품 메뉴가 출력
  • 상품 메뉴는 간단한 설명과 함께 출력 되며 최소 3개 이상 출력
  • 상품 메뉴 아래에는 Order(주문)와 Cancel(주문 취소) 옵션을 출력
"SHAKESHACK BURGER 에 오신걸 환영합니다."
아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.

[ SHAKESHACK MENU ]
1. Burgers         | 앵거스 비프 통살을 다져만든 버거
2. Forzen Custard  | 매장에서 신선하게 만드는 아이스크림
3. Drinks          | 매장에서 직접 만드는 음료
4. Beer            | 뉴욕 브루클린 브루어리에서 양조한 맥주

[ ORDER MENU ]
5. Order       | 장바구니를 확인 후 주문합니다.
6. Cancel      | 진행중인 주문을 취소합니다.

2. 상품 메뉴판 화면

  • 상품 메뉴 선택 시 해당 카테고리의 메뉴판이 출력
  • 메뉴판에는 각 메뉴의 이름과 가격과 간단한 설명이 표시
 "SHAKESHACK BURGER 에 오신걸 환영합니다."
아래 상품메뉴판을 보시고 상품을 골라 입력해주세요.

[ Burgers MENU ]
1. ShackBurger   | W 6.9 | 토마토, 양상추, 쉑소스가 토핑된 치즈버거
2. SmokeShack    | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
3. Shroom Burger | W 9.4 | 몬스터 치즈와 체다 치즈로 속을 채운 베지테리안 버거
3. Cheeseburger  | W 6.9 | 포테이토 번과 비프패티, 치즈가 토핑된 치즈버거
4. Hamburger     | W 5.4 | 비프패티를 기반으로 야채가 들어간 기본버거

3. 구매 화면

  • 상품 선택 시 해당 상품을 장바구니에 추가할지 확인하는 문구가 출력
  • 1. 확인 입력 시 장바구니에 추가되었다는 안내 문구와 함께 메인 메뉴로 다시 출력
"Hamburger     | W 5.4 | 비프패티를 기반으로 야채가 들어간 기본버거"
위 메뉴를 장바구니에 추가하시겠습니까?
1. 확인        2. 취소
Hamburger 가 장바구니에 추가되었습니다.

"SHAKESHACK BURGER 에 오신걸 환영합니다."
아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.

[ SHAKESHACK MENU ]
1. Burgers         | 앵거스 비프 통살을 다져만든 버거
2. Forzen Custard  | 매장에서 신선하게 만드는 아이스크림
3. Drinks          | 매장에서 직접 만드는 음료
4. Beer            | 뉴욕 브루클린 브루어리에서 양조한 맥주

[ ORDER MENU ]
5. Order       | 장바구니를 확인 후 주문합니다.
6. Cancel      | 진행중인 주문을 취소합니다.

4. 주문 화면

  • 5.Order 입력 시 장바구니 목록을 출력
  • 장바구니에서는 추가된 메뉴들과 총 가격의 합을 출력
  • 1.주문 입력 시 주문완료 화면으로 넘어가고, 2.메뉴판 입력 시 다시 메인 메뉴로 돌아옴
아래와 같이 주문 하시겠습니까?

[ Orders ]
ShackBurger   | W 6.9 | 토마토, 양상추, 쉑소스가 토핑된 치즈버거
SmokeShack    | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거

[ Total ]
W 15.8

1. 주문      2. 메뉴판

5. 주문완료 화면

  • 1.주문 입력 시 대기번호를 발급
  • 장바구니는 초기화되고 3초 후에 메인 메뉴판으로 돌아감
주문이 완료되었습니다!

대기번호는 [ 1 ] 번 입니다.
(3초후 메뉴판으로 돌아갑니다.)
"SHAKESHACK BURGER 에 오신걸 환영합니다."
아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.

[ SHAKESHACK MENU ]
1. Burgers         | 앵거스 비프 통살을 다져만든 버거
2. Forzen Custard  | 매장에서 신선하게 만드는 아이스크림
3. Drinks          | 매장에서 직접 만드는 음료
4. Beer            | 뉴욕 브루클린 브루어리에서 양조한 맥주

[ ORDER MENU ]
5. Order       | 장바구니를 확인 후 주문합니다.
6. Cancel      | 진행중인 주문을 취소합니다.

6. 주문 취소 화면

  • 메뉴판에서 6.Cancel 입력시 주문을 취소할지 확인을 요청하는 문구가 출력
  • 1.확인 을 입력하면 장바구니는 초기화되고 취소 완료 문구와 함께 메뉴판이 출력
진행하던 주문을 취소하시겠습니까?
1. 확인        2. 취소
진행하던 주문이 취소되었습니다.

"SHAKESHACK BURGER 에 오신걸 환영합니다."
아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.

[ SHAKESHACK MENU ]
1. Burgers         | 앵거스 비프 통살을 다져만든 버거
2. Forzen Custard  | 매장에서 신선하게 만드는 아이스크림
3. Drinks          | 매장에서 직접 만드는 음료
4. Beer            | 뉴욕 브루클린 브루어리에서 양조한 맥주

[ ORDER MENU ]
5. Order       | 장바구니를 확인 후 주문합니다.
6. Cancel      | 진행중인 주문을 취소합니다.

 

선택 요구사항

1. 주문 개수 기능 추가

  • 장바구니에 똑같은 상품이 담기면 주문 화면에서 상품 개수가 출력
아래와 같이 주문 하시겠습니까?

[ Orders ]
ShackBurger   | W 6.9 | 2개 | 토마토, 양상추, 쉑소스가 토핑된 치즈버거
SmokeShack    | W 8.9 | 1개 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거

[ Total ]
W 15.8

1. 주문      2. 메뉴판

2. 상품 옵션 기능 추가

  • 상품에 옵션을 선택 후 장바구니에 추가 할 수 있게 세분화
"Hamburger     | W 5.4 | 비프패티를 기반으로 야채가 들어간 기본버거"
위 메뉴의 어떤 옵션으로 추가하시겠습니까?
1. Single(W 5.4)        2. Double(W 9.0)
"Hamburger(Double) | W 9.0 | 비프패티를 기반으로 야채가 들어간 기본버거"
위 메뉴를 장바구니에 추가하시겠습니까?
1. 확인        2. 취소

3. 총 판매금액 조회 기능 추가

  • 구매가 완료될 때마다 총 판매 금액을 누적
  • 숨겨진 기능으로 0번 입력 시 총 판매금액을 출력
[ 총 판매금액 현황 ]
현재까지 총 판매된 금액은 [ W 102.4 ] 입니다.

1. 돌아가기

4. 총 판매상품 목록 조회 기능 추가

  • 구매가 완료될 때마다 판매 상품 목록을 저장
  • 숨겨진 기능으로 0번 입력 시 총 판매 상품 목록을 출력
[ 총 판매상품 목록 현황 ]
현재까지 총 판매된 상품 목록은 아래와 같습니다.

- ShackBurger    | W 6.9
- Float          | W 2.9
- SmokeShack     | W 8.9
- Shroom Burger  | W 9.4
- Fountain Sodar | W 2.7
- Cheeseburger   | W 6.9
- SmokeShack     | W 8.9
- Shroom Burger  | W 9.4
- Cheeseburger   | W 6.9

1. 돌아가기

MainMenu 클래스( 메뉴 ) 설계

Main 과 MainMenu( 메뉴 ), Goods( 상품 ), Order( 주문 ) 클래스를 각각 생성한 후, 메인 클래스에서는 나머지 클래스의 인스턴스를 생성하여 메소드를 호출해 메뉴판 프로그램을 실행시키기로 하였다.

먼저 메인 메뉴판 출력을 위해 메뉴 클래스에 mainMenu Map 을 생성 후 생성자에서 세팅해주었다. Map 은 put 순서와 관계 없이 출력되지만, 원하는 순서대로 출력되도록 하기 위해 LinkedHashMap을 사용했다.

public class MainMenu {
    Map<String, String> mainMenu = new LinkedHashMap<>(); // 메인메뉴 저장 컬렉션, LinkedHashMap : 데이터를 추가한 순서 대로 출력됨
    Scanner scan = new Scanner(System.in);
    private String name = ""; // 메뉴 이름

    MainMenu() {
        // 메인메뉴 세팅
        mainMenu.put("Burgers", "앵거스 비프 통살을 다져만든 버거");
        mainMenu.put("Forzen Custard", "매장에서 신선하게 만드는 아이스크림");
        mainMenu.put("Drinks", "매장에서 직접 만드는 음료");
        mainMenu.put("Beer", "뉴욕 브루클린 브루어리에서 양조한 맥주");
    }
}

printMainMenu() 메소드는 메뉴를 출력해준다. 람다식을 이용해 mainMenu 맵의 key와 value 값을 출력하도록 했는데, 멘 앞에 메인 메뉴 번호도 함께 나오도록 하고 싶었다. 그러나 람다 식 내부에 사용된 변수는 최종적이어야 하기때문에 원시 정수 변수는 쓸 수 없었다.

따라서 import로 AtomicInteger 클래스(int 자료형을 갖고 있는 wrapping 클래스, 원자성을 보장)와 getAndIncrement() 메소드를 가져와 인덱스 번호를 출력했다.

import java.util.concurrent.atomic.AtomicInteger;

void printMainMenu() {
        System.out.println("SHAKESHACK BURGER 에 오신걸 환영합니다.\n아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.\n");
        System.out.println("[ SHAKESHACK MENU ]");

        AtomicInteger indexHolder = new AtomicInteger(); // 인덱스 출력 하기 위함
        mainMenu.forEach((key, value) -> {
            System.out.println(indexHolder.getAndIncrement() + 1 + ". " + key + "\t| " + value);
        });

        System.out.println("\n[ ORDER MENU ]");
        System.out.println("5. Order       | 장바구니를 확인 후 주문합니다.");
        System.out.println("6. Cancel      | 진행중인 주문을 취소합니다.\n");
}

 

메인 메뉴를 선택하는 chooseMenu() 메소드에서 사용자가 입력한 메뉴 번호/이름을  mainMenu의 키 값과 비교하여 해당되는 키 값을 반환해주도록 하였다. 그리고 setName() 메소드 내에서 chooseMenu() 메소드를 실행하고 그 반환값을 name 변수에 저장했다. name 변수는 getter를 이용해 다른 클래스에서 값을 받을 수 있도록 하였다.

chooseMenu() 메소드는 내부 클래스에서만 사용하기 때문에 접근자를 private으로 설정했다.

// 메인메뉴 선택 메소드
    private String chooseMenu() {
        while (true) {
            System.out.print("메뉴 선택 : ");
            String choose = scan.nextLine();
            System.out.println();

            // 메뉴명으로 입력한 경우
            for (String menu : mainMenu.keySet()) {
                if (choose.equals(menu)) {
                    //description = (String) mainMenu.get(menu); // 메뉴명(키값)에 매치 되는 상품 설명(value 값) 저장
                    return choose;
                }
            }

            // 숫자로 입력 하거나 주문 옵션을 선택할 경우
            switch (choose) {
                case "0": // 숨겨진 기능 : 총 판매금액 조회, 총 판매상품 목록 현황
                    choose = "Total";
                    return choose;
                case "000": // 숨겨진 기능 : 프로그램 종료
                    choose = "000";
                    return choose;
                case "1":
                    choose = "Burgers";
                    //description = (String) mainMenu.get(choose);
                    return choose;
                // case 2 ~ 4 : 다양한 메인 메뉴
                case "5": // 장바구니 목록 주문
                    choose = "Order";
                case "Order":
                    return choose;
                case "6": // 주문 취소 후 장바구니 비우기
                    choose = "Cancel";
                case "Cancel":
                    return choose;
                default:
                    System.out.println("잘못된 입력입니다. 메뉴 번호나 이름을 입력해주세요.\n");
            }
        }
    }

Goods 클래스( 상품 ) 설계

메인 클래스는 첫 실행시  MainMenu 클래스메뉴 ) 인스턴스를 생성 후 '사용자가 선택한 메뉴명'을 받아온다. 그리고 switch 문에서 선택한 메뉴에 따라 기능을 수행한다.

주문이나 숨겨진 기능을 제외한 정상적인 메뉴를 받아왔을 경우, default 항목에서 상품 클래스 인스턴스를 생성하고 매개변수로 '선택한 메뉴명(mainMenuName)'을 넘겨준다.

 

상품 클래스는 메뉴 클래스를 상속받으며, 메뉴 클래스에서 선택했던 메인 메뉴에 따라 해당하는 상품을 출력하고 선택하도록 하는 클래스이다. 상품 목록을 출력하는 printGoods() 메소드와 사용자가 상품을 선택하도록 하는 chooseGoods() 메소드가 있다.

printGoods() 메소드는 메뉴 클래스의 printMainMenu()와 거의 동일하지만, 내부에서 setGoodsMap() 메소드를 실행하여 넘겨받았던 mainMenuName 값에 따라 goodsMenu 맵에 해당하는 상품 목록을 세팅한다.

이때 맵의 value 타입을 String[]로 설정하여 value 자리에 여러개의 String값이 들어갈 수 있도록 만들었다.

public class Goods extends MainMenu {
    Map<Integer, String[]> goodsMenu = new LinkedHashMap<>(); // 상품 목록 저장 컬렉션
    
	// 선택한 메인메뉴에 따라 상품 목록 세팅
    private void setGoodsMap() {
        switch (gName) {
            case "Burgers":
                goodsMenu.put(1, new String[]{"ShackBurger", "6.9", "토마토, 양상추, 쉑소스가 토핑된 치즈버거"});
                goodsMenu.put(2, new String[]{"SmokeShack", "8.9", "베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"});
                goodsMenu.put(3, new String[]{"Shroom Burger", "9.4", "몬스터 치즈와 체다 치즈로 속을 채운 베지테리안 버거"});
                goodsMenu.put(4, new String[]{"Cheeseburger", "6.9", "포테이토 번과 비프패티, 치즈가 토핑된 치즈버거"});
                goodsMenu.put(5, new String[]{"Hamburger", "5.4", "비프패티를 기반으로 야채가 들어간 기본버거"});
                break;
            case "Forzen Custard":
            	// ...
        }
    }
}

메뉴 클래스와 동일하게 상품을 선택하는 chooseGoods() 메소드에서 사용자가 입력한 상품 번호를  goodsMenu 의 키 값과 비교하여 해당되는 키 값을 반환해주도록 하였다. 그리고 setGName() 메소드 내에서 chooseGoods () 메소드를 실행하고 그 반환값을 gName 변수에 저장했다. gName 변수는 getter를 이용해 다른 클래스에서 값을 받을 수 있도록 하였다.

chooseGoods() 메소드는 내부 클래스에서만 사용하기 때문에 접근자를 private으로 설정했다.

마지막으로 선택한 상품을 장바구니에 추가할 것인지 체크하는 check() 메소드를 통해 추가 여부를 체크하도록 했다.

 

[▼ 메인 클래스]

// public class Main { public static void main(String[] args)

default: // 상품 추가 선택
    Goods goods = new Goods(mainMenuName); // 상품 클래스
    goods.printGoods(); // 상품 목록 출력
    goods.setGName(); // 상품 선택

    goodsName = goods.getGName();
    goodsDct = goods.getGDescription();
    goodsPrice = goods.getPrice();

    // 장바구니에 상품 추가 여부 확인
    int check2 = goods.check(goodsName, goodsPrice, goodsDct); // 선택한 상품을 장바구니에 추가할 것인지 체크
    if (check2 == 1) { // 추가 동의
        System.out.println(mainMenuName + " 가 장바구니에 추가되었습니다.\n");
        order.addCart(goodsName, goodsPrice, goodsDct); // 선택한 상품을 장바구니에 추가
    } else { // 추가 취소
        System.out.println();
        // 메인메뉴 선택부터 재시작
    }

[▼ 상품 클래스]

// 선택한 상품을 장바구니에 추가할 것인지 체크
    int check(String n, Double p, String d) {
        int ch;
        System.out.println(n + "\t| W " + p + " | " + d);
        System.out.println("위 메뉴를 장바구니에 추가하시겠습니까?\n1. 확인\t2. 취소");
        while (true) {
            ch = scan.nextInt();
            if (ch == 1 || ch == 2) {
                break;
            } else {
                System.out.println("'1' 또는 '2'를 입력해주세요.");
            }
        }
        return ch;
    }

 

상품 옵션을 출력하고 선택하는 추가 기능이 있었는데, chooseGoods() 메소드 내에서 해당 상품에 옵션이 있다면 printDetail() 메소드로 출력해주고, chooseDetail() 메소드로 선택할 수 있도록 구현했다.

Map<Integer, String[]> detailMenu = new LinkedHashMap<>(); // 상품 옵션 저장 컬렉션

// 상품 옵션 선택 메소드
    private String chooseDetail(String n) { // 매개변수 : 선택한 상품명
        while (true) {
            System.out.print("옵션 선택 : ");
            int choose = scan.nextInt();

            for (int menu : detailMenu.keySet()) {
                if (choose == menu) {  // 선택한 옵션 번호가 키 값과 동일할 경우
                    n = n + "(" + (String) detailMenu.get(menu)[0] + ")";  // 옵션 번호(키값)에 매치 되는 상품 옵션(value[0] 값)을 상품명(n)에 더하기 연산
                    price = Double.parseDouble(detailMenu.get(menu)[1]); // 옵션 가격(value[1] 값)
                    return n;
                }
            }
            System.out.println("올바른 상품 옵션 번호를 입력해주세요.\n");
        }
    }

Order 클래스( 주문 ) 설계

주문 클래스는 상품 클래스에서 장바구니에 추가하기로 check한 상품들을 장바구니에 넣어 출력하거나 주문을 완료하도록 하는 클래스이다. 선택한 상품을 장바구니에 추가하기로 결정했을 때 실행되는 addCart() 메소드와 장바구니 목록 확인 후 상품 주문 여부를 선택하는 orderCart() 메소드, 장바구니를 비워주는 clearCart() 메소드가 있다.

public class Order {
    Map<String, String[]> orderList = new LinkedHashMap<>(); // 장바구니 상품 목록 저장 컬렉션
    
    // 선택한 상품을 장바구니에 추가
    void addCart(String cName, double cPrice, String cDescription) { // 매개변수 : 상품명, 상품 가격, 상품 설명
        totalPrice += cPrice; // 장바구니에 담긴 상품 총 금액 + 선택한 상품 가격

        String num; // 상품명 별 장바구니에 담겨 있는 개수
        if (orderList.containsKey(cName)) { // orderList에 cName과 동일한 키값이 있다면
            num = Integer.toString((Integer.valueOf(orderList.get(cName)[1]) + 1)); // 해당 키값의 value[1] 값인 num++
        } else { // 새로운 cName(키값)이 들어오면 num = 1
            num = "1";
        }
        orderList.put(cName, new String[]{String.valueOf(cPrice), num, cDescription}); // num으로 주문한 상품 개수를 명시해서 orderList 세팅
    }
}

처음에 addCart()를 통해 장바구니에 상품이 제대로 저장되지 않는 문제가 있었다. 이는 장바구니 목록 저장용으로 맵을 썼기 때문이었는데, 같은 상품을 여러개 목록에 넣을 때 키 값(상품명)이 겹쳐서 생기는 문제였다.

ArrayList로 수정할까 했지만, 선택 요구사항에 장바구니에 똑같은 상품이 담기면 주문 화면에서 상품 개수가 출력되도록 하는 주문  개수 추가 기능이 있는 것을 보고 키가 겹칠 경우 value값이 마지막에 들어온 값으로 변경되는 Map의 특성을 이용하기로 했다.

새로운 키 값이 들어오면 num 변수에 "1"을 대입하고 orderList 맵에 상품명, 가격, 설명과 함께 num을 put한 후, 이후 동일한 키 값이 들어온다면 기존 num값에 1을 더하여 다시 put하도록 하였다. 이렇게 하면 맵을 출력할 때 num 값으로 상품 개수를 보여줄 수 있다.

 

orderCart() 메소드에서는 메소드를 실행하자마자 orderList.isEmpty() 여부를 확인하여 장바구니가 비어있다면 주문을 하지 못하도록 했다. 장바구니가 비어있지 않다면 장바구니에 있는 상품 목록과 상품의 총 금액인 totalPrice를 출력한 후, 사용자가 장바구니의 상품 목록을 주문하기로 선택한다면 메인 클래스로 1을 반환한다.

1을 반환받은 메인 클래스는 주문 클래스의 clearCart() 메소드를 통해 .clear()로 장바구니 맵을 비우고, 대기 번호와 3초 후 메뉴판으로 돌아간다는 메세지를 출력한다. 쓰레드를 이용해 for 문의 i가 하나씩 줄어들 때마다 1초의 텀이 생기도록 했다.

public class Main {
    public static void main(String[] args) {
    	// 코드 생략
                        System.out.println("주문이 완료되었습니다!\n");
                        order.clearCart(); // 장바구니 비우기

                        ++turn;
                        System.out.println("대기번호는 [ " + turn + " ] 번 입니다.");
                        for (int i = 3; i > 0; i--) {
                            System.out.println(i + "초후 메뉴판으로 돌아갑니다.");
                            try {
                                Thread.sleep(1000); // Sleep for 1 second
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
    }
}

마지막으로 주문 클래스에서 장바구니의 주문이 완료될 때, 추가 기능으로 다음 코드를 실행하여 지금까지의 총 판매 금액과 총 판매상품 목록이 각각의 변수에 저장되고, 메인 클래스에서 출력될 수 있도록 했다.

 

[▼ 주문 클래스]

ArrayList<String[]> totalOrder = new ArrayList<String[]>(); // 총 판매 상품 목록 저장 컬렉션
private double salesAmount = 0.0; // 지금까지의 총 판매 금액

// 장바구니 주문 완료 시
salesAmount += totalPrice; // 지금까지의 총 판매 금액에 장바구니에 담긴 상품 총 금액 더하기
addTotalOrder(); // 총 판매상품 목록 totalOrder에 주문한 상품을 추가

// 총 판매상품 목록에 주문한 상품을 추가
    private void addTotalOrder() {
        orderList.forEach((key, value) -> {
            int count = Integer.parseInt(value[1]);
            for (int i = 0; i < count; i++) { // 장바구니의 상품 개수가 1개 이상이면 개수만큼 totalOrder에 추가
                totalOrder.add(new String[]{key, value[0]});
            }
        });
    }

[▼ 메인 클래스]

// 메인 클래스
System.out.println("[ 총 판매금액 현황 ]\n현재까지 총 판매된 금액은 [ W " + Math.round(order.getSalesAmount() * 10) / 10.0 + " ] 입니다.\n");
System.out.println("[ 총 판매상품 목록 현황 ]\n현재까지 총 판매된 상품 목록은 아래와 같습니다.\n");
if (order.totalOrder.isEmpty()) { // 총 판매상품 목록이 비었다면
	System.out.println("목록이 비었습니다.\n");
} else {
	order.printTotalOrder(); // 총 판매상품 목록 출력
}

'TIL' 카테고리의 다른 글

Patch & Put 차이점  (2) 2023.12.07
통합 테스트, 단위 테스트  (2) 2023.12.04
환경변수 Path 복구  (0) 2023.10.12
팀 소개 페이지 미니 프로젝트(2)  (0) 2023.10.10
팀 소개 페이지 미니 프로젝트(1)  (0) 2023.10.10