Side Project/STUHEL (학습을 돕는 예약시스템)

초보개발자 Toy-project 개발일지 ▲NO.8 ▲ 스터디룸 예약기능 만들기, Sweet-alert, date-picker, Spring-boot, Oracle, Java, Javascript, 부트스트랩, 토이프로젝트

isjiji 2022. 8. 12. 19:13

토이 프로젝트를 만들게된 가장 큰 이유였던 <스터디룸 예약> 기능 개발일지를 기록하고자한다.

 

기능기획

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>