[#22] 프론트엔드 React 전환 및 인증 API 수정 #23

Merged
pricelees merged 9 commits from refactor/#22 into main 2025-07-27 03:39:20 +00:00
31 changed files with 2 additions and 2713 deletions
Showing only changes of commit 41def25709 - Show all commits

View File

@ -32,7 +32,6 @@ repositories {
dependencies {
// Spring
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
@ -71,8 +70,8 @@ tasks.withType<Test> {
tasks.withType<KotlinCompile> {
compilerOptions {
freeCompilerArgs.addAll(
"-Xjsr305=strict",
"-Xannotation-default-target=param-property"
"-Xjsr305=strict",
"-Xannotation-default-target=param-property"
)
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}

View File

@ -1,48 +0,0 @@
package roomescape.view
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import roomescape.auth.web.support.Admin
import roomescape.auth.web.support.LoginRequired
@Controller
class AuthPageController {
@GetMapping("/login")
fun showLoginPage(): String = "login"
}
@Controller
@RequestMapping("/admin")
class AdminPageController {
@Admin
@GetMapping
fun showIndexPage() = "admin/index"
@Admin
@GetMapping("/{page}")
fun showAdminSubPage(@PathVariable page: String) = when (page) {
"reservation" -> "admin/reservation-new"
"time" -> "admin/time"
"theme" -> "admin/theme"
"waiting" -> "admin/waiting"
else -> "admin/index"
}
}
@Controller
class ClientPageController {
@GetMapping("/")
fun showPopularThemePage(): String = "index"
@LoginRequired
@GetMapping("/reservation")
fun showReservationPage(): String = "reservation"
@LoginRequired
@GetMapping("/reservation-mine")
fun showReservationMinePage(): String = "reservation-mine"
}

View File

@ -1,15 +0,0 @@
.disabled {
pointer-events: none;
opacity: 0.6;
}
#theme-slots .theme-slot.active, #time-slots .time-slot.active {
background-color: #0a3711 !important;
color: white;
}
#time-slots .time-slot.disabled {
background-color: #cccccc;
color: #666666;
cursor: not-allowed;
}

View File

@ -1,62 +0,0 @@
.profile-image {
height: 30px;
width: 30px;
border-radius: 50%;
margin-right: 5px; /* 이름과의 간격 조정 */
}
.nav-item .dropdown-toggle::after {
display: none; /* 드롭다운 화살표 제거 */
}
.nav-item {
margin-right: 10px; /* 네비게이션 간격 조정 */
}
.content-container {
width: 70%;
margin: 50px auto;
}
.content-container-title {
text-align: center;
margin-bottom: 30px;
}
.form-group input {
width: 100%;
padding: 10px;
margin: 10px 0;
border-radius: 5px;
border: 1px solid #ddd;
}
/* Solid 버튼 */
.btn-custom {
background-color: #0a3711; /* 버튼 기본 배경색 */
color: white; /* 버튼 텍스트 색상 */
border: 1px solid #0a3711; /* 테두리 색상 일치 */
}
.btn-custom:hover {
background-color: #083d0f; /* 호버 상태에서의 배경색 */
color: white; /* 호버 상태에서의 텍스트 색상 */
border: 1px solid #083d0f; /* 호버 상태에서의 테두리 색상 */
}
/* Outline 버튼 */
.btn-outline-custom {
background-color: transparent; /* 버튼 기본 배경색 투명 */
color: #0a3711; /* 버튼 텍스트 색상 */
border: 1px solid #0a3711; /* 테두리 색상 */
}
.btn-outline-custom:hover {
background-color: #0a3711; /* 호버 상태에서의 배경색 */
color: white; /* 호버 상태에서의 텍스트 색상 */
border: 1px solid #0a3711; /* 호버 상태에서의 테두리 색상 유지 */
}
.cursor-pointer {
cursor: pointer;
}

View File

@ -1,132 +0,0 @@
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
a {
text-decoration: none;
text-align: center;
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
overflow: auto;
}
.max-w-540 {
max-width: 540px;
}
.btn-wrapper {
padding: 0 24px;
}
.btn {
padding: 11px 22px;
border: none;
border-radius: 8px;
background-color: #f2f4f6;
color: #4e5968;
font-weight: 600;
font-size: 17px;
cursor: pointer;
}
.btn.primary {
background-color: #3282f6;
color: #f9fcff;
}
.text-center {
text-align: center;
}
.flex {
display: flex;
}
.flex-column {
display: flex;
flex-direction: column;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.align-center {
align-items: center;
}
.confirm-loading {
margin-top: 72px;
height: 400px;
justify-content: space-between;
}
.confirm-success {
display: none;
margin-top: 72px;
}
.button-group {
margin-top: 32px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
}
.title {
margin-top: 32px;
margin-bottom: 0;
color: #191f28;
font-weight: bold;
font-size: 24px;
}
.description {
margin-top: 8px;
color: #4e5968;
font-size: 17px;
font-weight: 500;
}
.response-section {
margin-top: 60px;
display: flex;
flex-direction: column;
gap: 16px;
font-size: 20px;
}
.response-section .response-label {
font-weight: 600;
color: #333d48;
font-size: 17px;
}
.response-section .response-text {
font-weight: 500;
color: #4e5968;
font-size: 17px;
padding-left: 16px;
word-break: break-word;
text-align: right;
}
.color-grey {
color: #b0b8c1;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,45 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
requestRead(`/themes/most-reserved-last-week?count=10`) // 인기 테마 목록 조회 API endpoint
.then(render)
.catch(error => console.error('Error fetching times:', error));
});
function formatDate(dateString) {
let date = new Date(dateString);
let year = date.getFullYear();
let month = (date.getMonth() + 1).toString().padStart(2, '0'); // '04'
let day = date.getDate().toString().padStart(2, '0'); // '28'
return `${year}-${month}-${day}`; // '2024-04-28'
}
function render(data) {
const container = document.getElementById('theme-ranking');
data.data.themes.forEach(theme => {
const name = theme.name;
const thumbnail = theme.thumbnail;
const description = theme.description;
const htmlContent = `
<img class="mr-3 img-thumbnail" src="${thumbnail}" alt="${name}">
<div class="media-body">
<h5 class="mt-0 mb-1">${name}</h5>
${description}
</div>
`;
const div = document.createElement('li');
div.className = 'media my-4';
div.innerHTML = htmlContent;
container.appendChild(div);
})
}
function requestRead(endpoint) {
return fetch(endpoint)
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
});
}

View File

@ -1,57 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
fetch('/reservations-mine') // 내 예약 목록 조회 API 호출
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
})
.then(render)
.catch(error => console.error('Error fetching reservations:', error));
});
function render(data) {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
data.data.reservations.forEach(item => {
const row = tableBody.insertRow();
const theme = item.themeName;
const date = item.date;
const time = item.time;
const status = item.status.includes('CONFIRMED') ? (item.status === 'CONFIRMED' ? '예약' : '예약 - 결제 필요') : item.rank + '번째 예약 대기';
row.insertCell(0).textContent = theme;
row.insertCell(1).textContent = date;
row.insertCell(2).textContent = time;
row.insertCell(3).textContent = status;
if (status.includes('대기')) { // 예약 대기 상태일 때 예약 대기 취소 버튼 추가하는 코드, 상태 값은 변경 가능
const cancelCell = row.insertCell(4);
const cancelButton = document.createElement('button');
cancelButton.textContent = '취소';
cancelButton.className = 'btn btn-danger';
cancelButton.onclick = function () {
requestDeleteWaiting(item.id).then(() => window.location.reload());
};
cancelCell.appendChild(cancelButton);
} else { // 예약 완료 상태일 때
/*
TODO: [미션4 - 2단계] 예약 목록 조회 ,
예약 완료 상태일 결제 정보를 함께 보여주기
결제 정보 필드명은 자신의 response 맞게 변경하기
*/
row.insertCell(4).textContent = '';
row.insertCell(5).textContent = item.paymentKey;
row.insertCell(6).textContent = item.amount;
}
});
}
function requestDeleteWaiting(id) {
const endpoint = '/reservations/waiting/' + id;
return fetch(endpoint, {
method: 'DELETE'
}).then(response => {
if (response.status === 204) return;
throw new Error('Delete failed');
});
}

View File

@ -1,194 +0,0 @@
let isEditing = false;
const RESERVATION_API_ENDPOINT = '/reservations';
const TIME_API_ENDPOINT = '/times';
const THEME_API_ENDPOINT = '/themes';
const timesOptions = [];
const themesOptions = [];
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('add-button').addEventListener('click', addInputRow);
requestRead(RESERVATION_API_ENDPOINT)
.then(render)
.catch(error => console.error('Error fetching reservations:', error));
fetchTimes();
fetchThemes();
});
function render(data) {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
data.data.reservations.forEach(item => {
const row = tableBody.insertRow();
row.insertCell(0).textContent = item.id; // 예약 id
row.insertCell(1).textContent = item.name; // 예약자명
row.insertCell(2).textContent = item.theme.name; // 테마명
row.insertCell(3).textContent = item.date; // 예약 날짜
row.insertCell(4).textContent = item.time.startAt; // 시작 시간
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow));
});
}
function fetchTimes() {
requestRead(TIME_API_ENDPOINT)
.then(data => {
timesOptions.push(...data.data.times);
})
.catch(error => console.error('Error fetching time:', error));
}
function fetchThemes() {
requestRead(THEME_API_ENDPOINT)
.then(data => {
themesOptions.push(...data.data.themes);
})
.catch(error => console.error('Error fetching theme:', error));
}
function createSelect(options, defaultText, selectId, textProperty) {
const select = document.createElement('select');
select.className = 'form-control';
select.id = selectId;
// 기본 옵션 추가
const defaultOption = document.createElement('option');
defaultOption.textContent = defaultText;
select.appendChild(defaultOption);
// 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성
options.forEach(optionData => {
const option = document.createElement('option');
option.value = optionData.id;
option.textContent = optionData[textProperty]; // 동적 속성 접근
select.appendChild(option);
});
return select;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function addInputRow() {
if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음
const tableBody = document.getElementById('table-body');
const row = tableBody.insertRow();
isEditing = true;
const nameInput = createInput('text');
const dateInput = createInput('date');
const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt');
const themeDropdown = createSelect(themesOptions, "테마 선택", 'theme-select', 'name');
const cellFieldsToCreate = ['', nameInput, themeDropdown, dateInput, timeDropdown];
cellFieldsToCreate.forEach((field, index) => {
const cell = row.insertCell(index);
if (typeof field === 'string') {
cell.textContent = field;
} else {
cell.appendChild(field);
}
});
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow));
actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => {
row.remove();
isEditing = false;
}));
}
function createInput(type) {
const input = document.createElement('input');
input.type = type;
input.className = 'form-control';
return input;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function saveRow(event) {
// 이벤트 전파를 막는다
event.stopPropagation();
const row = event.target.parentNode.parentNode;
const nameInput = row.querySelector('input[type="text"]');
const themeSelect = row.querySelector('#theme-select');
const dateInput = row.querySelector('input[type="date"]');
const timeSelect = row.querySelector('#time-select');
const reservation = {
name: nameInput.value,
themeId: themeSelect.value,
date: dateInput.value,
timeId: timeSelect.value
};
requestCreate(reservation)
.then(() => {
location.reload();
})
.catch(error => console.error('Error:', error));
isEditing = false; // isEditing 값을 false로 설정
}
function deleteRow(event) {
const row = event.target.closest('tr');
const reservationId = row.cells[0].textContent;
requestDelete(reservationId)
.then(() => row.remove())
.catch(error => console.error('Error:', error));
}
function requestCreate(reservation) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(reservation)
};
return fetch(RESERVATION_API_ENDPOINT, requestOptions)
.then(response => {
if (response.status === 201) return response.json();
throw new Error('Create failed');
});
}
function requestDelete(id) {
const requestOptions = {
method: 'DELETE',
};
return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions)
.then(response => {
if (response.status !== 204) throw new Error('Delete failed');
});
}
function requestRead(endpoint) {
return fetch(endpoint)
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
});
}

View File

@ -1,250 +0,0 @@
let isEditing = false;
const RESERVATION_API_ENDPOINT = '/reservations';
const TIME_API_ENDPOINT = '/times';
const THEME_API_ENDPOINT = '/themes';
const MEMBER_API_ENDPOINT = '/members';
const timesOptions = [];
const themesOptions = [];
const membersOptions = [];
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('add-button').addEventListener('click', addInputRow);
document.getElementById('filter-form').addEventListener('submit', applyFilter);
requestRead(RESERVATION_API_ENDPOINT)
.then(render)
.catch(error => console.error('Error fetching reservations:', error));
fetchTimes();
fetchThemes();
fetchMembers();
});
function render(data) {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
data.data.reservations.forEach(item => {
const row = tableBody.insertRow();
const isPaid = item.status === 'CONFIRMED' ? '결제 완료' : '결제 대기';
row.insertCell(0).textContent = item.id; // 예약 id
row.insertCell(1).textContent = item.member.name; // 사용자 name
row.insertCell(2).textContent = item.theme.name; // 테마 name
row.insertCell(3).textContent = item.date; // date
row.insertCell(4).textContent = item.time.startAt; // 예약 시간 startAt
row.insertCell(5).textContent = isPaid; // 결제
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow));
});
}
function fetchTimes() {
requestRead(TIME_API_ENDPOINT)
.then(data => {
timesOptions.push(...data.data.times);
})
.catch(error => console.error('Error fetching time:', error));
}
function fetchThemes() {
requestRead(THEME_API_ENDPOINT)
.then(data => {
themesOptions.push(...data.data.themes);
populateSelect('theme', themesOptions, 'name');
})
.catch(error => console.error('Error fetching theme:', error));
}
function fetchMembers() {
requestRead(MEMBER_API_ENDPOINT)
.then(data => {
membersOptions.push(...data.data.members);
populateSelect('member', membersOptions, 'name');
})
.catch(error => console.error('Error fetching member:', error));
}
function populateSelect(selectId, options, textProperty) {
const select = document.getElementById(selectId);
options.forEach(optionData => {
const option = document.createElement('option');
option.value = optionData.id;
option.textContent = optionData[textProperty];
select.appendChild(option);
});
}
function createSelect(options, defaultText, selectId, textProperty) {
const select = document.createElement('select');
select.className = 'form-control';
select.id = selectId;
// 기본 옵션 추가
const defaultOption = document.createElement('option');
defaultOption.textContent = defaultText;
select.appendChild(defaultOption);
// 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성
options.forEach(optionData => {
const option = document.createElement('option');
option.value = optionData.id;
option.textContent = optionData[textProperty]; // 동적 속성 접근
select.appendChild(option);
});
return select;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function addInputRow() {
if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음
const tableBody = document.getElementById('table-body');
const row = tableBody.insertRow();
isEditing = true;
const dateInput = createInput('date');
const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt');
const themeDropdown = createSelect(themesOptions, "테마 선택", 'theme-select', 'name');
const memberDropdown = createSelect(membersOptions, "멤버 선택", 'member-select', 'name');
const cellFieldsToCreate = ['', memberDropdown, themeDropdown, dateInput, timeDropdown];
cellFieldsToCreate.forEach((field, index) => {
const cell = row.insertCell(index);
if (typeof field === 'string') {
cell.textContent = field;
} else {
cell.appendChild(field);
}
});
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow));
actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => {
row.remove();
isEditing = false;
}));
}
function createInput(type) {
const input = document.createElement('input');
input.type = type;
input.className = 'form-control';
return input;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function saveRow(event) {
// 이벤트 전파를 막는다
event.stopPropagation();
const row = event.target.parentNode.parentNode;
const dateInput = row.querySelector('input[type="date"]');
const memberSelect = row.querySelector('#member-select');
const themeSelect = row.querySelector('#theme-select');
const timeSelect = row.querySelector('#time-select');
const reservation = {
date: dateInput.value,
themeId: themeSelect.value,
timeId: timeSelect.value,
memberId: memberSelect.value,
};
requestCreate(reservation)
.then(() => {
location.reload();
})
.catch(error => console.error('Error:', error));
isEditing = false; // isEditing 값을 false로 설정
}
function deleteRow(event) {
const row = event.target.closest('tr');
const reservationId = row.cells[0].textContent;
requestDelete(reservationId)
.then(() => row.remove())
.catch(error => console.error('Error:', error));
}
function applyFilter(event) {
event.preventDefault();
const themeId = document.getElementById('theme').value;
const memberId = document.getElementById('member').value;
const dateFrom = document.getElementById('date-from').value;
const dateTo = document.getElementById('date-to').value;
const queryParams = {
themeId: themeId,
memberId: memberId,
dateFrom: dateFrom,
dateTo: dateTo
}
const searchParams = new URLSearchParams(queryParams);
const endpoint = '/reservations/search';
const url = `${endpoint}?${searchParams.toString()}`;
fetch(url, { // 예약 검색 API 호출
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
}).then(render)
.catch(error => console.error("Error fetching available times:", error));
}
function requestCreate(reservation) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(reservation)
};
return fetch('/reservations/admin', requestOptions)
.then(response => {
if (response.status === 201) return response.json();
throw new Error('Create failed');
});
}
function requestDelete(id) {
const requestOptions = {
method: 'DELETE',
};
return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions)
.then(response => {
if (response.status !== 204) throw new Error('Delete failed');
});
}
function requestRead(endpoint) {
return fetch(endpoint)
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
});
}

View File

@ -1,179 +0,0 @@
let isEditing = false;
const RESERVATION_API_ENDPOINT = '/reservations';
const TIME_API_ENDPOINT = '/times';
const timesOptions = [];
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('add-button').addEventListener('click', addInputRow);
requestRead(RESERVATION_API_ENDPOINT)
.then(render)
.catch(error => console.error('Error fetching reservations:', error));
fetchTimes();
});
function render(data) {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
data.data.reservations.forEach(item => {
const row = tableBody.insertRow();
row.insertCell(0).textContent = item.id;
row.insertCell(1).textContent = item.name;
row.insertCell(2).textContent = item.date;
row.insertCell(3).textContent = item.time.startAt;
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow));
});
}
function fetchTimes() {
requestRead(TIME_API_ENDPOINT)
.then(data => {
timesOptions.push(...data.data.times);
})
.catch(error => console.error('Error fetching time:', error));
}
function createSelect(options, defaultText, selectId, textProperty) {
const select = document.createElement('select');
select.className = 'form-control';
select.id = selectId;
// 기본 옵션 추가
const defaultOption = document.createElement('option');
defaultOption.textContent = defaultText;
select.appendChild(defaultOption);
// 넘겨받은 옵션을 바탕으로 드롭다운 메뉴 아이템 생성
options.forEach(optionData => {
const option = document.createElement('option');
option.value = optionData.id;
option.textContent = optionData[textProperty]; // 동적 속성 접근
select.appendChild(option);
});
return select;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function addInputRow() {
if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음
const tableBody = document.getElementById('table-body');
const row = tableBody.insertRow();
isEditing = true;
const nameInput = createInput('text');
const dateInput = createInput('date');
const timeDropdown = createSelect(timesOptions, "시간 선택", 'time-select', 'startAt');
const cellFieldsToCreate = ['', nameInput, dateInput, timeDropdown];
cellFieldsToCreate.forEach((field, index) => {
const cell = row.insertCell(index);
if (typeof field === 'string') {
cell.textContent = field;
} else {
cell.appendChild(field);
}
});
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow));
actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => {
row.remove();
isEditing = false;
}));
}
function createInput(type) {
const input = document.createElement('input');
input.type = type;
input.className = 'form-control';
return input;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function saveRow(event) {
// 이벤트 전파를 막는다
event.stopPropagation();
const row = event.target.parentNode.parentNode;
const nameInput = row.querySelector('input[type="text"]');
const dateInput = row.querySelector('input[type="date"]');
const timeSelect = row.querySelector('select');
const reservation = {
name: nameInput.value,
date: dateInput.value,
timeId: timeSelect.value
};
requestCreate(reservation)
.then(() => {
location.reload();
})
.catch(error => console.error('Error:', error));
isEditing = false; // isEditing 값을 false로 설정
}
function deleteRow(event) {
const row = event.target.closest('tr');
const reservationId = row.cells[0].textContent;
requestDelete(reservationId)
.then(() => row.remove())
.catch(error => console.error('Error:', error));
}
function requestCreate(reservation) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(reservation)
};
return fetch(RESERVATION_API_ENDPOINT, requestOptions)
.then(response => {
if (response.status === 201) return response.json();
throw new Error('Create failed');
});
}
function requestDelete(id) {
const requestOptions = {
method: 'DELETE',
};
return fetch(`${RESERVATION_API_ENDPOINT}/${id}`, requestOptions)
.then(response => {
if (response.status !== 204) throw new Error('Delete failed');
});
}
function requestRead(endpoint) {
return fetch(endpoint)
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
});
}

View File

@ -1,136 +0,0 @@
let isEditing = false;
const API_ENDPOINT = '/themes';
const cellFields = ['id', 'name', 'description', 'thumbnail'];
const createCellFields = ['', createInput(), createInput(), createInput()];
function createBody(inputs) {
return {
name: inputs[0].value,
description: inputs[1].value,
thumbnail: inputs[2].value,
};
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('add-button').addEventListener('click', addRow);
requestRead()
.then(render)
.catch(error => console.error('Error fetching times:', error));
});
function render(data) {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
data.data.themes.forEach(item => {
const row = tableBody.insertRow();
cellFields.forEach((field, index) => {
row.insertCell(index).textContent = item[field];
});
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow));
});
}
function addRow() {
if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음
const tableBody = document.getElementById('table-body');
const row = tableBody.insertRow();
isEditing = true;
createAddField(row);
}
function createAddField(row) {
createCellFields.forEach((field, index) => {
const cell = row.insertCell(index);
if (typeof field === 'string') {
cell.textContent = field;
} else {
cell.appendChild(field);
}
});
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow));
actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => {
row.remove();
isEditing = false;
}));
}
function createInput() {
const input = document.createElement('input');
input.className = 'form-control';
return input;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function saveRow(event) {
const row = event.target.parentNode.parentNode;
const inputs = row.querySelectorAll('input');
const body = createBody(inputs);
requestCreate(body)
.then(() => {
location.reload();
})
.catch(error => console.error('Error:', error));
isEditing = false; // isEditing 값을 false로 설정
}
function deleteRow(event) {
const row = event.target.closest('tr');
const id = row.cells[0].textContent;
requestDelete(id)
.then(() => row.remove())
.catch(error => console.error('Error:', error));
}
// request
function requestCreate(data) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
};
return fetch(API_ENDPOINT, requestOptions)
.then(response => {
if (response.status === 201) return response.json();
throw new Error('Create failed');
});
}
function requestRead() {
return fetch(API_ENDPOINT)
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
});
}
function requestDelete(id) {
const requestOptions = {
method: 'DELETE',
};
return fetch(`${API_ENDPOINT}/${id}`, requestOptions)
.then(response => {
if (response.status !== 204) throw new Error('Delete failed');
});
}

View File

@ -1,135 +0,0 @@
let isEditing = false;
const API_ENDPOINT = '/times';
const cellFields = ['id', 'startAt'];
const createCellFields = ['', createInput()];
function createBody(inputs) {
return {
startAt: inputs[0].value,
};
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('add-button').addEventListener('click', addRow);
requestRead()
.then(render)
.catch(error => console.error('Error fetching times:', error));
});
function render(data) {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
data.data.times.forEach(item => {
const row = tableBody.insertRow();
cellFields.forEach((field, index) => {
row.insertCell(index).textContent = item[field];
});
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('삭제', 'btn-danger', deleteRow));
});
}
function addRow() {
if (isEditing) return; // 이미 편집 중인 경우 추가하지 않음
const tableBody = document.getElementById('table-body');
const row = tableBody.insertRow();
isEditing = true;
createAddField(row);
}
function createAddField(row) {
createCellFields.forEach((field, index) => {
const cell = row.insertCell(index);
if (typeof field === 'string') {
cell.textContent = field;
} else {
cell.appendChild(field);
}
});
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('확인', 'btn-custom', saveRow));
actionCell.appendChild(createActionButton('취소', 'btn-secondary', () => {
row.remove();
isEditing = false;
}));
}
function createInput() {
const input = document.createElement('input');
input.type = 'time'
input.className = 'form-control';
return input;
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}
function saveRow(event) {
const row = event.target.parentNode.parentNode;
const inputs = row.querySelectorAll('input');
const body = createBody(inputs);
requestCreate(body)
.then(() => {
location.reload();
})
.catch(error => console.error('Error:', error));
isEditing = false; // isEditing 값을 false로 설정
}
function deleteRow(event) {
const row = event.target.closest('tr');
const id = row.cells[0].textContent;
requestDelete(id)
.then(() => row.remove())
.catch(error => console.error('Error:', error));
}
// request
function requestCreate(data) {
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
};
return fetch(API_ENDPOINT, requestOptions)
.then(response => {
if (response.status === 201) return response.json();
throw new Error('Create failed');
});
}
function requestRead() {
return fetch(API_ENDPOINT)
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
});
}
function requestDelete(id) {
const requestOptions = {
method: 'DELETE',
};
return fetch(`${API_ENDPOINT}/${id}`, requestOptions)
.then(response => {
if (response.status !== 204) throw new Error('Delete failed');
});
}

View File

@ -1,273 +0,0 @@
const THEME_API_ENDPOINT = '/themes';
document.addEventListener('DOMContentLoaded', () => {
requestRead(THEME_API_ENDPOINT)
.then(renderTheme)
.catch(error => console.error('Error fetching times:', error));
flatpickr("#datepicker", {
inline: true,
onChange: function (selectedDates, dateStr, instance) {
if (dateStr === '') return;
checkDate();
}
});
// ------ 결제위젯 초기화 ------
// @docs https://docs.tosspayments.com/reference/widget-sdk#sdk-설치-및-초기화
// @docs https://docs.tosspayments.com/reference/widget-sdk#renderpaymentmethods선택자-결제-금액-옵션
const paymentAmount = 1000;
const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm";
const paymentWidget = PaymentWidget(widgetClientKey, PaymentWidget.ANONYMOUS);
paymentWidget.renderPaymentMethods(
"#payment-method",
{value: paymentAmount},
{variantKey: "DEFAULT"}
);
document.getElementById('theme-slots').addEventListener('click', event => {
if (event.target.classList.contains('theme-slot')) {
document.querySelectorAll('.theme-slot').forEach(slot => slot.classList.remove('active'));
event.target.classList.add('active');
checkDateAndTheme();
}
});
document.getElementById('time-slots').addEventListener('click', event => {
if (event.target.classList.contains('time-slot') && !event.target.classList.contains('disabled')) {
document.querySelectorAll('.time-slot').forEach(slot => slot.classList.remove('active'));
event.target.classList.add('active');
checkDateAndThemeAndTime();
}
});
document.getElementById('reserve-button').addEventListener('click', onReservationButtonClickWithPaymentWidget);
document.getElementById('wait-button').addEventListener('click', onWaitButtonClick);
function onReservationButtonClickWithPaymentWidget(event) {
onReservationButtonClick(event, paymentWidget);
}
});
function renderTheme(themes) {
const themeSlots = document.getElementById('theme-slots');
themeSlots.innerHTML = '';
themes.data.themes.forEach(theme => {
const name = theme.name;
const themeId = theme.id;
themeSlots.appendChild(createSlot('theme', name, themeId));
});
}
function createSlot(type, text, id, booked) {
const div = document.createElement('div');
div.className = type + '-slot cursor-pointer bg-light border rounded p-3 mb-2';
div.textContent = text;
div.setAttribute('data-' + type + '-id', id);
if (type === 'time') {
div.setAttribute('data-time-booked', booked);
}
return div;
}
function checkDate() {
const selectedDate = document.getElementById("datepicker").value;
if (selectedDate) {
const themeSection = document.getElementById("theme-section");
if (themeSection.classList.contains("disabled")) {
themeSection.classList.remove("disabled");
}
const timeSlots = document.getElementById('time-slots');
timeSlots.innerHTML = '';
requestRead(THEME_API_ENDPOINT)
.then(renderTheme)
.catch(error => console.error('Error fetching times:', error));
}
}
function checkDateAndTheme() {
const selectedDate = document.getElementById("datepicker").value;
const selectedThemeElement = document.querySelector('.theme-slot.active');
if (selectedDate && selectedThemeElement) {
const selectedThemeId = selectedThemeElement.getAttribute('data-theme-id');
fetchAvailableTimes(selectedDate, selectedThemeId);
}
}
function fetchAvailableTimes(date, themeId) {
fetch(`/times/search?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
}).then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
}).then(renderAvailableTimes)
.catch(error => console.error("Error fetching available times:", error));
}
function renderAvailableTimes(times) {
const timeSection = document.getElementById("time-section");
if (timeSection.classList.contains("disabled")) {
timeSection.classList.remove("disabled");
}
const timeSlots = document.getElementById('time-slots');
timeSlots.innerHTML = '';
if (times.length === 0) {
timeSlots.innerHTML = '<div class="no-times">선택할 수 있는 시간이 없습니다.</div>';
return;
}
times.data.times.forEach(time => {
const startAt = time.startAt;
const timeId = time.id;
const isAvailable = time.isAvailable;
const div = createSlot('time', startAt, timeId, isAvailable); // createSlot('time', 시작 시간, time id, 예약 여부)
timeSlots.appendChild(div);
});
}
function checkDateAndThemeAndTime() {
const selectedDate = document.getElementById("datepicker").value;
const selectedThemeElement = document.querySelector('.theme-slot.active');
const selectedTimeElement = document.querySelector('.time-slot.active');
const reserveButton = document.getElementById("reserve-button");
const waitButton = document.getElementById("wait-button");
if (selectedDate && selectedThemeElement && selectedTimeElement) {
if (selectedTimeElement.getAttribute('data-time-booked') === 'false') {
// 선택된 시간이 이미 예약된 경우
reserveButton.classList.add("disabled");
waitButton.classList.remove("disabled"); // 예약 대기 버튼 활성화
} else {
// 선택된 시간이 예약 가능한 경우
reserveButton.classList.remove("disabled");
waitButton.classList.add("disabled"); // 예약 대기 버튼 활성화
}
} else {
// 날짜, 테마, 시간 중 하나라도 선택되지 않은 경우
reserveButton.classList.add("disabled");
waitButton.classList.add("disabled");
}
}
function onReservationButtonClick(event, paymentWidget) {
const selectedDate = document.getElementById("datepicker").value;
const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id');
const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id');
if (selectedDate && selectedThemeId && selectedTimeId) {
const reservationData = {
date: selectedDate,
themeId: selectedThemeId,
timeId: selectedTimeId,
};
const generateRandomString = () =>
window.btoa(Math.random()).slice(0, 20);
// TOSS 결제 위젯 Javascript SDK 연동 방식 중 'Promise로 처리하기'를 적용함
// https://docs.tosspayments.com/reference/widget-sdk#promise%EB%A1%9C-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0
const orderIdPrefix = "WTEST";
paymentWidget.requestPayment({
orderId: orderIdPrefix + generateRandomString(),
orderName: "테스트 방탈출 예약 결제 1건",
amount: 1000,
}).then(function (data) {
console.debug(data);
fetchReservationPayment(data, reservationData);
}).catch(function (error) {
// TOSS 에러 처리: 에러 목록을 확인하세요
// https://docs.tosspayments.com/reference/error-codes#failurl 로-전달되는-에러
alert(error.code + " :" + error.message);
});
} else {
alert("Please select a date, theme, and time before making a reservation.");
}
}
async function fetchReservationPayment(paymentData, reservationData) {
const reservationPaymentRequest = {
date: reservationData.date,
themeId: reservationData.themeId,
timeId: reservationData.timeId,
paymentKey: paymentData.paymentKey,
orderId: paymentData.orderId,
amount: paymentData.amount,
paymentType: paymentData.paymentType,
}
const reservationURL = "/reservations";
fetch(reservationURL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reservationPaymentRequest),
}).then(response => {
if (!response.ok) {
return response.json().then(errorBody => {
console.error("예약 결제 실패 : " + JSON.stringify(errorBody));
window.alert("예약 결제 실패 메시지: " + errorBody.message);
});
} else {
response.json().then(successBody => {
alert("예약이 완료되었습니다.");
console.log("예약 결제 성공 : " + JSON.stringify(successBody));
window.location.href = "/";
});
}
}).catch(error => {
console.error(error.message);
});
}
function onWaitButtonClick() {
const selectedDate = document.getElementById("datepicker").value;
const selectedThemeId = document.querySelector('.theme-slot.active')?.getAttribute('data-theme-id');
const selectedTimeId = document.querySelector('.time-slot.active')?.getAttribute('data-time-id');
if (selectedDate && selectedThemeId && selectedTimeId) {
const reservationData = {
date: selectedDate,
timeId: selectedTimeId,
themeId: selectedThemeId,
};
fetch('/reservations/waiting', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(reservationData)
})
.then(response => {
if (!response.ok) throw new Error('Reservation waiting failed');
return response.json();
})
.then(data => {
alert('Reservation waiting successful!');
window.location.href = "/";
})
.catch(error => {
alert("An error occurred while making the reservation waiting.");
console.error(error);
});
} else {
alert("Please select a date, theme, and time before making a reservation waiting.");
}
}
function requestRead(endpoint) {
return fetch(endpoint)
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
});
}

View File

@ -1,152 +0,0 @@
document.addEventListener('DOMContentLoaded', function () {
updateUIBasedOnLogin();
});
document.getElementById('logout-btn').addEventListener('click', function (event) {
event.preventDefault();
fetch('/logout', {
method: 'POST', // 또는 서버 설정에 따라 GET 일 수도 있음
credentials: 'include' // 쿠키를 포함시키기 위해 필요
})
.then(response => {
if (response.ok) {
// 로그아웃 성공, 페이지 새로고침 또는 리다이렉트
window.location.reload();
} else {
// 로그아웃 실패 처리
console.error('Logout failed');
}
})
.catch(error => {
console.error('Error:', error);
});
});
function updateUIBasedOnLogin() {
fetch('/login/check') // 로그인 상태 확인 API 호출
.then(response => {
if (!response.ok) { // 요청이 실패하거나 로그인 상태가 아닌 경우
throw new Error('Not logged in or other error');
}
return response.json(); // 응답 본문을 JSON으로 파싱
})
.then(data => {
// 응답에서 사용자 이름을 추출하여 UI 업데이트
document.getElementById('profile-name').textContent = data.data.name; // 프로필 이름 설정
document.querySelector('.nav-item.dropdown').style.display = 'block'; // 드롭다운 메뉴 표시
document.querySelector('.nav-item a[href="/login"]').parentElement.style.display = 'none'; // 로그인 버튼 숨김
})
.catch(error => {
// 에러 처리 또는 로그아웃 상태일 때 UI 업데이트
console.error('Error:', error);
document.getElementById('profile-name').textContent = 'Profile'; // 기본 텍스트로 재설정
document.querySelector('.nav-item.dropdown').style.display = 'none'; // 드롭다운 메뉴 숨김
document.querySelector('.nav-item a[href="/login"]').parentElement.style.display = 'block'; // 로그인 버튼 표시
});
}
// 드롭다운 메뉴 토글
document.getElementById("navbarDropdown").addEventListener('click', function (e) {
e.preventDefault();
const dropdownMenu = e.target.closest('.nav-item.dropdown').querySelector('.dropdown-menu');
dropdownMenu.classList.toggle('show'); // Bootstrap 4에서는 data-toggle 사용, Bootstrap 5에서는 JS로 처리
});
function login() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
// 입력 필드 검증
if (!email || !password) {
alert('Please fill in all fields.');
return; // 필수 입력 필드가 비어있으면 여기서 함수 실행을 중단
}
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
password: password
})
})
.then(response => {
if (200 !== response.status) {
alert('Login failed'); // 로그인 실패 시 경고창 표시
throw new Error('Login failed');
}
})
.then(() => {
updateUIBasedOnLogin(); // UI 업데이트
window.location.href = '/';
})
.catch(error => {
console.error('Error during login:', error);
});
}
function signup() {
// Redirect to signup page
window.location.href = '/signup';
}
function register(event) {
// 폼 데이터 수집
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const name = document.getElementById('name').value;
// 입력 필드 검증
if (!email || !password || !name) {
alert('Please fill in all fields.');
return; // 필수 입력 필드가 비어있으면 여기서 함수 실행을 중단
}
// 요청 데이터 포맷팅
const formData = {
email: email,
password: password,
name: name
};
// AJAX 요청 생성 및 전송
fetch('/members', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => {
if (!response.ok) {
alert('Signup request failed');
throw new Error('Signup request failed');
}
return response.json(); // 여기서 응답을 JSON 형태로 변환
})
.then(data => {
// 성공적인 응답 처리
console.log('Signup successful:', data);
window.location.href = '/login';
})
.catch(error => {
// 에러 처리
console.error('Error during signup:', error);
});
// 폼 제출에 의한 페이지 리로드 방지
event.preventDefault();
}
function base64DecodeUnicode(str) {
// Base64 디코딩
const decodedBytes = atob(str);
// UTF-8 바이트를 문자열로 변환
const encodedUriComponent = decodedBytes.split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join('');
return decodeURIComponent(encodedUriComponent);
}

View File

@ -1,69 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
fetch('/reservations/waiting') // 내 예약 목록 조회 API 호출
.then(response => {
if (response.status === 200) return response.json();
throw new Error('Read failed');
})
.then(render)
.catch(error => console.error('Error fetching reservations:', error));
});
function render(data) {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
data.data.reservations.forEach(item => {
const row = tableBody.insertRow();
const id = item.id;
const name = item.member.name;
const theme = item.theme.name;
const date = item.date;
const startAt = item.time.startAt;
row.insertCell(0).textContent = id; // 예약 대기 id
row.insertCell(1).textContent = name; // 예약자명
row.insertCell(2).textContent = theme; // 테마명
row.insertCell(3).textContent = date; // 예약 날짜
row.insertCell(4).textContent = startAt; // 시작 시간
const actionCell = row.insertCell(row.cells.length);
actionCell.appendChild(createActionButton('승인', 'btn-primary', approve));
actionCell.appendChild(createActionButton('거절', 'btn-danger', deny));
});
}
function approve(event) {
const row = event.target.closest('tr');
const id = row.cells[0].textContent;
const endpoint = `/reservations/waiting/${id}/confirm`
return fetch(endpoint, {
method: 'POST'
}).then(response => {
if (response.status === 200) return;
throw new Error('Delete failed');
}).then(() => location.reload());
}
function deny(event) {
const row = event.target.closest('tr');
const id = row.cells[0].textContent;
const endpoint = `/reservations/waiting/${id}/reject`
return fetch(endpoint, {
method: 'POST'
}).then(response => {
if (response.status === 204) return;
throw new Error('Delete failed');
}).then(() => location.reload());
}
function createActionButton(label, className, eventListener) {
const button = document.createElement('button');
button.textContent = label;
button.classList.add('btn', className, 'mr-2');
button.addEventListener('click', eventListener);
return button;
}

View File

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/admin">
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/waiting">Waiting</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/theme">Theme</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/time">Time</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">방탈출 어드민</h2>
</div>
<script src="/js/user-scripts.js"></script>
</body>
</html>

View File

@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/admin">
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/waiting">Waiting</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/theme">Theme</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/time">Time</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">방탈출 예약 페이지</h2>
<div class="d-flex">
<div class="table-container flex-grow-1 mr-3">
<div class="table-header d-flex justify-content-end">
<button id="add-button" class="btn btn-custom mb-2">예약 추가</button>
</div>
<table class="table">
<thead>
<tr>
<th>예약번호</th>
<th>예약자</th>
<th>테마</th>
<th>날짜</th>
<th>시간</th>
<th>결제 완료 여부</th>
<th></th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</div>
<div class="filter-section ml-3">
<form id="filter-form">
<div class="form-group">
<label for="member">예약자</label>
<select type="text" id="member" name="member" class="form-control">
<option value="">전체</option>
<option style="display:none"></option>
</select>
</div>
<div class="form-group">
<label for="theme">테마</label>
<select type="text" id="theme" name="theme" class="form-control">
<option value="">전체</option>
<option style="display:none"></option>
</select>
</div>
<div class="form-group">
<label for="date-from">From</label>
<input type="date" id="date-from" name="date-from" class="form-control">
</div>
<div class="form-group">
<label for="date-to">To</label>
<input type="date" id="date-to" name="date-to" class="form-control">
</div>
<button type="submit" class="btn btn-primary float-right">적용</button>
</form>
</div>
</div>
</div>
<script src="/js/user-scripts.js"></script>
<script src="/js/reservation-with-member.js"></script>
</body>
</html>

View File

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/admin">
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/waiting">Waiting</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/theme">Theme</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/time">Time</a>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">방탈출 예약 페이지</h2>
<div class="table-header">
<button id="add-button" class="btn btn-custom mb-2 float-right">예약 추가</button>
</div>
<div class="table-container"></div>
<table class="table">
<thead>
<tr>
<th>예약번호</th>
<th>예약자</th>
<th>날짜</th>
<th>시간</th>
<th></th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</div>
<script src="/js/reservation.js"></script>
</body>
</html>

View File

@ -1,80 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/admin">
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/waiting">Waiting</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/theme">Theme</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/time">Time</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">테마 관리 페이지</h2>
<div class="table-header">
<button id="add-button" class="btn btn-custom mb-2 float-right">테마 추가</button>
</div>
<div class="table-container"/>
<table class="table">
<thead>
<tr>
<th scope="col">순서</th>
<th scope="col">제목</th>
<th scope="col">설명</th>
<th scope="col">썸네일 URL</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</div>
<script src="/js/user-scripts.js"></script>
<script src="/js/theme.js"></script>
</body>
</html>

View File

@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/admin">
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/waiting">Waiting</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/theme">Theme</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/time">Time</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">시간 관리 페이지</h2>
<div class="table-header">
<button id="add-button" class="btn btn-custom mb-2 float-right">예약시간 추가</button>
</div>
<div class="table-container"/>
<table class="table">
<thead>
<tr>
<th scope="col">순서</th>
<th scope="col">시간</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</div>
<script src="/js/user-scripts.js"></script>
<script src="/js/time.js"></script>
</body>
</html>

View File

@ -1,77 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/admin">
<img src="/image/admin-logo.png" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/admin/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/waiting">Waiting</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/theme">Theme</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/time">Time</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">예약 대기 관리 페이지</h2>
<div class="table-container"/>
<table class="table">
<thead>
<tr>
<th scope="col">예약대기 번호</th>
<th scope="col">예약자</th>
<th scope="col">테마</th>
<th scope="col">날짜</th>
<th scope="col">시간</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</div>
<script src="/js/user-scripts.js"></script>
<script src="/js/waiting.js"></script>
</body>
</html>

View File

@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 예약 페이지</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">
<img src="https://avatars.githubusercontent.com/u/141792611?s=200&v=4" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">인기 테마</h2>
<ul class="list-unstyled" id="theme-ranking">
</ul>
</div>
<script src="/js/user-scripts.js"></script>
<script src="/js/ranking.js"></script>
</body>
</html>

View File

@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">
<img src="https://avatars.githubusercontent.com/u/141792611?s=200&v=4" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container" style="width: 300px;">
<h2 class="content-container-title">Login</h2>
<form id="login-form">
<div class="form-group">
<input type="email" class="form-control" id="email" placeholder="Email" required>
</div>
<div class="form-group">
<input type="password" class="form-control" id="password" placeholder="Password" required>
</div>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-outline-custom" onclick="signup()">Sign Up</button>
<button type="button" class="btn btn-custom" onclick="login()">Login</button>
</div>
</form>
</div>
<script src="/js/user-scripts.js"></script>
</body>
</html>

View File

@ -1,70 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 어드민</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">
<img src="https://avatars.githubusercontent.com/u/141792611?s=200&v=4" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container">
<h2 class="content-container-title">내 예약</h2>
<div class="table-container"></div>
<table class="table">
<thead>
<tr>
<th>테마</th>
<th>날짜</th>
<th>시간</th>
<th>상태</th>
<th>대기 취소</th>
<th>paymentKey</th>
<th>결제금액</th>
<th></th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</div>
<script src="/js/user-scripts.js"></script>
<script src="/js/reservation-mine.js"></script>
</body>
</html>

View File

@ -1,103 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>방탈출 예약 페이지</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link rel="stylesheet" href="/css/reservation.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/toss-style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">
<img src="https://avatars.githubusercontent.com/u/141792611?s=200&v=4" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<!-- Content -->
<div id="content-container" class="col-md-10 offset-md-1 p-5">
<h2 class="content-container-title">예약 페이지</h2>
<div class="d-flex" id="reservation-container">
<!-- Date Section -->
<div class="section border rounded col-md-4 p-3" id="date-section">
<h3 class="fs-5 text-center mb-3">날짜 선택</h3>
<div class="d-flex justify-content-center p-3">
<div id="datepicker"></div>
</div>
</div>
<!-- Theme Section -->
<div class="section border rounded col-md-4 p-3 disabled" id="theme-section">
<h3 class="fs-5 text-center mb-3">테마 선택</h3>
<div class="p-3" id="theme-slots">
<!-- Dynamically generated theme slots will go here -->
</div>
</div>
<!-- Time Section -->
<div class="section border rounded col-md-4 p-3 disabled" id="time-section">
<h3 class="fs-5 text-center mb-3">시간 선택</h3>
<div class="p-3" id="time-slots">
<!-- Dynamically generated time slots will go here -->
</div>
</div>
</div>
<!-- Reservation Button -->
<div class="button-group float-right">
<button id="wait-button" class="btn btn-secondary mt-3 disabled">예약대기</button>
</div>
</div>
</div>
<!-- 결제창 ui -->
<div class="wrapper w-100">
<div class="max-w-540 w-100">
<div id="payment-method" class="w-100"></div>
<div id="agreement" class="w-100"></div>
<div class="btn-wrapper w-100">
<button id="reserve-button" class="btn primary w-100">예약하기</button>
</div>
</div>
</div>
<script src="/js/user-scripts.js"></script>
<script src="https://js.tosspayments.com/v1/payment-widget"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="/js/user-reservation.js"></script>
</body>
</html>

View File

@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Signup</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">
<img src="https://avatars.githubusercontent.com/u/141792611?s=200&v=4" alt="LOGO" style="height: 40px;">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/reservation">Reservation</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<img class="profile-image" src="/image/default-profile.png" alt="Profile">
<span id="profile-name">Profile</span> <!-- 프로필 이름을 넣을 span 추가 -->
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/reservation-mine">My Reservation</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="logout-btn">Logout</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="content-container" style="width: 400px;">
<h2 class="content-container-title">Signup</h2>
<form id="signup-form">
<div class="form-group">
<label for="email">Email address</label>
<input type="email" class="form-control" id="email" placeholder="Enter email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" placeholder="Enter password">
</div>
<div class="form-group">
<label for="name">Name</label>
<input type="name" class="form-control" id="name" placeholder="Enter name">
</div>
<button type="submit" class="btn btn-custom" onclick="register()">Register</button>
</form>
</div>
<script src="/js/user-scripts.js"></script>
</body>
</html>

View File

@ -1,133 +0,0 @@
package roomescape.view
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import roomescape.util.RoomescapeApiTest
@WebMvcTest(controllers = [
AuthPageController::class,
AdminPageController::class,
ClientPageController::class
])
class PageControllerTest(
@Autowired private val mockMvc: MockMvc
) : RoomescapeApiTest() {
init {
listOf("/", "/login").forEach {
given("GET $it 요청은") {
`when`("로그인 및 권한 여부와 관계없이 성공한다.") {
then("비회원") {
doNotLogin()
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { isOk() }
}
}
then("회원") {
loginAsUser()
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { isOk() }
}
}
then("관리자") {
loginAsAdmin()
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { isOk() }
}
}
}
}
}
listOf("/admin", "/admin/reservation", "/admin/time", "/admin/theme", "/admin/waiting").forEach {
given("GET $it 요청을") {
`when`("관리자가 보내면") {
loginAsAdmin()
then("성공한다.") {
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { isOk() }
}
}
}
`when`("회원이 보내면") {
loginAsUser()
then("로그인 페이지로 이동한다.") {
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { is3xxRedirection() }
header {
string("Location", "/login")
}
}
}
}
}
}
listOf("/reservation", "/reservation-mine").forEach {
given("GET $it 요청을") {
`when`("로그인 된 회원이 보내면 성공한다.") {
then("회원") {
loginAsUser()
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { isOk() }
}
}
then("관리자") {
loginAsAdmin()
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { isOk() }
}
}
}
`when`("로그인 없이 보내면") {
then("로그인 페이지로 이동한다.") {
doNotLogin()
runGetTest(
mockMvc = mockMvc,
endpoint = it,
) {
status { is3xxRedirection() }
header {
string("Location", "/login")
}
}
}
}
}
}
}
}