하루 늦은 기록
프론트 디자인 및 이벤트 구현 완료
지금은 menu와 카테고리 더미데이터만 불러와서 테스트 해봤는데
다음주에는 결재하고 인증키 까지 받아서 db 에 저장하고 환불 하는 기능까지 해볼 예정
main 부분이 많이 바뀌진 않았지만 버그 수정 및 팀장님이 말하던 아이콘 위치나 이벤트는 완료
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">
<span><i class="far fa-star"></i></span>
<span><i class="far fa-star"></i></span>
<span><i class="far fa-star"></i></span>
<span><i class="far fa-star"></i></span>
<span><i class="far fa-star"></i></span>
</div> <!-- 5개의 별을 기본으로 설정 -->
<label>3.8</label> <!-- 예시 평점 -->
</div>
<button class="call-button button">호출하기</button>
</div>
<div class="buttons">
<button class="menu-btn button tab-btn" data-target="menu-content">메뉴</button>
<button class="announcement-btn button tab-btn" data-target="announcement-content">공지사항</button>
<button class="reviews-btn button tab-btn" data-target="review-content">리뷰</button>
</div>
<div class="content">
<div class="menu-content tab-content">
<div class="category-content">
<div th:each="category : ${categories}" class="category" th:text="${category.name}" th:data-targets="'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=\'/menu/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 tab-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 tab-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>
<template th:replace="~{user/modal/modal :: modal}" ></template>
</div>
</body>
</html>
main.css
@charset "UTF-8";
/* 메인 이미지 */
.container .restaurant-image{
width: 100%;
height: 200px;
overflow: hidden;
}
.container .restaurant-image img{
width: 100%;
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: var(--red);
border: none;
font-size: var(--font-small);
}
.container .title .report-button .fas{
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: var(--font-content); /* 별 크기 조정 */
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{
display: flex; /* 이 부분을 추가합니다 */
overflow-x: auto; /* 수평 스크롤을 위해 추가합니다 */
background-color: var(--white);
scrollbar-width: none;
z-index: 1; /* 다른 요소들 위에 오도록 z-index 설정 */
}
.sticky {
position: fixed;
top: 0; /* 또는 header 높이에 맞추어 조정 */
width: var(--innerBox-width);
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* 선택사항: 고정된 요소에 그림자 효과 추가 */
}
.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);
cursor: pointer;
}
.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);
margin-bottom: 5px;
}
.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 span{
font-size: var(--font-small);
}
.container .content .review-content .review .mid .star span .fas {
color: var(--gold); /* 노란색 별 */
}
.container .content .review-content .review .mid .star span .far{
color: var(--light-gray); /* 회색 별 */
}
.container .content .review-content .review .mid .date{
margin-left: 10px;
}
.container .content .review-content .review .content{
font-size: var(--font-content);
color: var(--black);
}
main.js
/* 식당 평점 별 부분*/
function ratingStar(){
const rating = document.querySelector('.ratings');
const stars = rating.querySelectorAll('.stars span i');
const label = rating.querySelector('label');
const ratingValue = parseFloat(label.textContent);
// 별 아이콘 초기화
stars.forEach(star => {
star.classList.remove('fas', 'fa-star', 'fa-star-half-alt');
star.classList.add('far', 'fa-star');
star.style.color = 'var(--light-gray)'; // 기본 별 색상
});
// 평점에 따라 별 아이콘 적용
for (let i = 0; i < stars.length; i++) {
if ((ratingValue - i) > 1 ) {
stars[i].classList.remove('far', 'fa-star-half-alt');
stars[i].classList.add('fas', 'fa-star');
stars[i].style.color = 'var(--gold)'; // 채워진 별 색상
}
else if((ratingValue - i) >= 0.5){
stars[i].classList.remove('far', 'fa-star');
stars[i].classList.add('fas', 'fa-star-half-alt');
stars[i].style.color = 'var(--gold)'; // 반 채워진 별 색상
break;
}
}
}
/* 메뉴 공지 사항 리뷰 버튼 관련 부분 */
const buttons = document.querySelectorAll(".tab-btn");
const contents = document.querySelectorAll(".tab-content");
// 탭 버튼 클릭 이벤트 핸들러
function handleTabClick(event) {
// 모든 버튼과 컨텐츠 초기화
buttons.forEach(btn => {
btn.style.backgroundColor = 'inherit';
btn.style.color = '#717171';
});
contents.forEach(content => {
content.style.display = 'none';
});
// 클릭된 버튼과 관련된 컨텐츠 활성화
const selectedContentId = event.target.getAttribute('data-target');
const selectedContent = document.querySelector(`.${selectedContentId}`);
console.log(selectedContentId);
event.target.style.backgroundColor = 'var(--orange)';
event.target.style.color = 'var(--white)';
selectedContent.style.display = 'block';
}
// 각 버튼에 이벤트 리스너 추가
buttons.forEach(button => {
button.addEventListener('click', handleTabClick);
});
// 슬라이더 부분
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;
});
// 현재 활성화된 카테고리를 갱신하고 배경과 글씨 색 변견
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');
ensureCategoryVisible(category);
} else {
category.classList.remove('active');
}
});
}
// 클릭한 카테고리가 화면에 완전히 보이지 않을 경우 스크롤
function ensureCategoryVisible(category) {
const categoryRect = category.getBoundingClientRect();
const containerRect = categoryContent.getBoundingClientRect();
console.log(category);
console.log("들어는 오니");
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', () => {
const targetId = this.getAttribute('data-targets');
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"
});
}
// 현재 클릭된 카테고리가 화면에 완전히 보이도록 스크롤 조정
ensureCategoryVisible(this); // 여기서 this는 현재 클릭된 카테고리 요소입니다.
});
});
// 페이지 로드 시 .category-content의 원래 위치를 계산하여 저장
const stickyElement = document.querySelector('.category-content');
const headerOffset = document.querySelector('header').offsetHeight; // 만약 header가 있다면
const originalOffsetTop = stickyElement.offsetTop - headerOffset; // header 높이를 고려한 조정값
// 스크롤 이벤트 리스너를 추가하는 새 함수
window.addEventListener('scroll', () => {
if (window.pageYOffset >= originalOffsetTop ) {
stickyElement.classList.add('sticky');
} else {
stickyElement.classList.remove('sticky');
}
});
// 리뷰 별점 정수값을 별 개수로 표시하는
function reviewStar(){
const reviewStars = document.querySelectorAll('.review .star');
reviewStars.forEach(star => {
const rating = parseInt(star.getAttribute('data-rating'));
const totalStars = 5;
let starsHtml = '';
// 채워진 별 생성
for (let i = 1; i <= rating; i++) {
starsHtml += '<span><i class="fas fa-star"></i></span>';
}
// 빈 별 생성
for (let i = rating + 1; i <= totalStars; i++) {
starsHtml += '<span><i class="far fa-star"></i></span>';
}
// 별점을 HTML에 삽입
star.innerHTML = starsHtml;
});
}
document.addEventListener('DOMContentLoaded', () => {
// 별포 표시 부분 함수
ratingStar();
// 리뷰 콘텐츠의 별점 개수 표시 하는 함수
reviewStar();
// 스크롤 이벤트
window.addEventListener('scroll', setActiveCategory);
// 초기 활성화 카테고리 설정
setActiveCategory();
});
그리고 코드가 너무 길어져 modal 부분을 분리했다
modal.html
<!DOCTYPE html>
<html lang="en">
<div th:fragment="modal">
<link href="/css/user/modal/modal.css" rel="stylesheet" type="text/css">
<script src="/js/user/modal/modal.js" type="text/javascript" defer></script>
<!-- 신고하기 모달 창 -->
<div id="reportModal" class="modal report-modal">
<div class="modal-content">
<h2 class="modal-title">가게 신고</h2>
<p class="modal-warning">가게 신고 시 돌이킬 수 없으며,<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" class="check" name="callOption" value="water" checked/><label></label><span>물 주세요</span></label>
<label><input type="radio" class="check" name="callOption" value="cup" /><label></label><span>컵 주세요</span></label>
<label><input type="radio" class="check" name="callOption" value="towel" /><label></label><span>물수건 주세요</span></label>
<label><input type="radio" class="check" name="callOption" value="staff" /><label></label><span>그냥 직원 오세요</span></label>
<input type="button" class="modal-submit button" id="submitCall" value="호출하기">
</form>
</div>
</div>
</div>
</html>
modal.css
/* 모달 창 스타일 */
.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: #ffffff;
margin: 10% auto; /* 페이지 중앙에 위치 조정 */
padding: 20px; /* 패딩 증가 */
border-radius: 8px; /* 모서리 둥글게 처리 */
width: 400px; /* 너비 조정 */
max-width: 90%; /* 최대 너비 설정 */
}
.modal-title {
font-size: var(--font-middle); /* 글씨 크기 조정 */
color: var(--black); /* 글씨 색상 변경 */
margin-bottom: 20px; /* 하단 여백 추가 */
}
.modal-warning {
color: var(--red); /* 경고 문구 색상 변경 */
font-size: 16px; /* 글씨 크기 조정 */
margin-bottom: 20px; /* 하단 여백 추가 */
}
.modal-label {
display: block; /* 라벨을 블록 요소로 변경 */
margin-bottom: 10px; /* 여백 추가 */
font-weight: bold; /* 글씨 두껍게 */
color: var(--black); /* 글씨 색상 변경 */
}
.modal-input {
width: 100%;
height: 100px;
border: 1px solid var(--light-gray);
border-radius: 4px; /* 모서리 둥글게 처리 */
padding: 10px; /* 내부 패딩 추가 */
margin: 0 0 20px 0; /* 하단 여백 추가 */
}
.modal-submit {
width: 100%;
font-size: var(--font-content);
}
/* 호출하기 모달 */
#callForm {
width: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
#callForm>label {
display: flex; /* 라벨 블록 처리 */
align-items: center;
margin-bottom: 10px; /* 라벨 간 간격 */
cursor: pointer; /* 마우스 오버 시 커서 변경 */
}
#callForm>.modal-submit{
margin-top: 20px;
}
/* 옵션 사용자 버튼 */
.check{
display: none;
}
/* 라벨의 가상 요소를 이용해 라디오 버튼 스타일링 */
#callForm .check[type="radio"] + label::before {
content: '\f111'; /* Font Awesome의 circle 아이콘 */
font-family: 'Font Awesome 5 Free';
font-weight: 500;
color: var(--light-gray);
font-size: var(--font-middle);
margin-right: var(--padding);
}
/* 체크된 라디오 버튼의 스타일 */
#callForm .check[type="radio"]:checked + label::before {
content: '\f058'; /* Font Awesome의 check-circle 아이콘 */
color: var(--orange);
}
modal.js
// 신고하기 버튼 클릭 이벤트
document.querySelector('.report-button').addEventListener('click', () => {
action();
document.getElementById('reportModal').style.display = 'block';
});
// 신고하기 버튼 클릭 이벤트로 모달 창 닫기 (옵션)
document.querySelector('.modal-submit').addEventListener('click', () => {
document.getElementById('reportModal').style.display = 'none';
document.body.style.overflow = ''; // 스크롤 활성화
});
// 호출하기 버튼 클릭 이벤트
document.querySelector('.call-button').addEventListener('click', () => {
action();
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'; // 모달 닫기
document.body.style.overflow = ''; // 스크롤 활성화
});
// 모달 창 밖을 클릭할 때 모달 창 닫기
window.addEventListener('click', e => {
if (e.target.classList.contains('modal')) {
e.target.style.display = 'none';
document.body.style.overflow = ''; // 스크롤 활성화
}
});
function action(){
window.scrollTo(0, 0); // 스크롤을 맨 위로 이동
document.body.style.overflow = 'hidden'; // 스크롤 비활성화
}
주말에 스프링 공부와 정보처리기사 실기 자격증 공부 예정
'공부 > Ganju' 카테고리의 다른 글
| [Spring/AWS] 팀프로젝트 10일차 (0) | 2024.04.09 |
|---|---|
| [Spring/AWS] 팀프로젝트 9일차 (0) | 2024.04.08 |
| [Spring/AWS] 팀프로젝트 7일차 (0) | 2024.04.04 |
| [Spring/AWS] 팀프로젝트 6일차 (0) | 2024.04.03 |
| [Spring/AWS] 팀프로젝트 5일차 (0) | 2024.04.02 |
