오늘은 어제에 이어 페이지 연결과 html css javascript 위주로 진행

 

user가 볼수 있는 main 페이지와 info, cart, order, review의 html 태그와 css 정도는 마무리 하고

main 자바 스크립트나 위치 스타일 마무리
info 페이지도 cs와 js 마무리

main, info는 thymeleaf 받을 태그 정도는 만들어 두고 나중에 controller 쪽 만지면 완료

 

 

오늘 배운것 css에서 gap과 sticky, script에서 DOMContentLoaded, getBoundingClientRect()

 

gap: 10px; 이건 각 항목의 간격을 고르게 주더라고 가운데는 벌어지는데 그걸 일정하게 해줌

 

position: sticky; 이건 해당 영역을 벗어나면 따라오게 해주는 신기한 명령

 

DOMContentLoaded, 페이지에서 무든 이벤트가 있을 때마다 감지 이걸 많이 쓰면 렉걸리려나

 

getBoundingClientRect, 스크롤 감지 관련 문제 때문에 GPT에게 물어가면서 함 GPT 만세

 

오늘 수정한 코드

 

main.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8" />
    <title></title>
    <link href="/css/style.css" rel="stylesheet">
    <link href="/css/user/main.css" rel="stylesheet" type="text/css">
    <script src="/js/user/main.js" type="text/javascript" defer></script>
</head>
<body>
<div class="innerBox">
    <header></header>
    <div class="container">
        <div class="restaurant-image">
            <img src="/images/sample.png" alt="식당 메인 이미지" class="restaurant-img">
        </div>
        <div class="title">
            <div class="restaurant-name">더조은 식당</div>
            <button class="report-button button">
                <i class="fas fa-exclamation-triangle"></i>신고하기
            </button>
        </div>
        <div class="ratings-reviews">
            <div class="ratings">
                <div class="stars">★★★★★</div>  <!-- 5개의 별을 기본으로 설정 -->
                <label>3.3</label> <!-- 예시 평점 -->
            </div>
            <button class="call-button button">호출하기</button>
        </div>
        <div class="buttons">
            <button class="menu-btn button">메뉴</button>
            <button class="announcement-btn button">공지사항</button>
            <button class="reviews-btn button">리뷰</button>
        </div>
        <div class="content">
            <div class="menu-content">
                <div class="category-content">
                    <div th:each="category : ${categories}" class="category" th:text="${category.name}" th:data-target="'menu-category-' + ${category.id}"></div>
                </div>
                <div class="menus-container">
                    <div th:each="category : ${categories}" class="menu-category"  th:id="'menu-category-' + ${category.id}" th:data-category-id="${category.id}" >
                        <div th:text="${category.name}" class="category-name"></div>
                        <div class="menus" th:each="menu : ${menus}">
                            <div th:if="${menu.category.id} == ${category.id}" class="menu" th:onclick="'location.href=\'/user/info?id=' + ${menu.id} + '\''">
                                <div class="image">
                                    <img src="/images/sample.png" alt="메뉴 이미지" class="restaurant-image">
                                </div>
                                <div class="text">
                                    <div th:text="${menu.name}" class="menu-name"></div>
                                    <!--th:text="${menu.info}"-->
                                    <div class="menu-info">맛있음</div>
                                    <div th:text="${menu.price + '원'}" class="menu-price"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="announcement-content">
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항1</div>
                        <div class="date">16:12</div>
                    </div>
                    <div class="content">쉬고싶어서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항2</div>
                        <div class="date">2024.04.03</div>
                    </div>
                    <div class="content">아파서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항3</div>
                        <div class="date">2024.04.02</div>
                    </div>
                    <div class="content">싱숭생숭해서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항4</div>
                        <div class="date">2024.04.01</div>
                    </div>
                    <div class="content">해어져서 쉽니다</div>
                </div>
                <div class="announcement">
                    <div class="title">
                        <div class="text">공지사항5</div>
                        <div class="date">2024.03.20</div>
                    </div>
                    <div class="content">그냥 쉽니다</div>
                </div>
            </div>
            <div class="review-content" >
                <div class="review">
                    <div class="title">박윤재</div>
                    <div class="mid">
                        <!-- 여기에 별점을 표시할 div를 추가합니다. 데이터 속성(data-rating)을 사용하여 별점을 지정합니다. -->
                        <div class="star" data-rating="5"></div>
                        <div class="date">1달전</div>
                    </div>
                    <div class="content">음식이 친절하고 사장님이 맛있어요</div>
                </div>
                <div class="review">
                    <div class="title">손지영</div>
                    <div class="mid">
                        <div class="star" data-rating="4"></div>
                        <div class="date">2024.04.01</div>
                    </div>
                    <div class="content">1주전</div>
                </div>
                <div class="review">
                    <div class="title">윤경재</div>
                    <div class="mid">
                        <div class="star" data-rating="1"></div>
                        <div class="date">2시간전</div>
                    </div>
                    <div class="content">집에 보내주세요</div>
                </div>
            </div>
        </div>
    </div>
    <footer></footer>
    <!-- 신고하기 모달 창 -->
    <div id="reportModal" class="modal report-modal">
        <div class="modal-content">
            <h2 class="modal-title">가게 신고</h2>
            <p class="modal-warning">가게 신고 시 돌이킬 수 없으며,<br>허위로 신고를 작성한 경우<br>신고는 무효화 됩니다.</p>
            <label for="reportReason" class="modal-label">신고 사유</label>
            <textarea id="reportReason" class="modal-input"></textarea>
            <button class="modal-submit button" id="reportCall">신고하기</button>
        </div>
    </div>
    <!-- 호출하기 모달 창 -->
    <div id="callModal" class="modal call-modal">
        <div class="modal-content">
            <h2 class="modal-title">호출하기</h2>
            <form id="callForm">
                <label><input type="radio" name="callOption" value="water" checked/> 물 주세요</label><br />
                <label><input type="radio" name="callOption" value="cup" /> 컵 주세요</label><br />
                <label><input type="radio" name="callOption" value="towel" /> 물수건 주세요</label><br />
                <label><input type="radio" name="callOption" value="staff" /> 그냥 직원오세요</label>
            </form>
            <button class="modal-submit button" id="submitCall">호출하기</button>
        </div>
    </div>
</div>
</body>
</html>

 

main.css

@charset "UTF-8";

/* 메인 이미지 */
.container .restaurant-image{
    width: 100%;
    height: 200px;
    overflow: hidden;
}

.container .restaurant-image img{
    width: 100%;
    height: auto;
    object-fit: cover;
    object-position: center top;
}

/* 타이틀과 신고버튼 */
.container .title{
    width: 100%;
    padding: var(--padding);
    display: flex;
    justify-content: space-between;
    align-items: center;
}

/* 타이틀 */
.container .title .restaurant-name{
    font-size: var(--font-big);
    font-weight: bold;
}

/* 신고 */
.container .title .report-button{
    padding: 0;
    background-color: transparent;
    color: red;
    border: none;
    font-size: var(--font-small);
}

.container .title .report-button .fa-exclamation-triangle{
    margin-right: 5px;
}

/* 별표와 리뷰 버튼*/
.container .ratings-reviews{
    margin-bottom: 10px;
    padding: var(--padding);
    padding-top: 0;
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

/* 별표 부분*/
.container .ratings-reviews .ratings {
    display: flex;
    align-items: center;
}

/* 별표의 별 부분 */
.container .ratings-reviews .ratings .stars{
    position: relative;
    display: inline-block;
    font-size: 20px; /* 별 크기 조정 */
    direction: rtl; /* 별을 오른쪽에서 왼쪽으로 채워나가기 위해 */
}

.container .ratings-reviews .ratings .stars::after{
    content: "★★★★★";
    color: lightgray; /* 기본 별 색상 설정 */
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 0;
 }

.star-fill {
    display: block;
    color: gold; /* 금색 별 색상 설정 */
    position: absolute;
    top: 0;
    left: 0;
    overflow: hidden;
    z-index: 1;
    direction: ltr; /* 금색 별이 왼쪽에서 시작하도록 설정 */
}

/* 별표의 숫자 부분 */
.container .ratings-reviews .ratings label{
    margin-left: 10px;
}

/* 호출 버튼 부분*/
.container .ratings-reviews .call-button{
    border: 0;
    border-radius: 0;
    padding: 10px 30px;
}


/* 메뉴 공지사항 리뷰 */
.container .buttons{
    border-bottom: 3px solid var(--orange);
    display: flex;
    width: 100%;
}

/* 메뉴 */
.container .buttons .menu-btn{
    border: 0;
    border-radius: 0;
    flex: 1;
    font-size: var(--font-content);
    padding: var(--padding);
}

/* 공지사항 */
.container .buttons .announcement-btn{
    background-color: inherit;
    color: var(--gray);
    border: 0;
    border-radius: 0;
    flex: 1;
    font-size: var(--font-content);
    padding: var(--padding);
}

/* 리뷰 */
.container .buttons .reviews-btn{
    background-color: inherit;
    color: var(--gray);
    border: 0;
    border-radius: 0;
    flex: 1;
    font-size: var(--font-content);
    padding: var(--padding);
}

/* 버튼을 눌렀을때 보여주는 콘텐츠 영역 */
.container .content{
    width: 100%;
}

/* 메뉴 콘텐츠 */
.container .content .menu-content{
    width: 100%;
}

/* 카테고리 슬라이더 부분 */
.container .content .menu-content .category-content{
    width: 100%;
    display: flex; /* 이 부분을 추가합니다 */
    overflow-x: auto; /* 수평 스크롤을 위해 추가합니다 */
    background-color: var(--white);
    scrollbar-width: none;
    position: sticky;
    top: 0; /* 상단에 고정 */
    z-index: 1; /* 다른 요소들 위에 오도록 z-index 설정 */
}

.container .content .menu-content .category-content .category{
    margin: 10px;
    padding: 10px 20px;
    flex: 0 0 auto; /* flex 항목이 자신의 크기를 유지하도록 설정합니다 */
    border-radius: 20px;
    text-align: center;
    background-color: var(--white);
    color:var(--black);
    cursor: pointer; /* 마우스 오버 시 커서 변경을 위해 추가합니다 */
    border: 1px solid var(--gray);
    transition: background-color 0.3s, color 0.3s; /* 부드러운 색상 전환 */
}

.container .content .menu-content .category-content .category.active{
    background-color: var(--gray);
    color: var(--white);
}

.container .content .menu-content .category-content:last-child{
    margin-right: 0;
}

/* 메뉴 리스트 */
.container .content .menu-content .menus-container {
}
.container .content .menu-content .menus-container .category-name{
    padding:var(--padding);
    background-color: var(--extra-light-gray);
    color: var(--gray);
    font-size: var(--font-middle);
    font-weight: bold;
    border-bottom: 2px solid var(--light-gray);
}
.container .content .menu-content .menus-container .menu{
    width: 100%;
    display: flex;
    border-bottom: 2px solid var(--light-gray);
}
.container .content .menu-content .menus-container .menu .image{
    padding: 10px;
    width: 120px;
    height: 120px;
    overflow: hidden;
}
.container .content .menu-content .menus-container .menu .image img{
    width: 100%;
    height: 100%;
}
.container .content .menu-content .menus-container .menu .text{
    padding: 10px;
    display: flex;
    flex-direction: column;
}
.container .content .menu-content .menus-container .menu .text .menu-name{
    margin-bottom: 10px;
    font-size: var(--font-content);
    font-weight: bold;
    color:var(--black);
}

.container .content .menu-content .menus-container .menu .text .menu-info{
    margin-bottom: 10px;
    flex: 1;
    font-size: var(--font-content);
    color:var(--gray);
}

.container .content .menu-content .menus-container .menu .text .menu-price{
    font-size: var(--font-content);
    color:var(--black);
}

/* 공지사항 콘텐츠 */
.container .content .announcement-content{
    width: 100%;
    display: none;
}

.container .content .announcement-content .announcement {
    width: 100%;
    display: flex;
    flex-direction: column;
    padding: 20px;
    border-bottom: 2px solid var(--light-gray);
}

.container .content .announcement-content .announcement .title {
    padding:0;
    font-size: var(--font-middle);
    color: var(--black);
    margin-bottom: 20px;
}
.container .content .announcement-content .announcement .date{
    padding:0;
    font-size: var(--font-small);
    color: var(--light-gray);
}
.container .content .announcement-content .announcement .content{
    padding:0;
    font-size: var(--font-content);
    color: var(--gray);
}

/* 리뷰 */
.container .content .review-content{
    width: 100%;
    display: none;
    padding: var(--padding);
}

.container .content .review-content .review{
    margin-bottom: 20px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    border: 2px solid var(--light-gray);
    padding: var(--padding);
}
.container .content .review-content .review .title{
    padding: 0;
    font-size: var(--font-content);
    color: var(--black);
}
.container .content .review-content .review .mid{
    display: flex;
    font-size: var(--font-small);
    color: var(--light-gray);
    flex: 1;
    padding-bottom: 20px;
}

.container .content .review-content .review .mid .star{
    font-size: var(--font-small); /* 별의 크기 조절 */
    color: #ccc; /* 기본 별 색상 (회색) */
    position: relative; /* 상대적 위치 설정 */
    margin-right: 20px;
}

.container .content .review-content .review .mid .star span.filled {
    color: gold; /* 노란색 별 */
}

.container .content .review-content .review .mid .star span.empty {
    color: #ccc; /* 회색 별 */
}

.container .content .review-content .review .mid .date{}
.container .content .review-content .review .content{
    font-size: var(--font-content);
    color: var(--black);
}

/* 모달 창 스타일 */
.modal {
    display: none; /* 초기에는 숨김 */
    position: absolute; /* 화면 중앙에 위치 */
    z-index: 1; /* 내용 위에 표시 */
    left: 0;
    top: 0;
    width: 100%; /* 전체 너비 */
    height: 100%; /* 전체 높이 */
    overflow: auto; /* 내용이 넘칠 경우 스크롤 */
    background-color: rgb(0,0,0); /* 검은색 배경 */
    background-color: rgba(0,0,0,0.4); /* 어두운 투명도 */
}

/* 모달 내용 스타일 */
.modal-content {
    background-color: #fefefe;
    margin: 15% auto; /* 페이지 중앙에 위치 */
    padding: 20px;
    border: 1px solid #888;
    width: 300px; /* 너비 설정 */
    height: 300px; /* 높이 설정 */
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

.modal-title {
    text-align: center;
    font-weight: bold;
}

.modal-warning {
    color: red;
    text-align: center;
}

.modal-label {
    text-align: left;
}

.modal-input {
    width: 100%;
    height: 100px;
}

.modal-submit {
    background-color: #222; /* 검은색 배경 */
    color: #fff; /* 하얀색 글씨 */
    text-align: center;
}

 

main.js - 여기서 별 구현은 GPT의 힘을 많이 빌림

const menu = document.querySelector(".menu-btn");
const announcement = document.querySelector(".announcement-btn");
const reviews = document.querySelector(".reviews-btn");
const menuContent = document.querySelector('.menu-content');
const announcementContent = document.querySelector('.announcement-content');
const reviewContent = document.querySelector('.review-content');

/* 식당 평점 별 부분*/
document.addEventListener('DOMContentLoaded', function() {
    var ratings = document.querySelectorAll('.ratings');

    ratings.forEach(function(rating) {
        var stars = rating.querySelector('.stars');
        var label = rating.querySelector('label');
        var ratingValue = parseFloat(label.textContent);
        var starPercentage = (ratingValue / 5) * 100; // 평점을 백분율로 변환
        var starPercentageRounded = `${parseFloat(starPercentage.toFixed(1))}%`; // 첫 번째 소수점 자리까지 표시

        // 금색 별을 표시할 span 요소 생성 및 스타일 설정
        var starFill = document.createElement('span');
        starFill.className = 'star-fill';
        starFill.style.width = starPercentageRounded;
        starFill.innerHTML = '★★★★★'; // 금색 별

        // 기존 별점 요소를 클리어하고, 금색 별 span과 기본 별을 추가
        stars.innerHTML = ''; // 기존 내용을 클리어
        stars.appendChild(starFill); // 금색 별 추가
        stars.innerHTML += '★★★★★'; // 기본 별 추가 (회색 별)
    });
});

/* 메뉴 버튼 클릭 시 */
menu.addEventListener('click', () => {
    menu.style.backgroundColor = '#ff7a2f';
    announcement.style.backgroundColor = 'inherit';
    reviews.style.backgroundColor = 'inherit';

    menu.style.color = '#fff';
    announcement.style.color = '#717171';
    reviews.style.color = '#717171';

    menuContent.style.display = 'block';
    announcementContent.style.display = 'none';
    reviewContent.style.display = 'none';
});

/* 공지사항 버튼 클릭 시 */
announcement.addEventListener('click', () => {
    menu.style.backgroundColor = 'inherit';
    announcement.style.backgroundColor = '#ff7a2f';
    reviews.style.backgroundColor = 'inherit';

    menu.style.color = '#717171';
    announcement.style.color = '#fff';
    reviews.style.color = '#717171';

    menuContent.style.display = 'none';
    announcementContent.style.display = 'block';
    reviewContent.style.display = 'none';
});


/* 리뷰 버튼 클릭 시 */
reviews.addEventListener('click', () => {
    menu.style.backgroundColor = 'inherit';
    announcement.style.backgroundColor = 'inherit';
    reviews.style.backgroundColor = '#ff7a2f';

    menu.style.color = '#717171';
    announcement.style.color = '#717171';
    reviews.style.color = '#fff';

    menuContent.style.display = 'none';
    announcementContent.style.display = 'none';
    reviewContent.style.display = 'block';
});

// 신고 버튼 클릭 이벤트
document.querySelector('.report-button').addEventListener('click', () => {
    document.getElementById('reportModal').style.display = 'block';
});

// 신고하기 버튼 클릭 이벤트로 모달 창 닫기 (옵션)
document.querySelector('.modal-submit').addEventListener('click', () => {
    document.getElementById('reportModal').style.display = 'none';
});

// 호출하기 버튼 클릭 이벤트
document.querySelector('.call-button').addEventListener('click', () => {
    document.getElementById('callModal').style.display = 'block';
});

// 호출하기 모달에서 호출하기 버튼 클릭 이벤트
document.getElementById('submitCall').addEventListener('click', () => {
    // 실제 애플리케이션에서는 이곳에 선택된 옵션을 처리하는 로직을 구현합니다.
    // 예: 선택된 라디오 버튼의 값을 서버로 전송
    console.log('호출 옵션:', document.querySelector('input[name="callOption"]:checked').value);
    document.getElementById('callModal').style.display = 'none'; // 모달 닫기
});

// 모달 창 밖을 클릭할 때 모달 창 닫기
window.addEventListener('click', e => {

    if (e.target.classList.contains('modal')) {
        e.target.style.display = 'none';
    }
});


// 슬라이더 부분
document.addEventListener('DOMContentLoaded', function() {
    const slider = document.querySelector('.category-content');
    let isDown = false;
    let startX;
    let scrollLeft;

    slider.addEventListener('mousedown', (e) => {
        isDown = true;
        slider.classList.add('active');
        startX = e.pageX - slider.offsetLeft;
        scrollLeft = slider.scrollLeft;
    });

    slider.addEventListener('mouseleave', () => {
        isDown = false;
        slider.classList.remove('active');
    });

    slider.addEventListener('mouseup', () => {
        isDown = false;
        slider.classList.remove('active');
    });

    slider.addEventListener('mousemove', (e) => {
        if(!isDown) return;
        e.preventDefault();
        const x = e.pageX - slider.offsetLeft;
        const walk = (x - startX) * 3; //scroll-fast
        slider.scrollLeft = scrollLeft - walk;
    });
});

document.addEventListener('DOMContentLoaded', function() {
    const categories = document.querySelectorAll('.category');
    const menuContainers = document.querySelectorAll('.menu-category');
    const categoryContent = document.querySelector('.category-content');

    // 현재 활성화된 카테고리를 표시하고 스크롤 시 갱신
    function setActiveCategory() {
        let currentActiveIndex = 0;
        menuContainers.forEach((container, index) => {
            const containerTop = container.getBoundingClientRect().top;
            if (containerTop - window.innerHeight / 2 < 0) {
                currentActiveIndex = index;
            }
        });

        categories.forEach((category, index) => {
            if (index === currentActiveIndex) {
                category.classList.add('active');
            } else {
                category.classList.remove('active');
            }
        });
    }

    // 클릭한 카테고리가 화면에 완전히 보이지 않을 경우 스크롤
    function ensureCategoryVisible(category) {
        const categoryRect = category.getBoundingClientRect();
        const containerRect = categoryContent.getBoundingClientRect();

        if (categoryRect.left < containerRect.left) {
            // 카테고리 버튼이 뷰포트 왼쪽 밖에 위치한 경우
            categoryContent.scrollLeft -= (containerRect.left - categoryRect.left) + 20; // 여백 추가
        } else if (categoryRect.right > containerRect.right) {
            // 카테고리 버튼이 뷰포트 오른쪽 밖에 위치한 경우
            categoryContent.scrollLeft += (categoryRect.right - containerRect.right) + 20; // 여백 추가
        }
    }

    // 카테고리 클릭 이벤트
    categories.forEach(category => {
        category.addEventListener('click', function() {
            const targetId = this.getAttribute('data-target');
            const targetElement = document.getElementById(targetId);

            if (targetElement) {
                // category-content의 높이를 가져옵니다.
                const categoryContentHeight = document.querySelector('.category-content').offsetHeight;

                // targetElement까지의 절대 위치를 계산합니다.
                const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;

                // category-content의 높이만큼 위치를 조정합니다.
                const offsetPosition = elementPosition - categoryContentHeight;

                // 계산된 위치로 스크롤합니다.
                window.scrollTo({
                    top: offsetPosition,
                    behavior: "smooth"
                });
            }
        });
    });

    // 스크롤 이벤트
    window.addEventListener('scroll', setActiveCategory);

    // 초기 활성화 카테고리 설정
    setActiveCategory();
});


// 리뷰 별점 정수값을 별 개수로 표시하는
document.addEventListener('DOMContentLoaded', function() {
    const reviewStars = document.querySelectorAll('.review .star');

    reviewStars.forEach(function(star) {
        const rating = parseInt(star.getAttribute('data-rating'));
        let starsText = '★★★★★';

        // 노란색 별을 표시할 부분과 회색 별을 표시할 부분을 결정
        let filledStars = starsText.slice(0, rating).replace(/★/g, '<span class="filled">★</span>');
        let emptyStars = starsText.slice(rating).replace(/★/g, '<span class="empty">★</span>');

        // 별점을 HTML로 설정
        star.innerHTML = filledStars + emptyStars;
    });
});

 

info.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title></title>
    <link href="/css/style.css" rel="stylesheet">
    <link href="/css/user/info.css" rel="stylesheet" type="text/css">
    <script src="/js/user/info.js" type="text/javascript" defer></script>
</head>
<body>
<div class="innerBox">
    <header></header>
    <div class="container">
        <form action="/user/info" method="post" class="info-form">
            <div class="menu-image">
                <img src="/images/sample.png" alt="음식 이미지" class="menu-img">
            </div>
            <div class="info">
                <div class="info-title">짜파게티</div>
                <div class="info-contents">맛있는 짜파게티 김치 미포함</div>
                <div class="info-price">300원</div>
            </div>
            <div class="option">
                <div class="option-title">
                    <div class="option-title-text">매운맛</div>
                    <div class="option-title-select">필수</div>
                </div>
                <div class="option-contents">
                    <div class="option-contents-select">
                        <input type="radio" name="spiciness" class="option-check" value="spicy">
                        <span class="option-contents-content">맵게 해주세요</span>
                        <span class="option-contents-price">+3000원</span>
                    </div>
                    <div class="option-contents-select">
                        <input type="radio" name="spiciness" class="option-check" value="not-spicy">
                        <span class="option-contents-content">안맵게 해주세요</span>
                        <span class="option-contents-price">+4000원</span>
                    </div>
                </div>
            </div>
            <div class="option">
                <div class="option-title">
                    <div class="option-title-text">토핑</div>
                    <div class="option-title-select">선택</div>
                </div>
                <div class="option-contents">
                    <div class="option-contents-select">
                        <input type="checkbox" class="option-check">
                        <span class="option-contents-content">치즈</span>
                        <span class="option-contents-price">1000원</span>
                    </div>
                    <div class="option-contents-select">
                        <input type="checkbox" class="option-check">
                        <span class="option-contents-content">감자</span>
                        <span class="option-contents-price">1000원</span>
                    </div>
                </div>
            </div>
            <div class="white-gap"></div>
            <div class="count">
                <div class="count-inner">
                    <div class="count-text">수량</div>
                    <div class="count-button">
                        <input type="button" class="minus" value="-">
                        <div class="text">1</div>개
                        <input type="button"  class="plus" value="+">
                    </div>
                </div>
                <input type="button" class="submit button" value="300원 담기" onclick="infosubmit(form)">
            </div>
        </form>
    </div>
    <footer></footer>
</div>
</body>
</html>

 

info.css

@charset "UTF-8";

.container form{
    width: 100%;
    margin: 0;
    padding: 0;
}
/* 메인 사진 */
.container .menu-image{
    width: 100%;
    height: 250px;
    overflow: hidden;
}

.container .menu-image img{
    width: 100%;
    height: auto;
    object-fit: cover;
    object-position: center top;
}

/* 음식 설명 */
.container .info{
    padding: var(--padding);
    margin: 0;
    display: flex;
    flex-direction: column;
    position: relative;
}

.container .info .info-title{
    font-size: var(--font-big);
    font-weight: bold;
    margin-bottom: 10px;
    color: var(--black);
}

.container .info .info-contents{
    font-size: var(--font-small);
    color: var(--light-gray);
}

.container .info .info-price{
    font-size: var(--font-middle);
    color: var(--black);
    text-align: right;
}

/* 옵션 */
.container .option{
    margin: 0;
    display: flex;
    flex-direction: column;
}

.container .option .option-title{
    padding: var(--padding);
    width: 100%;
    display: flex;
    justify-content: space-between;
    background-color: var(--extra-light-gray);
    color: var(--light-gray);
}

.container .option .option-title .option-title-text{
    padding: 10px;
    color: var(--gray);
}

.container .option .option-title .option-title-select{
    padding: 10px;
    background-color: var(--light-gray);
    color: var(--gray);
}

.container .option .option-title .option-title-select .option-check{
    margin-right: 10px;
}

.container .option .option-title .option-title-select .option-contents-content{
    margin: 10px 0;
}

.container .option .option-contents{
    padding: var(--padding);
    width: 100%;
    display: flex;
    flex-direction: column;
}

.container .option .option-contents .option-contents-select{
    display: flex;
    margin-bottom: 10px; /* 마지막 요소를 제외하고 각 요소의 하단에 간격 추가 */
}

.container .option .option-contents .option-contents-select:last-child {
    margin-bottom: 0; /* 마지막 요소는 하단 간격 제거 */
}

.container .option .option-contents .option-contents-select .option-contents-content{
    flex: 1;
}

/* count 때문에 넣는 빈공간 */
.container .white-gap{
    width: 100%;
    height: var(--fixedBtn-height);
}

/* 수량 */
.container .count {
    position: fixed;
    left: 50%;
    bottom: 0;
    margin: 0;
    /* 자신의 너비의 반만큼 왼쪽으로 이동하여 정확히 중앙에 위치하도록 조정 */
    transform: translateX(-50%);
    width: var(--innerBox-width);
    background-color: white;
    z-index: 1; /* 필요에 따라 조정 */
    padding: var(--padding);
}
.container .count .count-inner{
    display: flex;
    justify-content: space-between; /* 내부 요소를 양 끝으로 정렬 */
    width: 100%; /* 부모 컨테이너의 전체 너비 사용 */
    padding: 0 20px; /* 좌우 패딩 추가로 내부 요소들 사이 간격 조정 */
    margin-bottom: 10px;
}
.container .count .count-inner .count-text {
    font-size: var(--font-content); /* 필요에 따라 조정 */
    color: var(--black);
    display: flex;
    justify-content: center;
    align-items: center;
}

.container .count .count-inner .count-button {
    padding: 5px;
    display: flex;
    justify-content: right;
    border: 2px solid var(--light-gray);
}

.container .count .count-inner .count-button .minus,
.container .count .count-inner .count-button .plus{
    padding: 0 15px;
    display: flex;
    justify-content: center;
    align-items: center;
    border: 0;
    background-color: var(--white);
    font-size: var(--font-content); /* 버튼 내 텍스트 크기 */
    cursor: pointer; /* 마우스 커서 변경 */
}
.container .count .count-inner .count-button .text{
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: var(--font-content); /* 필요에 따라 조정 */
}

/* 버튼 */
.container .count .submit{
    width: calc(100% - 20px); /* 패딩 고려하여 너비 조정 */
}

info.js

/* form 하기 전에 체크 */

function infosubmit(form) {
    form.submit();
}

const ocs = document.querySelectorAll('.option-contents-select');

ocs.forEach(select => {
    select.addEventListener('click', e => {
        // 'this' 대신 'select' 사용
        const input = select.querySelector('input[type=checkbox], input[type=radio]');
        if (e.target !== input) {
            input.checked = !input.checked;
            // 라디오 버튼의 경우, 다른 라디오 버튼의 상태 변경을 위해 이벤트를 발생시킵니다.
            input.dispatchEvent(new Event('change'));
        }
    });
});

 

이제 메인은 반응형 하고 값 제대로 안들어갈때나 건드릴듯 

기능 참 많네 

'공부 > Ganju' 카테고리의 다른 글

[Spring/AWS] 팀프로젝트 8일차  (0) 2024.04.06
[Spring/AWS] 팀프로젝트 7일차  (0) 2024.04.04
[Spring/AWS] 팀프로젝트 5일차  (0) 2024.04.02
[Spring/AWS] 팀프로젝트 4일차  (0) 2024.04.02
[Spring/AWS] 팀프로젝트 3일차  (0) 2024.03.29

학원을 다니면서 연관된 자격증이나 없는것보다 있는게 나은 자격증 중에 가장 빨리 딸 수 있는게 cospro 1급이어서 하게되었습니다

 

자바를 2~3개월동안 배우면서 따로 준비한건 없고 

 

프로그래머스 코딩테스트 문제정도나 cos pro 문제 유형을 찾아보면서 익히는 정도로만 함

시험은 토요일로 기억하고 

 

추가로 전날 봤던건

https://edu.goorm.io/learn/lecture/17301/cos-pro-1%25EA%25B8%2589-%25EA%25B8%25B0%25EC%25B6%259C%25EB%25AC%25B8%25EC%25A0%259C-java

 

구름HOME

구름은 클라우드 기술을 이용하여 누구나 코딩을 배우고, 실력을 평가하고, 소프트웨어를 개발할 수 있는 클라우드 소프트웨어 생태계입니다.

www.goorm.io

구름 사이트의 cos pro 1급 java 무료 수강정도 였음

 

너무 긴장해서 들어가기전에 박카스 하나 먹고 들어감

 

시험은 결과를 보면서 할 수 있어서 좋았는데

마지막 문제가 if문을 써도 되는걸 while문을 써서 꼬아둔걸 빼곤 할만했음

 

 

만점 아닌건 아쉽지만 그럭저럭 첫 시험은 합격

자바 프로젝트 스도쿠 - 3일간의 삽질 일지

프로젝트 일정

2023. 12. 28 ~ 2023. 12. 31

 

Notion 링크  https://github.com/PARK-Yunjae/java_sudoku_PARK_Yunjae

 


사용법

  프로젝트로 실행 시에는 기본 더미 회원 데이터가 인식이 되는데

  exe 파일이나 jar 파일에서는 읽히지 않아 회원가입을 해야 진행됩니다.

  비밀번호에 영문 숫자 포함 4자이상패턴이 삽입되야 되니 참고.
  로그인 하면 나오는 난이도 클릭시 게임 진행 
  종료 버튼(사용자) 클릭시 그동안 진행 했던 데이터 메모장에 저장

 

join을 눌러 회원가입 진행 후 로그인

 

 

 

 

 

 

 

 

 

 

 

 

난이도를 선택하면 게임이 실행됩니다.

숫자 버튼을 클릭하고 화면의 빈곳을 클릭하면 맞으면 검은색 틀리면 빨간색 숫자가 나옵니다.

5번 틀리면 게임 오버

 

 

 

 

 

 

 

 

 

 


1. 오프닝

처음에는 이클립스 자바 콘솔 클리어 하는 걸 찾고 하려고했는데 결국 나오는건
외부 라이브러리를 받아서 하는 방법이나 언어나 프로그램을 바꾸는 방법밖에 없었습니다
그러다가 쓰레드에 알게되고 다른 방법인 Frame으로 새 창을 띄워서 하는 방법을 알게 되서 
시간이 될지는 모르겠지만 배우는겸 해서 해보려고 합니다

2. 구상 

무엇을 할까 많이 고민을 했는데 게임을 좋아 하기도 해서 결국 게임으로 정했고
그래픽이 들어가지 않는 게임중 뭐가 있을까 하다가 스도쿠도 좋아해서 스도쿠를 해보기로 했습니다
 
필요한 화면
   
메인화면 - 회원가입 로그인 종료 (화면에는 제목만 크게 간단하게 띄운다 배경은 아이보리나 흰색
회원가입 - 아이디와 비밀번호 이름을 입력받는다(아이디 중복 검사 비밀번호 패턴 검사)
- 메인 화면에서 회원 가입을 선택 하면 회원 가입 창으로 넘어감
- 회원가입이 끝나면 다시 메인 화면으로 돌아옴

로그인 - 메인 화면에서 로그인 창을 클릭하면 아이디와 비밀 번호를 입력하는 창이 뜨고 
- 틀리면 메시지를 띄워주고 메인 화면으로 돌아감
  
로그인 성공시에 로비 화면으로

로비화면 - 중간에는 난이도 버튼 상 중 하 랭킹보기 도움말(게임 설명)
- 상 중 하를 클릭 하면 게임 화면으로 넘어간다
- 랭킹 보기를 클릭 하면 랭킹 보기 화면으로 넘어간다
- 하단에는 로그아웃과 종료 버튼이 있다
- 로그아웃을 누르면 메인화면 종료 버트을 누르면 게임이 저장되지 않고 종료된다

게임화면 - 난이도별로 지워지는 숫자 개수와 실수 허용 개수가 다르다
- 상단에는 난이도 실수(난이도별로 다름) 점수 시간(스레드로)
- 중간에는 게임 화면
- 하단에는 숫자 버튼과 지우기버튼
- 실수 회수를 전부 채우면 게임 종료
- 게임을 클리어 하면 난이도별 시간 순으로 랭킹에 올라간다
- 하단에는 나가기와 종료 버튼이 있다
- 나가기를 누르면 로비 화면으로 종료 버튼을 누르면 게임이 저장되지 않고 종료가 된다

랭킹화면 - 랭킹은 순위 이름 점수 시간 순으로 보여주고 점수 기준으로 오름차순
- 방향키로 쉬움 보통 어려움 랭킹 순위를 넘겨볼 수 있다
- 나가기 버튼과 종료 버튼이 있다
- 나가기를 누르면 로비화면으로 종료를 누르면 게임이 저장되지 않고 종료된다
               
3. 설계 - 어떤 패키지와 어떤 클래스가 필요할까

Package - _Main, Controller, dao, dto, MainMenu, LobbyMenu, GameMenu, RankMenu, Frame

아직은 프레임을 다뤄본적이 없어서 생각은 여기까지

Class
_Main  - Main

Controller - SudoKuController (여기서 화면 전환 컨트롤 - 쇼핑몰에서 했던 것처럼)
- 여기서 화면 클래스가 가지고 있는 버튼이나 화면 정보를 맵으로 담을 수 있을까?

dto - Member (num, id, pw, name)
- Rank   (num, id, score, time, level)

dao - MemberDAO
- RankDAO
- FileDAO
- GameDAO

MainMenu - Join
- Login
- End

LobbyMenu - Low
- Middle
- High
- Logout
- Exit
- End

GameMenu - one, two, three, four, five, six, seven, eight, nine
- Eraser
- Exit

RankMenu - Exit
- End

Frame - MainFrame
- LobbyFrame
- GameFrame
- RankFrame

4. 구현 - 화면을 띄우는 것부터 막막하다

일단 화면을 띄우고 버튼을 만들어보는 실습부터 해보았다.
swing을 배워보려고 했으나 요즘  JavaFX가 대체되는 분위기라길래  JavaFX로 선회함.
GUI를 하나도 몰라서 브레인 스토밍이 잘 안되는 관계로 먼저 학습을 해보기로 함

- JavaFX 프로젝트 정상적으로 실행 시키는데 1시간이 걸릴줄은 몰랐음
https://heytech.tistory.com/176 링크를 남깁니다
- swing을 더 편하게 쓰는 도구를 받는 방법 링크
https://www.youtube.com/watch?v=rDRTD4uoVB4 
- 2:30 ~ 4:18
- 이것도 설치하고 오류나서 실패
- 재설치를 세번정도 하면서 해결이 되었는데 결국은 버전문제와
- 검색해서 나오는 설치방법이 다들 달라서 중복 설치 되거나 해서 충돌이 생기는 문제
- 2023-09 이클립스 jdk 17 버전에서 정상적으로 실행 됐던 방법은
https://wikidocs.net/208445
- 이 사이트를 통해서 Eclipse Marketplace를 통한 windowbuilder검색 후(띄어쓰기 안됨)
- WindowBuilder Current 를 설치하면 끝
- 또 주의사항이 restart 문구가 뜨기전에는 이클립스를 끄지 말것 
- 하단에 보면 설치중인 바가 보임

5. 재구상

JavaFx나 Swing을 좀 익힌 다음에서야 기존 구상은 수정이 필요 하다는걸 깨닫고 구상 단계부터 다시 함
위에선 그래픽이 들어가지 않는다고 했는데 GUI에선 어림도 없는 일이었고 
배경이나 버튼도 이미지가 필요함을 절실하게 느낌
버튼에 이미지를 써야 겠다는 점을 제외하곤 구상에선 달라진게 없을것 같다
랭킹뿐만 아니라 다른 메뉴에서도 방향키로 버튼을 옴겨다니는것도 나쁘지 않아 보임
랭킹은 난이도 별로 10위 까지만 보여주고 10위 밖으로 밀려나면 가지고 있다가 탈퇴 하거나 했을때 다시 올라온다

 6.재설계 

src에는 소스파일이 저장되서 img라는 새로운 폴더를 만들어서 이미지를 저장한다

Package - _Main, Controller, dao, dto, MainMenu, LobbyMenu, GameMenu, RankMenu, Frame

아직은 프레임을 다뤄본적이 없어서 생각은 여기까지

Class
_Main  - Main // 실행

Controller - SudoKuController // 패널 컨트롤 - 화면 숨기고, 화면 보이고

dto - Member (num, id, pw, name)   
- Rank   (id, score, time, level) // 저장만 되고 탈퇴시 삭제되니 변수가 크게 필요하지 않을듯

dao - MemberDAO // 회원 가입, 수정, 탈퇴
- RankDAO // 랭킹 계산해서 출력해주는 용도만 할듯
- FileDAO // 랭킹 맴버 파일 저장(x버튼 말고 종료 버튼 누를시 자동 저장), 로드(실행시)
- GameDAO // 게임 규칙을 지켜서 맵을 만든 후 난이도 별로 지워줘서 출력해주는 맵 구현
// 실수 스코어 시간 갱신, 정답시 체크 

panel - GamePanel // 81개의 텍스트, 1~9숫자 버튼 나가기, 상단에는 난이도 스코어 실수 시간(쓰레드?)
- JoinPanel // 회원가입 id pw name 텍스트, 회원가입 버튼, id 확인 버튼
- LobbyPanel // 상단에 이름 텍스트, 중앙에 상 중 하 난이도 버튼, 좌하단 랭크, 우하단 나가기
- MainPanel // 게임 실행 시 처음 나타나는 패널 id pw입력하는 텍스트 라벨과, 회원가입 로그인 버튼
// 나가기 버튼도 만들자
- RankPanel // 난이도 별로 10위까지 랭크 표시(스코어 순)

7.재구현 : 자고나서

GUI 라고 해서 결과를 확인하는건 쉽지 않아 보임 
어찌 저찌 화면 구현은 했는데 스도쿠 생성 로직이 생각보다 까다롭다
1. 같은 행 또는 열에 같은 숫자가 두번 이상 나오면 안된다
2. 스도쿠 배열은 9등분(3*3 크기의 부분 배열 9개)했을때  같은 영역 안에 숫자가 겹치면 안된다

규칙 하나하나는 괜찮은데 같이 적용 시키려니 까다롭다
생각보다 노다가로 해결하고

쉬움 :40  보통 :35  어려움 : 30  - 이것보다 1~2개가 적은게 일반적인 난이도로 보임
라벨로 어떻게든 클릭 이벤트 해보려고 했으나 무리 그냥 버튼으로 선회
한나절 동안 검색하고 수정하면서 방법을 찾아냄 다시 라벨로 전환
배우고 했으면 모르겠는데 하면서 배운다는게 결코 쉽지 않음을 다시 한번 느끼고 있음

JTable을 선택적으로 띄운다고 또 이상한테스트 하다가 늦어짐

  기존 재설계와 달라진 점은

Controller - SudoKuController 에서
  Frame - MainFrame     으로 변경된점 

   그리고 

    files 패키지를 추가하여 메모장에 랭킹과 맴버 리스트를 저장하고

     버튼을 전부 이미지로 변경하는 바람에

      img 폴더를 만들어 따로 저장하게됨

모르는것들은 계속 찾아가면서 하느라 많이 늦어졌는데 해보고 싶었던 기능 구현은 완료해서 여기에서 마무리 하는것으로
더 잘 할 수 있을것 같다는 아쉬움이 남는 프로젝트 였던것 같습니다.

 

 

프로젝트 exe 실행 파일

sudoku.exe
0.23MB

 

프로젝트 jar 실행 파일

sudoku.jar
0.17MB

 

스도쿠 로직 테스트

package _main;

import java.util.Random;

public class sudokuTest {
	public static void main(String[] args) {
//		int[][] map = new int[9][9];
		Random rd = new Random();

		// 00 01 02 03 04 05 06 07 08
		// 10 11 12 13 14 15 16 17 18
		// 20 21 22 23 24 25 26 27 28

		// 30 31 32 33 34 35 36 37 38
		// 40 41 42 43 44 45 46 47 48
		// 50 51 52 53 54 55 56 57 58

		// 60 61 62 63 64 65 66 67 68
		// 70 71 72 73 74 75 76 77 78
		// 80 81 82 83 84 85 86 87 88

		// 블럭당 3*3으로 돌고 그걸 9번 반복한다
//		while (true) {
//			int n = 0;
//			int m = 0;
//			for (int i = 0; i < 9; i += 1) {
//				// 3번째 까지 하면 y 값을 늘리고 x 값을 초기화 한다
//				if (m == 9) {
//					m = 0;
//					n += 3;
//				}
//
//				int num = 1;
//				// 순서대로 넣기
//				for (int y = n; y < n + 3; y += 1) {
//					for (int x = m; x < m + 3; x += 1) {
//						map[y][x] = num++;
//					}
//				}
//				// 셔플
//				for (int k = 0; k < 100; k += 1) {
//					int a = rd.nextInt(3) + n;
//					int b = rd.nextInt(3) + m;
//					int num2 = map[n][m];
//					map[n][m] = map[a][b];
//					map[a][b] = num2;
//				}
//				m += 3;
//			}
//
//			// 규칙 검사 - 각각 소규모 3*3 블럭 안에서 1~9를 굴렸기 때문에
//			// 가로 세로만 검사하자
//			// 1-9의 합은 45 각각의 합이 전부 45 라면 되지 않을까
//			int sum = 44;
//			int numGaro = 0;
//			int numSero = 0;
//			int answer = 0;
//			for (int i = 0; i < 9; i += 1) {
//				for (int k = 0; k < 9; k += 1) {
//					numGaro += map[i][k];
//					numSero += map[k][i];
//				}
//				System.out.println(numGaro);
//				System.out.println(numSero);
//				if (numGaro == sum && numSero == sum) {
//					answer += 1;
//				}else {
//					break;
//				}
//				numGaro = 0;
//				numSero = 0;
//			}
//			// 가로 세로 전부 45가 나오면 멈춰본다
//			if (answer == 9) {
//				break;
//			}
//			for (int i = 0; i < 9; i += 1) {
//				for (int k = 0; k < 9; k += 1) {
//					System.out.print(map[i][k] + " ");
//				}
//				System.out.println();
//			}
//			System.out.println();
//		}
//
//		
		// 전제가 잘못 되었다 숫자가 겹치지 않는 규칙을 만든 후 거기다 랜덤 숫자를 넣어보다
		int[] num = new int[9];
		// 숫자 순서대로 넣고
		for(int i=0 ; i<9 ; i+=1) {
			num[i] = i+1;
		}
		// 셔플
		for(int i=0 ; i < 100 ; i+=1) {
			int a = rd.nextInt(9);
			int num2 = num[a];
			num[a] = num[0];
			num[0] = num2;
		}
		// 셔플한 숫자를 순서대로 넣고
		int a1 = num[0];
		int a2 = num[1];
		int a3 = num[2];
		int a4 = num[3];
		int a5 = num[4];
		int a6 = num[5];
		int a7 = num[6];
		int a8 = num[7];
		int a9 = num[8];
		
		int[][] map = { {a6 ,a4 ,a7 ,a1 ,a8 ,a2 ,a9 ,a5 ,a3 },
				{a1 ,a8 ,a2 ,a9 ,a5 ,a3 ,a6 ,a4 ,a7 },
				{a9 ,a5 ,a3 ,a6 ,a4 ,a7 ,a1 ,a8 ,a2}, 
				{a4 ,a7 ,a6 ,a8 ,a2 ,a1 ,a5 ,a3 ,a9 },
				{a8 ,a2 ,a1 ,a5 ,a3 ,a9 ,a4 ,a7 ,a6 },
				{a5 ,a3 ,a9 ,a4 ,a7 ,a6 ,a8 ,a2 ,a1 },
				{a2 ,a1 ,a8 ,a3 ,a9 ,a5 ,a7 ,a6 ,a4 },
				{a3 ,a9 ,a5 ,a7 ,a6 ,a4 ,a2 ,a1 ,a8 },
				{a7 ,a6 ,a4 ,a2 ,a1 ,a8 ,a3 ,a9 ,a5 }};
				
		for(int i=0 ; i<9 ; i+=1) {
			for(int k=0 ; k<9 ; k+=1) {
				System.out.print(map[i][k] +" ");
			}
			System.out.println();
		}
	}
}


이 로직을 짤때 처음에는 겹치지 않는 숫자가 나올때 까지 하다보면 빠른 컴퓨터 속도르 되지 않을까 했는데 경우의 수를 무시했다.
결국 겹치지 않는 스도쿠를 먼저 만든 다음에 거기에 숫자를  a1 ~ a9에 들어가는 숫자를 랜덤으로 겹치지 않는 숫자로 넣는 방법으로 바꾸었다.

'공부 > Project' 카테고리의 다른 글

[Spring, AWS] Ganju Project  (0) 2024.04.23
[JSP/SQL] 프로젝트 EYEVEL  (0) 2024.03.25
[HTTP/CSS/Java Script] Project Momizi  (0) 2024.03.24

+ Recent posts