diff --git a/src/main/java/roomescape/controller/PageController.java b/src/main/java/roomescape/controller/PageController.java new file mode 100644 index 00000000..0aa3145a --- /dev/null +++ b/src/main/java/roomescape/controller/PageController.java @@ -0,0 +1,17 @@ +package roomescape.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class PageController { + @GetMapping + public String home() { + return "admin/index"; + } + + @GetMapping("/admin/reservation") + public String reservation() { + return "admin/reservation-legacy"; + } +} diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java new file mode 100644 index 00000000..e4d3fb88 --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -0,0 +1,35 @@ +package roomescape.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import roomescape.model.Reservation; +import roomescape.service.ReservationService; + +import java.sql.PreparedStatement; +import java.util.List; + +@RestController +@RequestMapping("/reservations") +public class ReservationController { + private final ReservationService reservationService; + + @Autowired + public ReservationController(ReservationService reservationService) { + this.reservationService = reservationService; + } + + @PostMapping + public Long insert(@RequestBody Reservation reservation) { + return reservationService.addReservation(reservation); + } + + @GetMapping + public List read() { + return reservationService.lookUpReservation(); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable Long id) { + reservationService.deleteReservation(id); + } +} diff --git a/src/main/java/roomescape/controller/ReservationTimeController.java b/src/main/java/roomescape/controller/ReservationTimeController.java new file mode 100644 index 00000000..81245efc --- /dev/null +++ b/src/main/java/roomescape/controller/ReservationTimeController.java @@ -0,0 +1,35 @@ +package roomescape.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import roomescape.model.ReservationTime; +import roomescape.service.ReservationTimeService; + +import java.util.List; + +@RestController +@RequestMapping("/times") +public class ReservationTimeController { + + private ReservationTimeService reservationTimeService; + + @Autowired + public ReservationTimeController(ReservationTimeService reservationTimeService) { + this.reservationTimeService = reservationTimeService; + } + + @PostMapping + public ReservationTime insert(@RequestBody String startAt) { + return reservationTimeService.addReservation(startAt); + } + + @GetMapping + public List read() { + return reservationTimeService.lookUpReservationTime(); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable Long id) { + reservationTimeService.deleteReservation(id); + } +} diff --git a/src/main/java/roomescape/model/Reservation.java b/src/main/java/roomescape/model/Reservation.java new file mode 100644 index 00000000..b583cdae --- /dev/null +++ b/src/main/java/roomescape/model/Reservation.java @@ -0,0 +1,33 @@ +package roomescape.model; + +import java.util.concurrent.atomic.AtomicLong; + +public class Reservation { + private Long id; + private String name; + private String date; + private Long timeId; + + public Reservation(Long id, String name, String date, Long timeId) { + this.id = id; + this.name = name; + this.date = date; + this.timeId = timeId; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDate() { + return date; + } + + public Long getTimeId() { + return timeId; + } +} diff --git a/src/main/java/roomescape/model/ReservationTime.java b/src/main/java/roomescape/model/ReservationTime.java new file mode 100644 index 00000000..dbf4077f --- /dev/null +++ b/src/main/java/roomescape/model/ReservationTime.java @@ -0,0 +1,24 @@ +package roomescape.model; + +public class ReservationTime { + private Long id; + private String startAt; + + public ReservationTime(String startAt) { + this(0L, startAt); + } + + public ReservationTime(Long id, String startAt) { + this.id = id; + this.startAt = startAt; + } + + public Long getId() { + return id; + } + + public String getStartAt() { + return startAt; + } + +} diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java new file mode 100644 index 00000000..f6684cc2 --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -0,0 +1,62 @@ +package roomescape.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import roomescape.model.Reservation; + +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +public class ReservationRepository { + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert jdbcInsert; + private final RowMapper rowMapper = (resultSet, rowNum) -> new Reservation( + resultSet.getLong("id"), + resultSet.getString("name"), + resultSet.getString("date"), + resultSet.getLong("time_id") + ); + + @Autowired + public ReservationRepository(SimpleJdbcInsert jdbcInsert, JdbcTemplate jdbcTemplate, DataSource dataSource) { + this.jdbcTemplate = jdbcTemplate; + this.jdbcInsert = new SimpleJdbcInsert(dataSource) + .withTableName("reservation") + .usingGeneratedKeyColumns("id"); + } + + public Long save(Reservation reservation) { + Map params = new HashMap<>(); + params.put("id", reservation.getId()); + params.put("name", reservation.getName()); + params.put("date", reservation.getDate()); + params.put("time_id", reservation.getTimeId()); + + return jdbcInsert.executeAndReturnKey(params).longValue(); + } + + public List readAll() { + String sql = "SELECT \n" + + "r.id as reservation_id, \n" + + "r.name as reservation_name, \n" + + "r.date as reservation_date, \n" + + "t.id as time_id, \n" + + "t.start_at as time_start_at \n" + + "FROM reservation as r inner join reservation_time as t on r.time_id = t.id"; + return jdbcTemplate.query(sql, rowMapper); + } + + public void deleteById(Long id) { + String sql = "DELETE FROM reservation WHERE id = ?"; + jdbcTemplate.update(sql, id); + } +} diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java new file mode 100644 index 00000000..91d6072b --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -0,0 +1,50 @@ +package roomescape.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import roomescape.model.ReservationTime; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ReservationTimeRepository { + + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert jdbcInsert; + + private final RowMapper rowMapper = (resultSet, rowNum) -> new ReservationTime( + resultSet.getLong("id"), + resultSet.getString("start_at") + ); + + @Autowired + public ReservationTimeRepository(JdbcTemplate jdbcTemplate, SimpleJdbcInsert jdbcInsert, DataSource dataSource) { + this.jdbcTemplate = jdbcTemplate; + this.jdbcInsert = new SimpleJdbcInsert(dataSource) + .withTableName("reservation_time") + .usingGeneratedKeyColumns("id"); + } + + public ReservationTime save(String startAt) { + Map params = new HashMap<>(); + params.put("startAt", startAt); + return new ReservationTime(jdbcInsert.executeAndReturnKey(params).longValue(), startAt); + } + + public List readAll() { + String sql = "SELECT * FROM reservation_time"; + return jdbcTemplate.query(sql, rowMapper); + } + + public void deleteById(Long id) { + String sql = "DELETE FROM reservation_time WHERE id = ?"; + jdbcTemplate.update(sql, id); + } +} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java new file mode 100644 index 00000000..d326a4b7 --- /dev/null +++ b/src/main/java/roomescape/service/ReservationService.java @@ -0,0 +1,30 @@ +package roomescape.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import roomescape.model.Reservation; +import roomescape.repository.ReservationRepository; + +import java.util.List; + +@Service +public class ReservationService { + private final ReservationRepository reservationRepository; + + @Autowired + public ReservationService(ReservationRepository reservationRepository) { + this.reservationRepository = reservationRepository; + } + + public Long addReservation(Reservation reservation) { + return reservationRepository.save(reservation); + } + + public List lookUpReservation() { + return reservationRepository.readAll(); + } + + public void deleteReservation(Long id) { + reservationRepository.deleteById(id); + } +} diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java new file mode 100644 index 00000000..8db0aeca --- /dev/null +++ b/src/main/java/roomescape/service/ReservationTimeService.java @@ -0,0 +1,32 @@ +package roomescape.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import roomescape.model.ReservationTime; +import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; + +import java.util.List; + +@Service +public class ReservationTimeService { + + private final ReservationTimeRepository reservationTimeRepository; + + @Autowired + public ReservationTimeService(ReservationTimeRepository reservationTimeRepository) { + this.reservationTimeRepository = reservationTimeRepository; + } + + public ReservationTime addReservation(String startAt) { + return reservationTimeRepository.save(startAt); + } + + public List lookUpReservationTime() { + return reservationTimeRepository.readAll(); + } + + public void deleteReservation(Long id) { + reservationTimeRepository.deleteById(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29b..476363a8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +spring.datasource.url=jdbc:h2:mem:database +spring.datasource.driver-class-name=org.h2.Driver \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 00000000..5571e7d1 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE reservation_time +( + id BIGINT NOT NULL AUTO_INCREMENT, + start_at VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE reservation +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + date VARCHAR(255) NOT NULL, + time_id BIGINT, + PRIMARY KEY (id), + FOREIGN KEY (time_id) REFERENCES reservation_time (id) +); \ No newline at end of file diff --git a/src/main/resources/static/js/reservation-new.js b/src/main/resources/static/js/reservation-new.js new file mode 100644 index 00000000..ec361a71 --- /dev/null +++ b/src/main/resources/static/js/reservation-new.js @@ -0,0 +1,194 @@ +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.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); + }) + .catch(error => console.error('Error fetching time:', error)); +} + +function fetchThemes() { + requestRead(THEME_API_ENDPOINT) + .then(data => { + themesOptions.push(...data); + }) + .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 dateInput = row.querySelector('input[type="date"]'); + const timeSelect = row.querySelector('#time-select'); + const themeSelect = row.querySelector('#theme-select'); + + const reservation = { + name: nameInput.value, + date: dateInput.value, + timeId: timeSelect.value, + themeId: themeSelect.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 === 200) 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 !== 200) 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'); + }); +} diff --git a/src/main/resources/static/js/theme.js b/src/main/resources/static/js/theme.js new file mode 100644 index 00000000..502d0e0e --- /dev/null +++ b/src/main/resources/static/js/theme.js @@ -0,0 +1,135 @@ +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.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 === 200) 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 !== 200) throw new Error('Delete failed'); + }); +} diff --git a/src/main/resources/templates/admin/index.html b/src/main/resources/templates/admin/index.html index 417102cf..46199dc0 100644 --- a/src/main/resources/templates/admin/index.html +++ b/src/main/resources/templates/admin/index.html @@ -22,10 +22,13 @@ diff --git a/src/main/resources/templates/admin/reservation-legacy.html b/src/main/resources/templates/admin/reservation-legacy.html index 320ef3c7..73592458 100644 --- a/src/main/resources/templates/admin/reservation-legacy.html +++ b/src/main/resources/templates/admin/reservation-legacy.html @@ -22,10 +22,13 @@ diff --git a/src/main/resources/templates/admin/reservation-new.html b/src/main/resources/templates/admin/reservation-new.html new file mode 100644 index 00000000..4c34ec90 --- /dev/null +++ b/src/main/resources/templates/admin/reservation-new.html @@ -0,0 +1,61 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

방탈출 예약 페이지

+
+ +
+
+ + + + + + + + + + + + + +
예약번호예약자테마날짜시간
+
+ + + + diff --git a/src/main/resources/templates/admin/reservation.html b/src/main/resources/templates/admin/reservation.html index f31714eb..4c34ec90 100644 --- a/src/main/resources/templates/admin/reservation.html +++ b/src/main/resources/templates/admin/reservation.html @@ -22,10 +22,13 @@ @@ -42,6 +45,7 @@

방탈출 예약 페이지

예약번호 예약자 + 테마 날짜 시간 @@ -52,6 +56,6 @@

방탈출 예약 페이지

- + diff --git a/src/main/resources/templates/admin/theme.html b/src/main/resources/templates/admin/theme.html new file mode 100644 index 00000000..794602e9 --- /dev/null +++ b/src/main/resources/templates/admin/theme.html @@ -0,0 +1,60 @@ + + + + + + 방탈출 어드민 + + + + + + + + +
+

테마 관리 페이지

+
+ +
+
+ + + + + + + + + + + + +
순서제목설명썸네일 URL
+
+ + + + diff --git a/src/main/resources/templates/admin/time.html b/src/main/resources/templates/admin/time.html index b8449b31..e7a8abfe 100644 --- a/src/main/resources/templates/admin/time.html +++ b/src/main/resources/templates/admin/time.html @@ -22,10 +22,13 @@