코드리뷰 - View 부분(HTML,CSS,JavaScript)
Controller은 두개로 HomeController, LoginController
LoginController
@Controller
@RequiredArgsConstructor
public class LoginController {
private final AuthenticationManager authenticationManager;
private final UserService userService;
@GetMapping("/login")
public String showLoginPage() {
return "login";
}
@PostMapping("/login")
public String login(@ModelAttribute("username") String username,
@ModelAttribute("password") String password,
Model model) {
try {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/home";
} catch (AuthenticationException e) {
model.addAttribute("error", "Invalid username or password");
return "login";
}
}
@GetMapping("/signup")
public String showSignupPage() {
return "signup";
}
@PostMapping("/signup")
public String signup(@ModelAttribute UserDto.Signup signupDto, Model model) {
try {
userService.signup(signupDto);
return "redirect:/login";
} catch (Exception e) {
model.addAttribute("error", "An error occurred during signup: " + e.getMessage());
return "signup";
}
}
}
유저의 기능은 진짜 별거 안넣었다. 회원가입 기능과 로그인 기능뿐이다.
get 메서드는 html 파일을 불러오는 기능을 하고, Post 메서드는 실제 처리를 해주는 메서드다.
post메서드의 signup을 보면, service의 signup을 수행하고 성공하면 login 페이지로 리다이렉트 시켜준다.
Login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
<link rel="stylesheet" th:href="@{/css/styles.css}">
</head>
<body>
<div class="container">
<div class="logo">
<img th:src="@{/images/logo.png}" alt="Logo">
</div>
<h2>Wodiary</h2>
<form th:action="@{/perform_login}" method="post">
<div>
<label for="username">이름:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">비밀번호:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<button type="submit" class="btn login">로그인<div class="dot"></div></button>
</div>
<div th:if="${param.error}">
<p style="color: red;">아이디 또는 비밀번호를 잘못 입력하셨습니다.</p>
</div>
</form>
<div>
<a th:href="@{/signup}">
<button type="button" class="btn signup">회원 가입<div class="dot"></div></button>
</a>
</div>
</div>
</body>
</html>
뭐 설명할것도 딱히 없다.
Signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Sign Up</title>
<link rel="stylesheet" th:href="@{/css/styles.css}">
</head>
<body>
<div class="container">
<h2>회원 가입</h2>
<form th:action="@{/api/v1/users/signup}" method="post">
<div>
<label for="username">이름:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">비밀번호:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<label for="email">이메일:</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<button type="submit" class="submit">회원 가입</button>
</div>
</form>
</div>
</body>
</html>
이것도 뭐 딱히 특별한것도 없다.
이제부터 좀 길다.
Home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
<link rel="stylesheet" th:href="@{/css/styles.css}">
<style>
/* 모달 스타일 */
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 100px;
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: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<div class="calendar-controls">
<a th:href="@{/home?year={year}&month={month}(year=${currentYear}, month=${currentMonth - 1})}">
<
</a>
<span th:text="${currentYear} + '년 ' + ${currentMonth} + '월'"></span>
<a th:href="@{/home?year={year}&month={month}(year=${currentYear}, month=${currentMonth + 1})}">
>
</a>
</div>
<div class="calendar">
<div class="day-names">
<div style="color: red;">일</div>
<div>월</div>
<div>화</div>
<div>수</div>
<div>목</div>
<div>금</div>
<div style="color: blue;">토</div>
</div>
<div class="days">
<div th:each="emptyDay : ${emptyDays}">
<div class="day empty"></div>
</div>
<div th:each="day : ${days}">
<a th:href="@{'/wsession/' + ${currentYear} + '-' + ${paddedMonth} + '-' + ${day}}">
<div class="day" th:text="${day}"th:classappend="${sessionDays.contains(day) ? ' session' : ''}"></div>
</a>
</div>
</div>
</div>
<div class="monthly-stats">
<p>이번달 총 볼륨: <span th:text="${totalVolume}+kg"></span></p>
</div>
<div class="logout">
<form th:action="@{/logout}" method="post">
<button type="submit" class="logout-button">로그아웃</button>
</form>
</div>
<button onclick="openModal()" class="copy-button">세션 복사</button>
</div>
<div id="copyModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">×</span>
<h2>Copy Session</h2>
<label for="sourceDate">세션을 복사할 날짜:</label>
<input type="date" id="sourceDate" name="sourceDate"><br><br>
<label for="targetDate">세션을 붙혀넣을 날짜:</label>
<input type="date" id="targetDate" name="targetDate"><br><br>
<button onclick="submitCopySession()" class="copy-button">Submit</button>
</div>
</div>
<button onclick="initiateCopySession()">Copy Session</button>
<script>
function openModal() {
document.getElementById('copyModal').style.display = "block";
}
function closeModal() {
document.getElementById('copyModal').style.display = "none";
}
function submitCopySession() {
var sourceDate = document.getElementById('sourceDate').value;
var targetDate = document.getElementById('targetDate').value;
if (!sourceDate || !targetDate) {
alert('Both source date and target date are required.');
return;
}
copySession(sourceDate, targetDate);
}
function fetchSession(date) {
var token = localStorage.getItem('token');
fetch(`/api/v1/wsession/${date}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
})
.then(response => {
if (response.ok) {
window.location.href = `/wsession/${date}`;
} else if (response.status === 401) {
alert('Unauthorized. Please login again.');
window.location.href = '/login';
} else {
alert('Error fetching session.');
}
})
.catch(error => console.error('Error:', error));
}
function initiateCopySession() {
var sourceDate = prompt('세션을 복사할 날짜를 입력하세요.(YYYY-MM-DD)');
if (!sourceDate) {
alert('Source date is required.');
return;
}
var targetDate = prompt('붙혀넣을 날짜를 입력하세요.(YYYY-MM-DD)');
if (!targetDate) {
alert('Target date is required.');
return;
}
copySession(sourceDate, targetDate);
}
function copySession(sourceDate, targetDate) {
fetch('/api/v1/wsession/copy', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
},
body: JSON.stringify({sourceDate: sourceDate, targetDate: targetDate})
})
.then(response => {
if (response.ok) {
alert('Session copied successfully.');
window.location.reload();
} else if (response.status === 401) {
alert('Unauthorized. Please login again.');
window.location.href = '/login';
} else {
response.text().then(text => alert('Error copying session: ' + text));
}
})
.catch(error => console.error('Error:', error));
}
</script>
</body>
</html>
부분별로 설명해보자면,
<style>
/* 모달 스타일 */
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 100px;
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: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
</style>
이 부분은 모달창의 스타일을 정하는 css 이다.
<body>
<div class="container">
<div class="calendar-controls">
<a th:href="@{/home?year={year}&month={month}(year=${currentYear}, month=${currentMonth - 1})}">
<
</a>
<span th:text="${currentYear} + '년 ' + ${currentMonth} + '월'"></span>
<a th:href="@{/home?year={year}&month={month}(year=${currentYear}, month=${currentMonth + 1})}">
>
</a>
</div>
<div class="calendar">
<div class="day-names">
<div style="color: red;">일</div>
<div>월</div>
<div>화</div>
<div>수</div>
<div>목</div>
<div>금</div>
<div style="color: blue;">토</div>
</div>
<div class="days">
<div th:each="emptyDay : ${emptyDays}">
<div class="day empty"></div>
</div>
<div th:each="day : ${days}">
<a th:href="@{'/wsession/' + ${currentYear} + '-' + ${paddedMonth} + '-' + ${day}}">
<div class="day" th:text="${day}"th:classappend="${sessionDays.contains(day) ? ' session' : ''}"></div>
</a>
</div>
</div>
</div>
<div class="monthly-stats">
<p>이번달 총 볼륨: <span th:text="${totalVolume}+kg"></span></p>
</div>
<div class="logout">
<form th:action="@{/logout}" method="post">
<button type="submit" class="logout-button">로그아웃</button>
</form>
</div>
<button onclick="openModal()" class="copy-button">세션 복사</button>
</div>
<div id="copyModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">×</span>
<h2>Copy Session</h2>
<label for="sourceDate">세션을 복사할 날짜:</label>
<input type="date" id="sourceDate" name="sourceDate"><br><br>
<label for="targetDate">세션을 붙혀넣을 날짜:</label>
<input type="date" id="targetDate" name="targetDate"><br><br>
<button onclick="submitCopySession()" class="copy-button">Submit</button>
</div>
</div>
<button onclick="initiateCopySession()">Copy Session</button>
이부분은 html로 home에 달력을 표시하고, 로그아웃 버튼, 세션복사버튼이 있다.
세션복사를 누르면 모달이 뜨는데, 거기에 대한 세팅이다.
이렇게 모달이 뜬다.
이제 자바스크립트부분을 보자.
<script>
function openModal() {
document.getElementById('copyModal').style.display = "block";
}
function closeModal() {
document.getElementById('copyModal').style.display = "none";
}
function submitCopySession() {
var sourceDate = document.getElementById('sourceDate').value;
var targetDate = document.getElementById('targetDate').value;
if (!sourceDate || !targetDate) {
alert('Both source date and target date are required.');
return;
}
copySession(sourceDate, targetDate);
}
function fetchSession(date) {
var token = localStorage.getItem('token');
fetch(`/api/v1/wsession/${date}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
})
.then(response => {
if (response.ok) {
window.location.href = `/wsession/${date}`;
} else if (response.status === 401) {
alert('Unauthorized. Please login again.');
window.location.href = '/login';
} else {
alert('Error fetching session.');
}
})
.catch(error => console.error('Error:', error));
}
function initiateCopySession() {
var sourceDate = prompt('세션을 복사할 날짜를 입력하세요.(YYYY-MM-DD)');
if (!sourceDate) {
alert('Source date is required.');
return;
}
var targetDate = prompt('붙혀넣을 날짜를 입력하세요.(YYYY-MM-DD)');
if (!targetDate) {
alert('Target date is required.');
return;
}
copySession(sourceDate, targetDate);
}
function copySession(sourceDate, targetDate) {
fetch('/api/v1/wsession/copy', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
'Content-Type': 'application/json'
},
body: JSON.stringify({sourceDate: sourceDate, targetDate: targetDate})
})
.then(response => {
if (response.ok) {
alert('Session copied successfully.');
window.location.reload();
} else if (response.status === 401) {
alert('Unauthorized. Please login again.');
window.location.href = '/login';
} else {
response.text().then(text => alert('Error copying session: ' + text));
}
})
.catch(error => console.error('Error:', error));
}
</script>
위에서 설명한 copySession의 파라미터(sourceDate, targetDate)를 매핑하고 예외처리하고, api를 매핑해준다.
addExercise.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Add Exercise</title>
<link rel="stylesheet" th:href="@{/css/styles.css}">
<style>
.multi-select-container {
display: flex;
flex-direction: column;
}
.multi-select-item {
margin: 5px 0;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
.multi-select-item.selected {
background-color: #d3d3d3;
}
</style>
</head>
<body>
<div class="container">
<h2 th:text="'운동 세션 추가'"></h2>
<form id="addExerciseForm">
<label for="date">날짜</label>
<input type="date" id="date" name="date" th:value="${date}" required>
<p>운동 종목</p>
<div id="exerciseTypesContainer" class="multi-select-container">
<div class="multi-select-item" data-value="SQUATS">Squats</div>
<div class="multi-select-item" data-value="BENCH_PRESS">Bench Press</div>
<div class="multi-select-item" data-value="DEAD_LIFT">Dead Lift</div>
<div class="multi-select-item" data-value="OVERHEAD_PRESS">Overhead Press</div>
<div class="multi-select-item" data-value="BARBELL_ROW">Barbell Row</div>
<div class="multi-select-item" data-value="PULL_UPS">Pull Ups</div>
<div class="multi-select-item" data-value="DUMBBELL_FLIES">Dumbbell FlIES</div>
<div class="multi-select-item" data-value="LEG_PRESS">Leg Press</div>
<div class="multi-select-item" data-value="BICEPS_CURLS">Biceps Curls</div>
<div class="multi-select-item" data-value="TRICEPS_EXTENSIONS">Triceps Extensions</div>
</div>
<button type="submit" class="btn add-wsession-button">세션 추가<div class="dot"></div></button>
</form>
<a th:href="@{/home}"><button type="button" class="btn back">뒤로가기<div class="dot"></div></button></a>
</div>
<script>
let selectedExercises = [];
document.querySelectorAll('.multi-select-item').forEach(item => {
item.addEventListener('click', () => {
toggleSelect(item.getAttribute('data-value'));
});
});
function toggleSelect(exercise) {
const index = selectedExercises.indexOf(exercise);
if (index === -1) {
selectedExercises.push(exercise);
} else {
selectedExercises.splice(index, 1);
}
document.getElementById('selectedExercises').value = selectedExercises.join(',');
}
document.querySelectorAll('.multi-select-item').forEach(item => {
item.addEventListener('click', () => {
item.classList.toggle('selected');
});
});
document.getElementById('addExerciseForm').addEventListener('submit', function(event) {
event.preventDefault();
const date = document.getElementById('date').value;
const exerciseTypes = Array.from(document.querySelectorAll('.multi-select-item.selected')).map(item => item.dataset.value);
fetch('/api/v1/wsession', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: date,
exerciseTypes: exerciseTypes
})
})
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Failed to create session');
}
})
.then(data => {
alert('운동 세션이 추가되었습니다!');
window.location.href = `/wsession/${date}`;
})
.catch(error => {
console.error('Error:', error);
alert('운동 세션 추가 중 오류가 발생했습니다.');
});
});
</script>
</body>
</html>
운동종목들이 여러개니 스크롤할 수 있게 css로 스타일을 정의하고, 운동종목들을 여러개 선택할 수 있으니 multi-select-item으로 설정하고 버튼까지 정의했다.
자바스크립트부분을 보면 createWsession 메서드를 매핑해서 세션을 만들수 있게 했다.
viewSession.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>View Session</title>
<link rel="stylesheet" th:href="@{/css/styles.css}">
</head>
<body>
<div class="container">
<h3 th:text="${date} + ' 운동 세션'" class="text.black"></h3>
<div th:if="${wsession != null}">
<div th:each="exercise : ${wsession.exercises}">
<h2 th:text="${exercise.type}"></h2>
<div th:each="set : ${exercise.sets}" class="set-container">
<p th:text="'세트 ' + ${set.setOrder} + ': ' + ${set.weight} + 'kg x ' + ${set.reps} + '회'" class="set-description"></p>
<button th:attr="onclick=|deleteSet(${set.id})|" class="delete-set-button">X</button> <!--세트삭제버튼-->
</div>
<button type="button" th:attr="onclick=|deleteExercise(${exercise.id})|" class="delete-exercise-button">종목 삭제</button>
<button type="button" th:onclick="'addSet(' + ${exercise.id} + ')'" class="add-set-button">세트 추가</button>
</div>
<button id="addMoreExercisesButton" th:data-date="${date}" class="add-exercise-button">종목 추가</button>
</div>
<div th:if="${session == null}">
<p>운동 세션이 없습니다.</p>
<a th:href="@{/api/v1/exercises/{exerciseID}/addSet?date=${date}}"><button type="button" class="add-set">세트 추가하기</button></a>
</div>
<a th:href="@{/home}"><button type="button" class="back">뒤로가기</button></a>
</div>
<script>
function addSet(exerciseId) {
console.log('addSet called with exerciseId:', exerciseId);
// 세트 추가를 위한 폼 생성
const form = document.createElement('form');
form.method = 'POST';
form.action = `/api/v1/exercises/${exerciseId}/sets`;
const weightLabel = document.createElement('label');
weightLabel.textContent = '무게 (kg)';
form.appendChild(weightLabel);
const weightInput = document.createElement('input');
weightInput.type = 'number';
weightInput.name = 'weight';
weightInput.required = true;
form.appendChild(weightInput);
const repsLabel = document.createElement('label');
repsLabel.textContent = '반복 횟수';
form.appendChild(repsLabel);
const repsInput = document.createElement('input');
repsInput.type = 'number';
repsInput.name = 'reps';
repsInput.required = true;
form.appendChild(repsInput);
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.textContent = '세트 추가';
submitButton.style.backgroundColor = 'blue';
form.appendChild(submitButton);
// 세트 추가 폼을 다이얼로그로 표시
const dialog = document.createElement('dialog');
dialog.appendChild(form);
document.body.appendChild(dialog);
dialog.showModal();
form.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(form);
fetch(`/api/v1/exercises/${exerciseId}/sets`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
weight: formData.get('weight'),
reps: formData.get('reps')
})
})
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Failed to add set');
}
})
.then(data => {
alert('세트가 추가되었습니다.');
dialog.close();
window.location.reload(); // 페이지 새로고침
})
.catch(error => {
console.error('Error:', error);
alert('세트 추가 중 오류가 발생했습니다.');
});
});
}
document.getElementById('addMoreExercisesButton').addEventListener('click', function() {
const date = this.getAttribute('data-date');
window.location.href = `/wsession/${date}/addExercise`;
});
function deleteSet(setId) {
fetch(`/api/v1/exercises/sets/${setId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => {
if (response.ok) {
window.location.reload();
} else if (response.status === 401) {
alert('Unauthorized. Please login again.');
window.location.href = '/login';
} else {
response.text().then(text => alert('Error deleting set: ' + text));
}
})
.catch(error => console.error('Error:', error));
}
function deleteExercise(exerciseId) {
fetch(`/api/v1/exercises/${exerciseId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => {
if (response.ok) {
alert('운동이 삭제되었습니다.');
window.location.reload();
} else if (response.status === 401) {
alert('Unauthorized. Please login again.');
window.location.href = '/login';
} else {
response.text().then(text => alert('Error deleting set: ' + text));
}
})
.catch(error => console.error('Error:', error));
}
</script>
</body>
</html>
원하는 날짜를 클릭하면 이동하는 페이지로 해당 날짜에 운동세션이 존재한다면 viewSession.html이 보여진다. 종목별 삭제버튼과 세트추가버튼이 있다. 자바스크립트로 해당하는 메서드에 매핑했고, 세트추가하면 세트추가 창도 따로 만들어줬다. 아래는 결과.
설명할게 딱히 없어서 이정도로 끝냈지만, 나름 view단 만들면서 frontend의 고충을 느꼈고 재밌는 작업이었다.
작업중에 버튼 css예쁜게 없나 구글링해서 마우스를 올려놓으면 반짝거리는원이 돌아가는 css를 적용도해봤다.
처음해봤는데 되게 재밌었던 작업이었다.