토이 프로젝트를 만들게된 가장 큰 이유였던 <스터디룸 예약> 기능 개발일지를 기록하고자한다.
기능기획
1. 사용자가 원하는 날짜, 시간을 선택한다.
2. 사용자가 선택한 시간 중 예약가능한 스터디룸이 출력 된다.
3. 스터디룸을 고르고 스터디룸을 예약한다.
4. 사용자가 선택한 날짜-시간-스터디룸이 DB에 저장된다.
5. 마이페이지에서 예약한 스터디룸을 확인할 수있다.
6. 마이페이지에서 스터디룸 예약을 취소할 수 있다.
1~4 까지는 여기서 다루고, 5~6은 아래 링크에서 다루겠다. (달력만들기도 있습니다!)
목차
1. datepicker 사용하기
2. select 태그, option태그
3. 스터디룸 예약/확인
4. Controller
5. ServiceImpl
6. Mapper
개발완료된 화면 먼저 확인해볼까요~? ↓↓↓

화면에 뜨는 컴포넌트들은 부트스트랩에서 제공하는 css를 크기만 조절해서 사용했다.
다른 페이지를 개발할 때도 동일한 컴포넌트들이 크기면 변형된 채로 사용되는 것을 볼 수 있을 것이다.
1. date-picker
예약일자를 선택할 때, 부트스트랩에서 제공하는 date-picker를 사용했다.
굳이 데이터피커를 사용한 이유는 !!??
→ 선택할 수 있는 날짜를 지정해주기 위해서!
아무래도 장소를 예약하는 기능이다보니 오늘 이전의 날짜로 예약하는 것은 무의미하고,
너무 많은 기간을 예약할 수 있도록 하는것은 기능남용을 발생시킬 수 도 있기에,
선택할 수 있는 날짜를 오늘부터 2개월까지로 한정지었다.
→ 사용자가 편의를 위해
날짜를 예약할 때, 직접 입력하는 것보다 달력을 띄워서 선택하게하면 사용자가 날짜를 고를 때 다른 달력을 확인하지 않아도 되기 때문에 훨씬 편리할것이라 생각했다.

데이터피크는 cdn방식으로 사용할 수 있다. 현재 최신버전은 1.9.0 인 것 같은데,
나는 더 깔끔한 모양을 찾다가 (구글링) 아래 cdn을 가져와 사용하고 있다.
부트스트랩 제공 datepicker → https://cdnjs.com/libraries/bootstrap-datepicker
<head>
<!--datepicker cdn-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js" integrity="sha512-uto9mlQzrs59VwILcLiRYeLKPPbS/bT71da/OEBYEwcdNUk8jYIy+D176RYoop1Da+f9mvkYrmj5MCLZWEtQuA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css" integrity="sha512-aOG0c6nPNzGk+5zjwyJaoRUgCdOrfSDhmMID2u4+OIslr0GjpLKo7Xm0Ao3xmpM4T8AmIouRkqwj1nrdVsLKEQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
처음에는 datepicker가 아닌 flatpicker라는 달력 오픈소스를 사용했다.
하지만 길이나 모양이 이상하게 조절이 안돼서 datepicker로 갈아탔다.
button에 날짜를 담도록 만들었는데 그 이유는 자주 사용된 버튼 모양을 사용하여 웹페이지 분위기에 일관성을 주기 위해서이다.
버튼을 누르면 달력이 나오고, 날짜가 선택되면 버튼 value값으로 날짜가 입력된다.
아래는 달력을 담을 html소스.
<div class="row gx-4 gx-lg-5 justify-content-center" >
<div class="col-lg-8 col-xl-6 text-center" style="margin:10px;">
<input type="button" class="btn btn-light btn-xl" id="datepicker">
</div>
</div>
먼저 화면이 뜨자마자 오늘날짜가 뜨도록 new Date() 를 사용해 오늘 날짜를 입력시켜준다.
getMonth() 는 0부터 시작되기 때문에 +1 을 해주어야 원하는 월을 얻을 수 있다.
그리고 10월 이전월들에는 앞에 0 을 추가해주도록한다. ex) 1월 2월 → 01월 02월
datepicker의 정말 좋은점은 간단하다는 점! 손코딩으로 하나하나 달력을 만들지 않아도 돼서 넘나 편하다(다음 게시글에는 손코딩으로 하나하나 달력을 만드는 내용이 올라옵니다;;)
제일 중요하다고 여겨지는 부분은
minDate : 0
maxDate: +60
선택할 수 있는 날짜를 오늘부터 60일 까지로 선택하는 것인데, 이런 기능 제공 아주 칭찬해 ~
이렇게 하면 예약날짜를 선택할 수 있는 달력세팅은 완성이다.
아래는 Javascript소스
let today=new Date();
todayMonth=today.getMonth()+1;
if(todayMonth<10) todayMonth="0"+todayMonth;
bookDate.value=today.getFullYear()+"-"+todayMonth+"-"+today.getDate();
$(function(){ //최소선택시간 최대선택시간 초기년월
$("#datepicker").datepicker({
dateFormat: 'yyyy-mm-dd',
monthNames: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
monthNamesShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
dayNames: ['일', '월', '화', '수', '목', '금', '토'],
dayNamesShort: ['일', '월', '화', '수', '목', '금', '토'],
dayNamesMin: ['일', '월', '화', '수', '목', '금', '토'],
showMonthAfterYear: true,
yearSuffix: '년',
minDate: 0,
maxDate: +60
})
})
2. select 태그 option 태그
이제 select 태그로 예약시간을 선택할 건데, 1시간 단위로 예약할 수 있게끔 만들었다. 아주 간단.
선택된 옵션의 value는 시작시간으로 주었다. 그리고 DB에 넘길 때도 value값, 즉 시작시간만 넘겨주게끔 만들었다.
1시간 단위로만 예약할 수 있기 때문에 시작시간만 저장하는것이 데이터를 다룰 때 편리할 것이라고 생각했기때문.
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-lg-8 col-xl-6 text-center"style="margin:10px;">
<select id="bookTime">
<option value="9">09:00 - 10:00</option>
<option value="10">10:00 - 11:00</option>
<option value="11">11:00 - 12:00</option>
<option value="12">12:00 - 13:00</option>
<option value="13">13:00 - 14:00</option>
<option value="14">14:00 - 15:00</option>
<option value="15">15:00 - 16:00</option>
<option value="16">16:00 - 17:00</option>
<option value="17">17:00 - 18:00</option>
<option value="18">18:00 - 19:00</option>
<option value="19">19:00 - 20:00</option>
<option value="20">20:00 - 21:00</option>
<option value="21">21:00 - 22:00</option>
</select>
</div>
</div>
3. 예약가능한 스터디룸 확인/예약하기

여기가 예약기능의 하이라이트 !
"예약 가능한 세미나실"이라는 버튼을 누르면 스터디룸 호실이 쫘르륵 펼쳐진다.
그리고 이미 예약되어있는 스터디룸은 선택할 수 없게 막혀 있고, 색깔도 다르다.
예약할 수 있는 스터디룸 호실을 클릭하면 알림창에 사용자가 선택한 날짜, 시간, 스터디룸호실이 한번더 뜨고 알림창의 확인 버튼을 누르면 예약에 성공하게된다.
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-lg-8 col-xl-6 text-center">
<input type="button" style="background-color:#FFFF8FF;margin:10px;" class="btn btn-light btn-xl" onclick="retrieveBookableRoom()" value="예약 가능한 세미나실">
</div>
</div>
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-lg-8 col-xl-6 text-center">
<table id="studyRoomTable" style="display:none">
<tr>
<td><input type="button" value="101" id="room101" disabled="true" onclick="sendRoomId(this)" class="btn btn-primary btn-xl" style="padding:30px 33px;margin:1px;"/></td>
<td><input type="button" value="102" id="room102" disabled="true" onclick="sendRoomId(this)" class="btn btn-primary btn-xl" style="padding:30px 33px;margin:1px;"/></td>
<td><input type="button" value="103" id="room103" disabled="true" onclick="sendRoomId(this)" class="btn btn-primary btn-xl" style="padding:30px 33px;margin:1px;"/></td>
</tr>
<tr>
</tr>
<tr>
<td><input type="button" value="104" id="room104" disabled="true" onclick="sendRoomId(this)" class="btn btn-primary btn-xl" style="padding:30px 33px;margin:1px;"/></td>
<td><input type="button" value="105" id="room105" disabled="true" onclick="sendRoomId(this)" class="btn btn-primary btn-xl" style="padding:30px 33px;margin:1px;"/></td>
<td><input type="button" value="106" id="room106" disabled="true" onclick="sendRoomId(this)" class="btn btn-primary btn-xl" style="padding:30px 33px;margin:1px;"/></td>
</tr>
</table>
</div>
</div>
<!--사용가능한 세미나실 -->
function retrieveBookableRoom(){
console.log(bookTime.value);
console.log(bookDate.value);
studyRoomTable.style.display="none";
let xhr = new XMLHttpRequest();
xhr.open('Get'
, "/stuhel/book/retrieveBookableRoom?bookTime="+bookTime.value+"&bookDate="+bookDate.value
, true);
xhr.setRequestHeader('Accept','application/json');
xhr.send();
xhr.onreadystatechange = () => {
if(xhr.readyState == 4 && xhr.status == 200){
let txt = xhr.responseText;
txt = JSON.parse(txt);
console.log(txt.roomId);
if(txt.roomId.length <= 0){alert("예약가능한 세미나실이 없습니다."); return;}
//studyRoomTable.style="none";
room101.disabled=true;
room102.disabled=true;
room103.disabled=true;
room104.disabled=true;
room105.disabled=true;
room106.disabled=true;
studyRoomTable.style="flex";
for(let i of txt.roomId){
if(i==101){
room101.disabled=false;
room101.style.backgroundColor="tomato";
}if(i==102){
room102.disabled=false;
room102.style.backgroundColor="tomato";
}if(i==103){
room103.disabled=false;
room103.style.backgroundColor="tomato";
}if(i==104){
room104.disabled=false;
room104.style.backgroundColor="tomato";
}if(i==105){
room105.disabled=false;
room105.style.backgroundColor="tomato";
} if(i==106){
room106.disabled=false;
room106.style.backgroundColor="tomato";
}
}
}
}
}
<!--세미나실 예약확인 창 -->
function sendRoomId(roomId){
swal({
title : '세미나실 예약'
, text : 'ヽ(✿゚▽゚)ノ \n'
+bookDate.value+'\n'
+bookTime.value+'시 ~ '+(parseInt(bookTime.value)+1) + '시 \n'
+'해당 일시에 '+roomId.value+'호 세미나실을 예약하시겠습니까?'
, buttons : ["취소","확인"]
, buttonColor : 'tomato'
}).then(function(result) {
if(result==null) return;
sendBookdata(roomId.value); <!--예약자, 예약방, 예약시간, 예약날짜 DB로 보내서 insert해주기-->
});
}
<!--세미나실 예약-->
function sendBookdata(roomId){
let insertBookData ={"roomId":roomId,"bookTime":bookTime.value,"bookDate":bookDate.value};
insertBookData=JSON.stringify(insertBookData);
let xhr = new XMLHttpRequest();
xhr.open('POST'
, "/stuhel/book/roomBook?insertBookData="+encodeURI(insertBookData)
, true);
xhr.setRequestHeader('Accept','application/json');
xhr.send();
xhr.onreadystatechange = () => {
if(xhr.readyState == 4 && xhr.status == 200){
let txt = xhr.responseText;
txt = JSON.parse(txt);
alert("선택하신 세미나실이 예약되었습니다.");
room101.disabled=true;
room102.disabled=true;
room103.disabled=true;
room104.disabled=true;
room105.disabled=true;
room106.disabled=true;
studyRoomTable.style="none";
}
}
}
Controller
retrieveBoolableRoom()
컨트롤러에서 @RequestParam 으로 데이터를 받은 데이터들을 TO에 담아준다.
retrieveBoolableRoom메소드에서 gson을 사용하지 않고 하나하나 담은것은 넘어올 데이터가 딱 두개밖에 없기 때문이다.
사용자가 선택한 날짜와 시간! 이 두 데이터를 가지고 사용가능한 스터디룸을 받아올 것이다.
roomBook()
roomBook()메서드에서는 json형식으로 들어온 데이터들을 gson을 사용해서 TO에 담아준다.
why? 여기서도 딱 세가지 데이터 밖에 들어오지 않지만, 굳이 gson을 사용한 이유는 데이터를 주고받는 다양한 방법을 연습해보기 위해서이다.
심지어 Javascript에서 json형식을 만들 때, new Object를 사용하지 않고, '{ }' 이 중괄호 안에 하나하나 데이터를 넣어주었다.
암튼 다시 controller로 돌아와서 session에 저장해둔 memberId를 TO에 넣어주었다. session에 저장된 memberId는 로그인할 때 session에 입력해둔 사용자 ID 이다. 이 ID 는 DB에 저장되어서 사용자가 자신의 예약현황을 보거나, 예약을 취소할 때 데이터를 꺼낼 기준으로 사용될 것이다.
TMI time
누가 읽을지는 모르겠지만, 나는 front-end보다 back-end를 더 좋아한다. 이유는 보이지 않는 데이터가 교환되고, 저장되고, 가공해서, 또 교환되고, 저장할 때와는 다른 모양으로 세상 밖으로 나가는 이 흐름자체가 좋기 때문이다.
back-end에서 데이터를 이렇게 저렇게 옮기고 가공할 때, 내가 데이터들이 가는 길을 만들어 주는건지, 데이터들이 나를 끌고 가는건지는 모르겠지만 데이터의 흐름을 원활하게 만들고난 후에는 front에서는 맛볼 수 없는 쾌감이 있다. 괜히 자신감이 솓는 기분 ㅎ_ㅎ
//controller
@GetMapping("/retrieveBookableRoom")
HashMap<String,ArrayList> retrieveBookableRoom(@RequestParam("bookTime") int bookTime, @RequestParam("bookDate") String bookDate){
BookTO bookTO=new BookTO();
bookTO.setBookDate(bookDate);
bookTO.setBookTime(bookTime);
ArrayList<Integer> roomId = bookService.retrieveBookableRoom(bookTO);
HashMap<String, ArrayList> map = new HashMap<>();
map.put("roomId",roomId);
return map;
}
@PostMapping("/roomBook")
HashMap<String,Integer> roomBook(@RequestParam("insertBookData") String insertBookData, HttpServletRequest request){
HttpSession session=request.getSession();
BookTO bookTO = gson.fromJson(insertBookData, BookTO.class);
String memberId=session.getAttribute("memberId").toString();
bookTO.setUserId(memberId);
HashMap<String, Integer> map = new HashMap<>();
map=bookService.roomBook(bookTO);
return map;
}
ServiceImpl
service단은 so간단..
업무 로직은 service단에서 짜는거라고 알고 있는데, 사실 나는 service단에서 하는일이 제일 없는것 같다.
TO를 Controller에서 DAO로 DAO에서 Controller로 전달하기만 할뿐,,
와아아이~~? 무엇을 해야할지..
@Override
public ArrayList<Integer> retrieveBookableRoom(BookTO bookTO) {
ArrayList<Integer> roomName = bookDAO.selectBookableRoom(bookTO);
return roomName;
}
@Override
public HashMap<String, Integer> roomBook(BookTO bookTO) {
HashMap<String,Integer> map =new HashMap<>();
bookDAO.insertRoomBook(bookTO);
return map;
}
DAO는 생략하고~ Mapper !
selectBookableRoom
예약가능한 스터디룸 조회하는 sql에서는 sub query를 사용했다.
스터디룸예약정보가 있는 ROOM_BOOK 테이블에서, 사용자가 원하는 예약 날짜/시간과 동일한 예약정보가 있는지 확인하는 일을 subquery 가 한다.
그리고 NOT IN ( )을 사용하여서 subquery에서 조회하지 못한 스터디룸ID만 최종적으로 조회하여 스터디룸 ID들을 앞단으로 보내주었다.
insertRoomBook
예약을 신청한 사용자ID, 스터디룸ID, 예약날짜, 예약시간 을 insert해준다. 그리고 Sequence number를 만들어서 고유의 예약 ID를 생성해주었다.
<resultMap id="RoomName" type="String">
<result property="roomId" column="ROOM_ID"/>
</resultMap>
<select id="selectBookableRoom" parameterType="com.helper.study.stuhel.book.to.BookTO" resultMap="RoomName">
SELECT ROOM_ID
FROM STUDY_ROOM S
WHERE S.ROOM_ID NOT IN (select r.ROOM_ID
from ROOM_BOOK r
where r.BOOK_DATE = #{bookDate}
and r.BOOK_TIME = #{bookTime})
</select>
<insert id="insertRoomBook" parameterType="com.helper.study.stuhel.book.to.BookTO">
INSERT INTO ROOM_BOOK (
BOOK_ID
,USER_ID
,ROOM_ID
,BOOK_DATE
,BOOK_TIME
)VALUES(
ROOM_SEQ.NEXTVAL
,#{userId}
,#{roomId}
,#{bookDate}
,#{bookTime}
)
</insert>