딸기말차

[Java] 12. Mini Project_1 본문

Bootcamp/Java

[Java] 12. Mini Project_1

딸기말차 2023. 7. 9. 01:36

엔코아 플레이데이터(Encore Playdata) Backend 2기 백엔드 개발 부트캠프 (playdata.io)

 

백엔드 개발 부트캠프

백엔드 기초부터 배포까지! 매력있는 백엔드 개발자 포트폴리오를 완성하여 취업하세요.

playdata.io


1. 기획

여태 배운 내용을 토대로, 첫 프로젝트를 진행하게 되었다. 해당 프로젝트에 강사님께서 거신 조건은 다음과 같다.
1. DB를 연결해 사용할 것
2. Collections, Thread를 사용할 것
3. extends / implements / abstract 중 한가지 이상을 사용할 것
4. 구성한 기능을 FileWriter를 사용해 HTML 파일로 내보낼 것
 
개발자란, 실생활에서 일어나거나 생기는 문제들을 프로그래밍을 통해 해결하는 사람들이다.
때문에 해당 조건들을 듣고 프로젝트를 구상하기에 앞서, 실생활에 필요할 법한 기능을 가진 프로그램을 만들어보면 좋을 것 같다는 생각을 가지고 기획을 시작하게 되었다.
 
우리는 현재 카페에서 굉장히 많은 소비를 하고있다. 필자도 아침마다 스타벅스에서 아메리카노 한잔은 꼭 마시는 편이다. 이런 상황에 조금이라도 소비를 아낄 수 있다면 어떨까? 라는 생각을 하게 되었다.
만약 생소한 지역에가서 커피를 마시려는데, 여러 카페들 중 가장 싼 곳을 고를 수만 있다면 평소의 소비를 조금이라도 줄일 수 있을 것이다. 때문에 먹고 싶은 커피 종류를 고르면, 근처 가게 중 가장 저렴한 가게를 골라 결제할 수 있는 시스템을 만들어 보게 되었다.


2.  기능 설계

기획을 마치고, 해당 프로그램에 어떤 기능이 있으면 좋을까? 를 주제로 논의를 하였다.
다양한 아이디어가 오고 갔지만, 첫 프로젝트이고 아직 Frontend 에 대해 수업을 하지도, 클라이언트-서버 간 통신을 배운 것도 아니었다. 때문에 가능한 담백하게 기능을 구성하기로 하였고, 구현할 기능은 다음과 같다.
1. 원하는 메뉴를 입력받는다.
2. 해당 메뉴가 가장 싼 가게를 찾아 알려준다.
3. 해당 메뉴의 쿠폰을 가지고 있는지 확인하고, 쿠폰이 있다면 사용여부를 결정한다.
4. 쿠폰을 사용한다면 10% 할인 된 가격으로 결제를 진행한다.
5. 결제 내역을 화면에 뿌려준다.
6. 최종 결제 여부를 확인한다.
 
해당 기능들을 위해 필요한 것이 무엇이 있을까? 우선 기능을 구현하기 이전에 정보들을 저장할 DB table을 구상하였다.
1. 가게이름, 메뉴이름, 가격, 쿠폰유무를 저장하는 cafe table
2. 가게이름, 메뉴이름을 저장하는 coupon table
3. 가게이름, 메뉴이름, 가격, 결제시간을 저장하는 cafepayment table
 
구현한 DB를 사용해, 프로그램 실행 시 DB에서 데이터들을 select해 가져와 저장한다. 그리고 Scanner을 통해 1번 기능을 실행, 메뉴를 입력받는다.
 
여기까지 진행되었다면, 입력받은 메뉴를 사용해 기능들을 구현할 메서드가 필요할 것이다.
1. 메뉴 이름을 통해 가장 싼 가게를 찾아주는 메서드
2. 해당 가게 메뉴의 쿠폰을 가지고 있는지 확인하는 메서드
3. 쿠폰 사용을 결정하는 메서드
4. 결제 내역을 보여주기 위해 카페 데이터를 가져오는 메서드
5. 최종 결제 여부를 확인하는 메서드
 
이렇게 큰 틀에서 구현을 위해 필요한 재료들을 정리한 후, 각 재료들을 어디에 담아두면 좋을지에 대한 구조 설계를 시작하였다.


3.  구조 설계

하나의 프로젝트를 만들 때 마구잡이로 만드는 것이 아니라, 구현할 클래스나 인터페이스들을 각 용도에 맞는 패키지에 분리해 둔다면, 이 후 유지보수 작업을 진행하기가 굉장히 수월할 것이다.
때문에 기능을 위한 재료들을 어떻게 분리해둘지 많은 고민을 했고, 다음과 같은 구조로 결정하게 되었다.
 

프로젝트 구조

1. db 패키지
DB를 연결하기 위해 필요한 MySQLConnector 클래스와, 해당 DB에 접근해 사용할 query를 담은 메서드들을 구현한 MySQLQuery 클래스, DB 접근 시 사용한 객체들을 close하기 위한 추상 클래스인 Close를 담아두었다.
 
2. domain 패키지
DB에서 가져온 데이터의 레코드와 일치하는 Cafe & Coupon 클래스와, 가져온 데이터들을 이용할 메서드들을 담은 CafeRepository 인터페이스, 해당 인터페이스의 구현체인 CafeImpl 클래스를 담아두었다.
 
3. service 패키지
위의 기능 설계에서 정한 메서드들을 담은 CafeServiceRepository 인터페이스, 해당 인터페이스의 구현체인 CafeServiceImpl 클래스를 담아두었다.
 
4. util 패키지
현재 시간을 구하는 CurrentDateTime 클래스를 담아두었다.
 
5. view 패키지
화면을 구성할 html 태그들을 만드는 CreateHtmlTag 클래스와, 만든 태그들을 실질적으로 HTML File로 만들어 File Write를 하기 위한 CreateHtmlFile 클래스를 담아두었다.


4.  db Package

db package는 db에 접속하기 위한 정보 및 query들을 가지고 있는 패키지이다. 현재 프로젝트에선 spring 같은 프레임워크를 사용하지 않기 때문에 이를 만들었지만, 추후 프레임워크를 사용하면 db 연결 정보는 설정파일에서 해결하고 query는 JPA 등을 사용하기 때문에 나중엔 사용하지 않을 패키지이다.
 
1. MySQLConnector Class
- DB 접속 정보들
- 접속을 시행하기 위한 connectMySql()
 
이 때 접속을 위한 정보들은 누군가 쉽게 접근하면 안되고, fix된 정보이기 때문에 private final을 사용하여 선언하였다.

public class MySQLConnector {
    private final String dbName="db_cafe";
    private final String driver = "com.mysql.cj.jdbc.Driver";
    private final String url = "jdbc:mysql://localhost:3306/" + dbName + "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
    private final String id_mysql = "db id";
    private final String pw_mysql = "db pw"; 

    public MySQLConnector() {
    }

    public Connection connectMySQL() {
        Connection conn = null;
        try {
            Class.forName(driver);
            conn = DriverManager.getConnection(url, id_mysql, pw_mysql);
            System.out.println("MySQL 접속 성공");
        } catch (ClassNotFoundException e) {
            System.out.println("Class.forName(driver) ERR: " + e.getMessage());
        } catch (SQLException e) {
            System.out.println("getConnection() ERR: " + e.getMessage());
        }
        return conn;
    }
}

 
2. MySQLQuery Class
1) cafe table의 모든 데이터를 가져오기 위한 selectAll()
2) cafe table에서 한 레코드만 뽑아 가져오기 위한 selectOneCafe()
3) coupon table의 모든 데이터를 가져오기 위한 selectAllCoupon()
4) cafe table의 쿠폰 보유여부를 update하기 위한 updateCafeByCoupon()
5) 쿠폰을 사용 후 cafe table의 쿠폰 보유여부를 update하기 위한 updateCafeUseCoupon()
6) coupon table에서 사용한 쿠폰을 제거하기 위한 deleteUseCoupon()
7) 결제 완료 후 결제 정보는 카페도 알고 있어야 하기 때문에, cafepayment table에 insert 하기 위한 insertCafePayment()
 
이 때 해당 메서드들을 실행하려면 DB에 연결하기 위한 Connector가 필요하기 때문에 내부에 선언하였고, 해당 부분은 외부에서 접근할 필요가 없고 변치않는 내용이기 때문에 private final로 선언하였다.

public class MySQLQuery extends Close {

    private final MySQLConnector mySQLConnector = new MySQLConnector();
    private final Connection conn = mySQLConnector.connectMySQL();

    public MySQLQuery() {
    }

    public Map<String, ArrayList<Cafe>> selectAllCafe() {
        Map<String, ArrayList<Cafe>> cafes = new HashMap<>();
        Statement stmt = null;
        ResultSet rs = null;

        try {
            stmt = conn.createStatement();
            rs = stmt.executeQuery("select * from cafe");
            while(rs.next()) {
                Cafe cafeData = new Cafe(rs.getString("store"), rs.getString("menu"), rs.getInt("price"), rs.getInt("isCoupon"));
                if (!cafes.containsKey(rs.getString("store")))
                    cafes.computeIfAbsent(rs.getString("store"), k -> new ArrayList<>()).add(cafeData);
                else
                    cafes.computeIfPresent(rs.getString("store"), (k, v) -> v).add(cafeData);
            }
        } catch (SQLException e) {
            System.out.println("selectAllCafe ERR: " + e.getMessage());
        } finally {
            this.close(stmt, rs);
        }
        return cafes;
    }

    public Cafe selectOneCafe(String store, String menu) {
        Cafe cafe = new Cafe();
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            pstmt = conn.prepareStatement("select * from cafe where store = ? and menu = ?");
            pstmt.setString(1, store);
            pstmt.setString(2, menu);
            rs = pstmt.executeQuery();
            while (rs.next())
                cafe = new Cafe(rs.getString("store"), rs.getString("menu"), rs.getInt("price"), rs.getInt("isCoupon"));
        } catch (SQLException e) {
            System.out.println("selectOneCafe ERR: " + e.getMessage());
        } finally {
            this.close(pstmt, rs);
        }
        return cafe;
    }

    public ArrayList<Coupon> selectAllCoupon() {
        ArrayList<Coupon> coupons = new ArrayList<>();
        Statement stmt = null;
        ResultSet rs = null;
        try {
            stmt = conn.createStatement();
            rs = stmt.executeQuery("select * from coupon");
            while (rs.next())
                coupons.add(new Coupon(rs.getString("store"), rs.getString("menu")));
        } catch (SQLException e) {
            System.out.println("selectAllCoupon ERR: " + e.getMessage());
        } finally {
            this.close(stmt, rs);
        }
        return coupons;
    }

    public void updateCafeByCoupon(ArrayList<Coupon> coupons) {
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("update cafe set isCoupon = ? where store = ? and menu = ?");
            for (Coupon c : coupons) {
                pstmt.setInt(1, 1);
                pstmt.setString(2, c.getStore());
                pstmt.setString(3, c.getMenu());
                pstmt.executeUpdate();
            }
        } catch (SQLException e) {
            System.out.println("updateCafeByCoupon ERR: " + e.getMessage());
        } finally {
            this.close(pstmt);
        }
    }

    public void updateCafeUseCoupon(boolean couponUse, String store, String menu) {
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("update cafe set isCoupon = ? where store = ? and menu = ?");
            pstmt.setInt(1, couponUse ? 0 : 1); // 추후 front에서 예 / 아니오를 눌렀을 때 값을 true / false로 받아 처리하기 위해
            pstmt.setString(2, store);
            pstmt.setString(3, menu);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            System.out.println("updateCafeUseCoupon ERR: " + e.getMessage());
        } finally {
            this.close(pstmt);
        }
    }

    public void deleteUseCoupon(String store, String menu) {
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("delete from coupon where store = ? and menu = ?");
            pstmt.setString(1, store);
            pstmt.setString(2, menu);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            System.out.println("deleteUseCoupon ERR: " + e.getMessage());
        } finally {
            this.close(pstmt);
        }
    }

    public void insertCafePayment(Cafe cafe) {
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("insert into cafepayment values (NULL, ?, ?, ?, ?)");
            pstmt.setString(1, cafe.getStore());
            pstmt.setString(2, cafe.getMenu());
            pstmt.setInt(3, cafe.getPrice());
            pstmt.setString(4, cafe.getTime());
            pstmt.executeUpdate();
        } catch (SQLException e) {
            System.out.println("insertCafePayment ERR: " + e.getMessage());
        } finally {
            this.close(pstmt);
        }
    }
}

 
3. Close abstract class
DB에 연결되어 사용할 Statement, PreparedStatement, ResultSet 객체들은 사용 후 close해 DB 서버에 가해지는 부담을 줄여야한다. 해당 부분은 공통 코드기 때문에, abstract class로 구현 후 상속을 통해 사용하였고, method overroad 기법을 사용해 구현하였다.

public abstract class Close {
    public void close(Statement stmt, ResultSet rs) {
        try {
            rs.close();
            stmt.close();
        } catch (SQLException e) {
            System.out.println("Statement, ResultSet CLOSE ERR : " + e.getMessage());
        }
    }

    public void close(PreparedStatement pstmt, ResultSet rs) {
        try {
            rs.close();
            pstmt.close();
        } catch (SQLException e) {
            System.out.println("PreparedStatement, ResultSet CLOSE ERR : " + e.getMessage());
        }
    }

    public void close(PreparedStatement pstmt) {
        try {
            pstmt.close();
        } catch (SQLException e) {
            System.out.println("PreparedStatement CLOSE ERR : " + e.getMessage());
        }
    }
}

5.  domain Package

domain package는 클라이언트 - 기능 요청 - 기능 실행 - DB로 구성되는 웹 서비스에서 DB와 직접적으로 연결 된, 기능 실행 및 DB 접근을 담당하는 패키지이다. 때문에 DB table의 레코드와 일치하는 클래스가 존재하고, 기능 실행을 통해 DB에 직접 접근해 비즈니스 로직을 실행하는 클래스 또한 존재한다.
하지만 이번 프로젝트 구조를 보면 db라는 패키지가 따로 있다. 왜냐하면 보통 비즈니스 로직을 처리할 때 최근 많은 회사들이 JPA를 사용하는데, JPA는 java에서 직접 query를 사용하는 것이 아니라 해당 query를 실행해주는 java 코드를 사용한다.
때문에 JPA를 사용하고 있다 가정하고 비즈니스 로직을 실행하는 클래스를 구현하였고, 실제 query는 db 패키지의 MySQLQuery class에서 실행하였다.
 
1. Cafe Class, Coupon class
DB table들의 한 레코드와 일치하는 클래스들이다.
해당 데이터들은 마음대로 바꾸면 안되기 때문에 private로 생성하였고, 생성자를 통해 DB에서 데이터를 읽어올 때만 갱신할 수 있게 구현하였다. 동일한 이유로 setter는 구현하지 않았고, 오직 읽기만 가능하도록 getter만 구현하였다.
또한 결제 내역을 구현하는데는 결제 시간이 필요하지만, 굳이 cafe table의 데이터에 시간을 포함할 필요는 없기 때문에 Cafe Class는 util에 있는 CurrentDateTime 클래스를 상속받아 구현하였다. 

public class Cafe extends CurrentDateTime {
    private String store; // 가게이름
    private String menu; // 메뉴이름
    private int price; // 메뉴가격
    private int isCoupon; // 쿠폰 보유여부

    public Cafe() {
    }

    public Cafe(String store, String menu, int price, int isCoupon) {
        this.store = store;
        this.menu = menu;
        this.price = price;
        this.isCoupon = isCoupon;
    }

    public String getStore() {
        return store;
    }

    public String getMenu() {
        return menu;
    }

    public int getPrice() {
        return price;
    }

    public int getCoupon() {
        return isCoupon;
    }
}
public class Coupon {
    private String store; // 가게이름
    private String menu; // 메뉴이름

    public Coupon() {
    }

    public Coupon(String store, String menu) {
        this.store = store;
        this.menu = menu;
    }

    public String getStore() {
        return store;
    }

    public String getMenu() {
        return menu;
    }
}

 
2. CafeRepository
위의 MySQLQuery() 에서 구현한 메서드들을 호출하기 위한 메서드들을 구현해 둔 interface이다.

public interface CafeRepository {
    public void selectAllCafe(); // select All cafe data
    public Cafe selectOneCafe(String store, String menu); // select one cafe data
    public void selectAllCoupon(); // select All coupon data
    public void updateCafeByCoupon(); // use coupon data, update cafe data
    public void updateCafeUseCoupon(boolean couponUse, String store, String menu); // if use coupon, update cafe data
    public void deleteUseCoupon(String store, String menu); // if use coupon, delete from coupon
    public void insertCafePayment(Cafe cafe); // insert payment info in cafepayment table
}

 
3. CafeImpl Class
CafeRepository interface의 구현체이다. 생성자에 selectAllCafe(), selectAllCoupon(), updateCafeByCoupon()을 넣어 해당 클래스의 객체가 생성 될 때 카페, 쿠폰 정보를 읽어와 cafe table의 쿠폰 보유여부를 update할 수 있게 구현하였다.
 
1) public Map<String, ArrayList<Cafe>> cafes = new HashMap<>();
cafe table의 데이터를 가져와 map 형태로 저장, 외부에서 사용할 수 있어야하기 때문에 public
2) private ArrayList<Coupon> coupons = new ArrayList<>();
coupon 정보는 사용유무에 따라 변할 수 있지만 update에만 사용되기 때문에 private
3) private final MySQLQuery mysqlQuery = new MySQLQuery();
객체 생성 후 내용변경 없이 사용만 할 것이기 때문에 private final

public class CafeImpl implements CafeRepository {
    public Map<String, ArrayList<Cafe>> cafes = new HashMap<>(); // classify cafe data
    private ArrayList<Coupon> coupons = new ArrayList<>(); // coupon data
    private final MySQLQuery mySQLQuery = new MySQLQuery(); // query method

    public CafeImpl() {
        this.selectAllCafe();
        this.selectAllCoupon();
        this.updateCafeByCoupon();
    }

    @Override
    public void selectAllCafe() {
        cafes = mySQLQuery.selectAllCafe();
    }

    @Override
    public Cafe selectOneCafe(String store, String menu) {
        return mySQLQuery.selectOneCafe(store, menu);
    }

    @Override
    public void selectAllCoupon() {
        coupons = mySQLQuery.selectAllCoupon();
    }

    @Override
    public void updateCafeByCoupon() {
        mySQLQuery.updateCafeByCoupon(coupons);
    }

    @Override
    public void updateCafeUseCoupon(boolean couponUse, String store, String menu) {
        mySQLQuery.updateCafeUseCoupon(couponUse, store, menu);
    }

    @Override
    public void deleteUseCoupon(String store, String menu) {
        mySQLQuery.deleteUseCoupon(store, menu);
    }

    @Override
    public void insertCafePayment(Cafe cafe) {
        mySQLQuery.insertCafePayment(cafe);
    }
}

6.  service Package

service package는 클라이언트들이 실제로 사용하는 기능들에 관한 패키지이다. 하지만 그렇다고 여기서 비즈니스 로직들을 전부 구현하게 되버리면, 클라이언트 - 기능 요청 - 기능 실행 - DB로 구성되는 웹 서비스에서 요청부분에 기능을 전부 구현하는 것과 마찬가지가 되버린다.
때문에 service 패키지는 해당 기능 실행을 가리키는 이정표 역할을 담당해야 하고, 꼭 필요한게 아니라면 로직을 직접 구현할 필요는 없다고 생각한다.
 
1. CafeServiceRepository
사용자가 실질적으로 사용하는 기능들을 구현한 interface이다.

public interface CafeServiceRepository {
    public String findCheapStore(String menu); // find cheap store from cafe data
    public boolean checkCoupon(String cafe, String menu) throws InterruptedException; // check coupon by input menu
    public void useCoupon(boolean couponUse, String store, String menu); // decision use coupon
    public Cafe paymentInfo(String store, String menu); // return current payment data
    public void choosePayment(Cafe cafe, String input) throws InterruptedException; // choose payment
}

 
2. CafeServiceImpl Class
1) 고른 메뉴가 가장 싼 가게를 찾기 위한 findCheapStore()
2) 고른 메뉴의 쿠폰 보유여부를 확인하기 위한 checkCoupon()
3) 쿠폰을 사용한다면, cafe table과 coupon table의 정보를 바꾸기 위한 useCoupon()
4) 결제 내역을 출력하기 위해 cafe table에서 해당 메뉴의 레코드를 가져오는 paymentInfo()
5) 결제 여부를 확인 후 cafepayment table에 결제 내역을 insert 하기 위한 choosePayment()
 
6) private String CafeImpl cafes = new CafeImpl();
해당 메서드들을 사용하려면 cafe table의 정보를 가지고 있는 객체가 필요하다. 해당 객체는 CafeImpl 내에 구현해 두었고, 때문에 CafeImpl 객체를 생성해 사용하였다. 
이 때 service에선 정보를 가지고 있는 객체에 접근해 호출하거나, 데이터 변경을 요청만하지 실질적으로 변경을 하는 것은 아니기 때문에 private final로 선언하였다.

public class CafeServiceImpl implements CafeServiceRepository {
    private final CafeImpl cafes = new CafeImpl();
    @Override
    public String findCheapStore(String menu) {
        ArrayList<String> stores = new ArrayList<>(cafes.cafes.keySet());
        String cheapStore = "";
        int min = Integer.MAX_VALUE;
        for (String s : stores) {
            for (Cafe c : cafes.cafes.get(s)) {
                if (c.getMenu().equals(menu) && c.getPrice() < min) {
                    cheapStore = s;
                    min = c.getPrice();
                }
            }
        }
        return cheapStore;
    }

    @Override
    public boolean checkCoupon(String store, String menu) throws InterruptedException {
        Thread.sleep(2000);
        for (Cafe c : cafes.cafes.get(store))
            if (c.getMenu().equals(menu) && c.getCoupon() == 1)
                return true;
        return false;
    }

    @Override
    public void useCoupon(boolean couponUse, String store, String menu) {
        cafes.updateCafeUseCoupon(couponUse, store, menu);
        cafes.deleteUseCoupon(store, menu);
    }

    @Override
    public Cafe paymentInfo(String store, String menu) {
        return cafes.selectOneCafe(store, menu);
    }

    @Override
    public void choosePayment(Cafe cafe, String input) throws InterruptedException {
        if (input.equals("예")) {
            Thread.sleep(2000);
            cafes.insertCafePayment(cafe);
            System.out.println("결제가 완료되었습니다.");
        }
        else
            System.out.println("결제가 취소되었습니다.");
    }

    public CafeImpl getCafes() {
        return cafes;
    }
}

7.  util Package

util package는 기능 구현에 필요는 하지만, 비즈니스 로직에는 들어가지 않는 기능들이 포함된다. 예시로 문자열 처리, random 값 생성, 날짜 및 시간 처리 등이 존재한다.
 
1. CurrentDateTime Class
Java의 LocalDateTime 객체를 이용해 현재 시간을 구하는 클래스이다. 현재 시간은 cafe 데이터나 coupon 데이터에 필요하지 않고, 오직 결제 내역에만 들어가는 데이터이다.
때문에 cafe table 이나 coupon table에 하나의 column으로 넣는 것 보단, util 패키지를 따로 만들어 시간을 구하는 기능을 만들어 놓고, 해당 기능을 Cafe Class에 상속해둔 후, 필요할 때만 불러서 사용할 수 있게 만들었다.
 
또한, 구한 시간은 값을 변경할 일이 없기 때문에 private final로 선언하고, getter를 통해 읽기만 가능하게 하였다.

public class CurrentDateTime {
    private final LocalDateTime localDateTime = LocalDateTime.now();
    private final String time = localDateTime.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초"));

    public String getTime() {
        return time;
    }
}

8.  view Package

view package는 말 그대로 화면에 출력하는 것에 관련된  package이다. 보통 html 태그들을 포함하는 파일들이 속한다.
 
1. CreateHtmlTag Class
1) 저장한 카페 정보를 통해 메뉴판을 만들기 위한 createHtml(Map<String, ArrayList<Cafe>> cafes)
2) 가격 가장 싼 카페를 화면에 나타내기 위한 createHtml(String cheapStore, String menu)
3) 쿠폰 사용여부를 확인하기 위한 createHtml(boolean coupon)
4) 결제 내역을 화면에 나타내기 위한 createHtml(Cafe cafe, boolean coupon)
 
이 때 메서드를 호출할 때마다 String 값을 return해 합쳐서 사용하는 것보단, 클래스 내에 StringBuilder를 생성해 해당 메서드들이 호출될 때마다 StringBuilder에 tag들이 추가될 수 있게 구현하였다.
또한, static 영역에 선언하여 굳이 객체를 생성하지 않고 사용할 수있도록 구현하였다.

public class CreateHtmlTag {
    public static StringBuilder tags = new StringBuilder();
    
    public static void createHtml(Map<String, ArrayList<Cafe>> cafes) {
        tags.append("<html>") ;
        tags.append("<head><title>Cafe</title></head>");
        tags.append("<header>");
        tags.append("<h1 style='text-align: center; margin-top: 20px;'>");
        tags.append("카페 별 메뉴판");
        tags.append("</h1>");
        tags.append("</header>");

        tags.append("<body>");
        for(String s : cafes.keySet()) {
            tags.append("<div style='float: left; text-align: center; margin: 20px; border: 1px solid gray;'>");
            tags.append("<div style='margin-top: 5px; margin-bottom: 5px; font-weight: bold; font-size:20px;'>");
            tags.append(s); // 카페 이름 출력
            tags.append("</div>");
            tags.append("<table>");
            int index = 0;
            for (int i = 0; i < cafes.get(s).size(); i++) {
                tags.append("<tr>");
                for (int j = 0 ; j < 4; j++) {
                    if (index >= cafes.get(s).size())
                        break;
                    tags.append("<td>");
                    tags.append("<button style='width:100px; height:80px; margin: 5px;'>");
                    tags.append(cafes.get(s).get(index).getMenu());
                    tags.append("<br><br>");
                    tags.append(cafes.get(s).get(index).getPrice()).append("원");
                    tags.append("</button>");
                    tags.append("</td>");
                    index++;
                }
                tags.append("</tr>");
            }
            tags.append("</table>");
            tags.append("</div>");
        }
    }

    public static void createHtml(String cheapStore, String menu) {
        tags.append("<table>");
        tags.append("<tr>");
        tags.append("<td>");
        tags.append("<span style='margin: 20px; font-size: 20px;'>");
        tags.append("<span style='font-size: 30px; color: red;'>");
        tags.append(menu);
        tags.append("</span>");
        tags.append("가 가장 싼 가게는 ");
        tags.append("<span style='font-size: 30px; color: red;'>");
        tags.append(cheapStore);
        tags.append("</span>");
        tags.append(" 입니다.");
        tags.append("</span>");
        tags.append("<td>");
        tags.append("</tr>");
        tags.append("</table>");
        tags.append("<hr style='margin-top:20px; margin-bottom: 20px;'>");
    }
    
    public static void createHtml(boolean coupon) {
        if (coupon) {
            tags.append("<div style='margin: 20px'>");
            tags.append("<span style='font-size: 20px; margin-right: 10px;'>");
            tags.append("해당 메뉴의 쿠폰을 사용하시겠습니까?");
            tags.append("</span>");
            tags.append("<input type='radio' name='coupon'>");
            tags.append("<span style='margin-right: 10px;'>");
            tags.append("예");
            tags.append("</span>");
            tags.append("<input type='radio' name='coupon'>");
            tags.append("<span>");
            tags.append("아니오");
            tags.append("</span>");
            tags.append("</div>");
            tags.append("<hr style='margin-top:20px; margin-bottom: 20px;'>");
        }
        else {
            tags.append("<div style='margin: 20px'>");
            tags.append("<span style='font-size: 20px;'>");
            tags.append("해당 메뉴의 쿠폰이 없습니다.");
            tags.append("</span>");
            tags.append("</div>");
            tags.append("<hr style='margin-top:20px; margin-bottom: 20px;'>");
        }
    }

    public static void createHtml(Cafe cafe, boolean coupon) {
        tags.append("<div style='float: left; text-align: center; margin: 20px; padding: 10px; border: 1px solid gray;'>");
        tags.append("<span style='font-size: 30px;'>");
        tags.append("주문 내역");
        tags.append("<hr />");
        tags.append("</span>");

        tags.append("<table>");
        tags.append("<tr>");
        tags.append("<td>");
        tags.append("<div style='text-align: left; width:500px; height:500px; margin: 5px;' disabled>");
        tags.append("<span style='font-size: 20px; color: black;'>");
        tags.append("매장명 : ").append(cafe.getStore()).append("<br><br>");
        tags.append("결제한 메뉴 : ").append(cafe.getMenu()).append("<br><br>");
        if (coupon)
            tags.append("결제 금액 : ").append(cafe.getPrice() - (cafe.getPrice()) * 10 / 100).append("원").append("<br><br>");
        else
            tags.append("결제 금액 : ").append(cafe.getPrice()).append("원").append("<br><br>");
        tags.append("결제한 시간 : ").append(cafe.getTime()).append("<br><br>");
        tags.append("</span>");
        tags.append("</div>");
        tags.append("</td>");
        tags.append("</tr>");
        tags.append("</table>");
        tags.append("</div>");
        tags.append("</body>");
        tags.append("</html>");
    }
}

 
2. CreateHtmlFile Class
위의 클래스에서 만든 tags를 파라미터로 받아 FileWriter를 통해 HTML 파일을 생성해주는 클래스이다.

public class CreateHtmlFile {
    public static void filewrite(String tags) {
        FileWriter fr;
        try {
            fr = new FileWriter("C:\\filetest\\pos.html");
            fr.write(tags);
            fr.close();
        } catch (IOException e) {
            System.out.println("IOException: " + e.getMessage());
        }
    }
}

9.  12일차 후기

일주일 정도 전에 미리 프로젝트 공지를 해주셨기 때문에, 남는시간에 조금씩 프로젝트를 진행하였었다.
이 때 가장 시간을 많이 소모한 부분은 기획, 기능 설계, 구조 설계 부분이었다.
 
기획부분에선 처음엔 일주일 간 카페에서 사용할 금액을 정하고 메뉴를 고르는 가계부 같은 것을 만드려 하였지만, 팀원과 의견을 나누다보니 애매한 부분이 생겨 기획 자체를 고치게 되었고, 기능 설계에 있어서도 다양한 기능을 넣어볼까 했으나 시간문제, 혹은 현재 배운 범위를 벗어나는 문제 때문에 만드려던 기능을 수정하거나, 빼는 일이 발생했기 때문이다.
 
그리고 단언컨데 구조 설계 파트가 가장 오랜시간을 잡아먹었다고 생각한다. 추후 유지보수 문제까지 고려하며 설계하려하니, 패키지 구조를 어떻게해야할지, 어떤 패키지에 어떤 기능을 넣어야할지 많은 고민을 했기 때문이다.
하지만 이렇게 설계들을 마무리하고 막상 구현에 들어가니, 이미 구조를 확실히 정해놨기 때문에 구현자체는 금방 마무리 짓고 테스트를 진행 할 수 있었다. 
 
이미 만들어진 프로젝트 위에서 작업하는게 아니라, 실제 상용화 될 프로그램이라 가정하고 프로젝트를 진행하니 설계를 한다는 것이 굉장히 어려운 과정이라는 것을 깨달았다. 하지만 해당 설계를 통해 이전엔 웹 서비스라는 과정이 머릿속에 서로 떨어져서만 존재했지만, 이젠 어떤 구조로 과정이 진행되는지 정리가 되었고, 기존에 알고만 있던 지식들을 정리하는데 정말 큰 도움이 되었던 프로젝트 경험이었다.

'Bootcamp > Java' 카테고리의 다른 글

[Java] 11. CRUD 실습 풀이  (0) 2023.07.06
[Java] 10. CRUD 실습  (0) 2023.07.05
[Java] 9. JDBC, CRUD  (0) 2023.07.04
[Java] 8. Generic, SOLID, Lambda, Singleton, Database  (0) 2023.07.03
[Java] 7. Thread, Collections, 외부 라이브러리  (0) 2023.07.02