티스토리 뷰

이전글들에 이어 화면 소스작성 진행해 보겠습니다. 이번소스 역시 분량이 많기 때문에 자세한 소스는 아래 Github 참고 부탁드립니다.

 

1. static 폴더

1.1. style.css

@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600");
@import "reset.css";
@import "header.css";
@import "globals.css";
@import "navigation.css";
@import "search-bar.css";
@import "friends.css";
@import "chats.css";
@import "find.css";
@import "more.css";
@import "profile.css";
@import "chat.css";
@import "bigScreen.css";
@import "mobile.css";
@import "login.css";
@import "modal.css";
  • 모든 css 파일을 import 하여 1개의 파일로 관리합니다.

 

1.2. common.js

// Post API 호출
function postApi(url, params, successCallback, errorCallback) {
  $.ajax({
    type: 'POST',
    url: url,
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    data: JSON.stringify(params),
    success: function (data) {
      if (typeof successCallback === 'function') {
        successCallback(data);
      }
    },
    error: function (err) {
      if (typeof errorCallback === 'function') {
        errorCallback(err);
      }
    }
  });
}

function multiApi(url, formData, successCallback, errorCallback) {

  $.ajax({
    type: 'POST',
    url: url,
    contentType: false, 
    processData: false, 
    data: formData,
    success: function (data) {
      if (typeof successCallback === 'function') {
        successCallback(data);
      }
    },
    error: function (err) {
      if (typeof errorCallback === 'function') {
        errorCallback(err);
      }
    }
  });
}


// 모달 열기
function openModal() {
  document.querySelector('.modal').style.display = 'block';
}

// 모달 닫기
function closeModal() {
  document.querySelector('.modal').style.display = 'none';
}

function onApiError(err) {
  closeModal();
  alert(err.responseJSON.exception.errorMessage);
}

function onApiSuccess(data) {
  closeModal();
  alert(data.msg);
}

function goBack(){
  window.history.back();
}
  • ajax 통신을 통해 API와 통신하는 소스를 공통으로 작성합니다.

 

2. templates 폴더

2.1. head.html

<!DOCTYPE html>
<html lagn="ko" xmlns:th="http://www.thymeleaf.org">

<th:block th:fragment="headFragment">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
    <link th:href="@{/css/styles.css}" rel="stylesheet"/>
    <link rel="icon" th:href="@{/favicon.ico}" type="image/x-icon">
    <script th:src="@{/js/jquery-3.7.1.min.js}"></script>
    <script th:src="@{/js/common.js}"></script>
    <script src="https://kit.fontawesome.com/a9eb1f10be.js" crossorigin="anonymous"></script>
    <title>깨톡</title>
  </head>
</th:block>
</html>
  • 공통으로 사용하는 head 정보들은 담습니다.

 

2.2. index.html [친구목록]

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="layout/fragments/head.html :: headFragment"/>
<body>
<header class="top-header">
  <th:block th:replace="layout/fragments/header_top.html :: headerTopFragment"/>
  <div class="header__bottom">
    <div class="header__column">
      <span class="header__text"></span>
    </div>
    <div class="header__column">
      <span class="header__text">친구목록</span>
      <span class="header__number" th:text="${userVO.friendList.size()}"></span>
    </div>
    <div class="header__column">
      <i class="fa fa-cog"></i>
    </div>
  </div>
</header>
<main class="friends">
  <div class="search-bar">
    <i class="fa fa-search"></i>
    <input type="text" placeholder="이름을 검색해 보세요." onkeyup="searchFriend(this)" />
  </div>

  <!-- 나의 프로필 -->
  <section class="friends__section">
    <header class="friends__section-header">
      <h6 class="friends_profile-title">나의 프로필</h6>
    </header>
    <div class="friends__section-rows">
      <div class="friends__section-row with-tagline">
        <div class="friends__section-column">
          <img th:src="@{'/profile/' + ${userVO.lginData.userId} + '.png'}" alt=""
               onerror="this.src='/images/avatar.png'"
               th:onclick="'location.href=\'/userProfile/'+ @{${userVO.lginData.userId}} + '\''">
          <span class="friends__section-name" th:text="${userVO.lginData.userName}"></span>
        </div>
        <span class="friends__section-tagline" th:text="${userVO.lginData.statMsg}"></span>
      </div>
    </div>
  </section>

  <!-- 즐겨찾기 -->
  <section class="friends__section">
    <header class="friends__section-header">
      <h6 class="friends_profile-title">즐겨찾기</h6>
    </header>
    <div class="friends__section-rows" id="bookmark_row">
      <!-- 즐겨찾기 친구목록 -->
    </div>
  </section>

  <!-- 나머지 친구목록 -->
  <section class="friends__section">
    <header class="friends__section-header">
      <h6 class="friends__section-title">친구목록</h6>
    </header>
    <div class="friends__section-rows" id="etc_row">
      <!-- 나머지 친구목록 -->
    </div>
  </section>

  <div class="chat-btn" onclick="enterFindFriend()">
    <i class="fa fa-user"></i>
  </div>
</main>

<th:block th:replace="layout/fragments/footer.html :: footerFragment"/>
<div class="bigScreenText">
  Please make your screen smaller
</div>
<script th:inline="javascript">
  var userId = [[${userVO.lginData.userId}]];

  window.onload = function () {
    var tabs = document.querySelectorAll('.tab-bar__tab');
    tabs.forEach(function (tab) {
      tab.classList.remove('tab-bar__tab--selected');
      if (tab.id === 'tab1') {
        tab.classList.add('tab-bar__tab--selected');
      }
    });

    // 친구목록 불러오기
    var friendList = [[${userVO.friendList}]];
    for (var i = 0; i < friendList.length; i++) {
      var friend = friendList[i];
      var str = '';

      str += '<div class="friends__section-row with-tagline">';
      str += '  <div class="friends__section-column">';
      str += '      <img src="/profile/' + friend.friendId + '.png" alt="" onerror="this.src=\'/images/avatar.png\'" onclick="location.href=\'/userProfile/' + friend.friendId + '\'">';
      str += '      <span class="friends__section-name">' + friend.friendName;
      str += '      </span>';
      if (friend.bookmarkYn === 'Y') {
        str += '&nbsp<i class="fa fa-star" onclick="bookmarkOnOff(' + "'N', '" + friend.friendName + "', '"+friend.friendId+"'"+')"></i>';
      } else {
        str += '&nbsp<i class="fa fa-star-o" onclick="bookmarkOnOff(' + "'Y', '" + friend.friendName + "', '"+friend.friendId+"'"+')"></i>';
      }
      str += '  </div>';
      str += '  <span class="friends__section-tagline">' + friend.friendStatMsg + '</span>';
      str += '</div>';

      if (friend.bookmarkYn === 'Y') {
        $("#bookmark_row").append(str);
      } else {
        $("#etc_row").append(str);
      }
    }
  }

  function enterFindFriend() {
    var findUserId = prompt("검색하실 유저 아이디를 입력해주세요.")

    if(!findUserId || findUserId === ''){
      return;
    }

    var data = {
      userId: findUserId
    }

    // API 호출
    openModal();
    postApi('/findFriend', data, onFindFriendSuccess, onApiError);
  }

  function onFindFriendSuccess(data) {
    closeModal();
    if (data.existYn !== 'Y') {
      alert('유저가 존재하지 않습니다');
    } else {
      window.location.replace('/userProfile/' + data.userId);
    }
  }

  function bookmarkOnOff(onoff, name, id) {
    var type = onoff === 'Y' ? '등록' : '해제';
    var check = confirm('\'' + name + '\' 님을 즐겨찾기 ' + type + "하시겠습니까?");
    if (check) {
      var data = {
        userId: userId,
        friendId: id,
        bookmarkOnOff: onoff
      }

      // API 호출
      openModal();
      postApi('/updateBookmark', data, onUpdateBookmarkSuccess, onApiError);
    }
  }

  function onUpdateBookmarkSuccess(data) {
    closeModal();
    alert(data.msg);
    window.location.replace('/');
  }

  function searchFriend(input) {
    let filter = input.value.toUpperCase();
    let div = document.getElementsByClassName("friends__section-rows");

    for (let i = 0; i < div.length; i++) {
      let chatUser = div[i].getElementsByClassName("friends__section-name")[0];
      let txtValue = chatUser.textContent || chatUser.innerText;

      if (txtValue.toUpperCase().indexOf(filter) > -1) {
        div[i].style.display = "";
      } else {
        div[i].style.display = "none";
      }
    }
  }

</script>
</body>
</html>

 

2.3. chat.html [채팅방]

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="layout/fragments/head.html :: headFragment"/>
<body class="body-chat">
<header class="top-header chat-header">
  <th:block th:replace="layout/fragments/header_top.html :: headerTopFragment"/>

  <div class="header__bottom">
    <div class="header__column">
      <a href="/chats">
        <i class="fa fa-chevron-left fa-lg"></i>
      </a>
    </div>
    <div class="header__column">
      <span class="header__text" th:text="${roomVO.roomInfo.roomName}"></span>
    </div>
    <div class="header__column">
      <i class="fa fa-search"></i>
      <i class="fa fa-bars" onclick="togglePopup()"></i>

      <div class="chat_overlay" id="overlay" style="display:none;">
        <div class="chat_popup" id="popup">
          <div>
            <a onclick="togglePopup()">
              <i class="fa fa-times fa-lg" aria-hidden="true"></i>
            </a>
          </div>
          <br/>
          <button onclick="exitRoom()">방나가기</button>
          <br/>
        </div>
      </div>
    </div>
  </div>
</header>

<main class="chat" id="chatList">

  <th:block th:each="message : ${roomVO.messageList}">
    <!-- 시스템이 보낸 메세지 -->
    <div class="date-divider" th:if="${message.userId == 'system'}">
      <span class="date-divider__text" th:text="${message.message}"></span>
    </div>

    <!-- 내가 보낸 메세지 -->
    <div class="chat__message chat__message-from-me"
         th:if="${message != null and userVO.lginData.userId == message.userId and message.userId != 'system'}">
      <div>
        <div class="chat__message-unread-me" th:if="${message.unread != '0'}" th:text="${message.unread}"></div>
        <span class="chat__message-time" th:text="${#strings.substring(message.time, 11, 16)}"></span>
      </div>
      <span class="chat__message-body" th:text="${message.message}"></span>
    </div>

    <!-- 내가 받은 메세지 -->
    <div class="chat__message chat__message-to-me"
         th:if="${message != null and userVO.lginData.userId != message.userId and message.userId != 'system'}">
      <img th:src="@{'/profile/' + ${message.userId} + '.png'}" alt="" class="chat-message-avatar"
           onerror="this.src='/images/avatar.png'">
      <div class="chat__message-center">
        <h3 class="chat__message-username" th:text="${message.userName}"></h3>
        <span class="chat__message-body" th:text="${message.message}"></span>
      </div>
      <div>
        <div class="chat__message-unread-you" th:if="${message.unread != '0'}" th:text="${message.unread}"></div>
        <span class="chat__message-time" th:text="${#strings.substring(message.time, 11, 16)}"></span>
      </div>
    </div>
  </th:block>

</main>

<div class="type-message">
  <i class="fa fa-plus fa-lg" onclick="inviteUser()"></i>
  <div class="type-message__input">
    <input type="text" id="messageInput" onkeyup="handleKeyPress()">
    <i class="fa fa-smile-o fa-lg"></i>
    <span class="record-message" onclick="handleOnClickSend()">
      <i class="fa fa-send fa-lg"></i>
    </span>
  </div>
</div>
<div class="bigScreenText">
  Please make your screen smaller
</div>
<script th:inline="javascript">

  window.onload = function () {
    scrollDown();
  };

  function scrollDown() {
    window.scrollTo(0, document.body.scrollHeight);
  }

  var userId = [[${userVO.lginData.userId}]];
  var userName = [[${userVO.lginData.userName}]];
  var roomId = [[${roomVO.roomInfo.roomId}]];
  var ws_url = [[${wsUrl}]];

  openModal();
  const ws = new WebSocket("ws://" + ws_url + "/ws/chatting/" + roomId);

  function handleKeyPress() {
    if (window.event.keyCode === 13) {
      var message = document.getElementById('messageInput').value;
      sendMessage(message);
      document.getElementById('messageInput').value = ''; // 메시지를 보내고 입력란 비우기
    }
  }

  function handleOnClickSend() {
    var message = document.getElementById('messageInput').value;
    sendMessage(message);
    document.getElementById('messageInput').value = ''; // 메시지를 보내고 입력란 비우기
  }

  function inviteUser() {
    let userId = prompt("초대하실 유저 아이디를 입력해주세요.")

    if(!userId || userId === ''){
      return;
    }

    let data = {
      userId: userId,
      roomId: roomId
    }

    // API 호출
    openModal();
    postApi('/inviteRoom', data, onApiSuccess, onApiError);
  }

  ws.onopen = function () {
    closeModal();
    console.log('입장성공');
  };

  ws.onmessage = function (event) {
    let message = event.data;
    let data = JSON.parse(message);
    let str = '';
    if (data.userId === userId) {
      // 내가 보낸 메세지
      str += '<div class="chat__message chat__message-from-me">';
      str += '  <div>';
      if(data.unread && data.unread !== '0'){
        str += '    <div class="chat__message-unread-me">'+data.unread+'</div>';
      }
      str += '    <span class="chat__message-time">' + data.time.substring(11, 16) + '</span>';
      str += '  </div>';
      str += '  <span class="chat__message-body">' + data.message + '</span>';
      str += '</div>';
    } else if(data.userId === 'system'){
      // 시스템이 보낸 메세지
      str += ' <div class="date-divider"">';
      str += '  <span class="date-divider__text">' + data.message + '</span>';
      str += '</div>';
    } else {
      // 상대가 보낸 메세지
      str += ' <div class="chat__message chat__message-to-me">';
      str += '  <img src="/profile/' + data.userId + '.png" alt="" class="chat-message-avatar" onerror="this.src=\'/images/avatar.png\'">';
      str += '  <div class="chat__message-center">';
      str += '    <h3 class="chat__message-username">' + data.userName + '</h3>';
      str += '    <span class="chat__message-body">' + data.message + '</span>';
      str += '  </div>';
      str += '  <div>';
      if(data.unread && data.unread !== '0'){
        str += '    <div class="chat__message-unread-you">'+data.unread+'</div>';
      }
      str += '    <span class="chat__message-time">' + data.time.substring(11, 16) + '</span>';
      str += '  </div>';
      str += ' </div>';
    }
    $("#chatList").append(str);
    scrollDown();
  };

  ws.onclose = function () {
    console.log("ws.onclose");
  };

  ws.onerror = function (error) {
    closeModal();
    alert('소켓 연결중 오류가 발생하였습니다. 잠시 후 다시 이용해 주시길 바랍니다.');
    window.location.replace('/chats');
  };

  // 메시지를 보내는 함수
  function sendMessage(message) {
    if (ws.readyState === WebSocket.OPEN) {
      let currentTime = getCurrentTime();
      let messageId = userId + "_" + roomId + "_" + currentTime;
      let data = {
        messageId: messageId,
        message: message,
        userId: userId,
        userName: userName,
        time: currentTime
      };

      ws.send(JSON.stringify(data));
    } else if (ws.readyState === WebSocket.CONNECTING) {
      console.log('WebSocket 연결이 아직 진행 중입니다.');
    } else {
      console.error('WebSocket 연결이 닫혀 있습니다.');
    }
  }

  function getCurrentTime() {
    let now = new Date();
    let year = now.getFullYear();
    let month = String(now.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 1을 더함
    let day = String(now.getDate()).padStart(2, '0');
    let hours = String(now.getHours()).padStart(2, '0');
    let minutes = String(now.getMinutes()).padStart(2, '0');
    let seconds = String(now.getSeconds()).padStart(2, '0');

    // YYYY-MM-DD HH:mm:ss
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  }

  function togglePopup() {
    let overlay = document.getElementById('overlay');
    if (overlay.style.display === 'flex') {
      overlay.style.display = 'none';
    } else {
      overlay.style.display = 'flex';
    }
  }

  function exitRoom() {
    let data = {
      userId: userId,
      roomId: roomId
    }
    // API 호출
    openModal();
    postApi('/exitRoom', data, onExitRoomSuccess, onApiError);
  }

  function onExitRoomSuccess(data) {
    closeModal();
    alert(data.msg);
    window.location.replace('/chats');
  }

</script>

</body>
</html>
  • 웹소켓통신을 하여 실시간 채팅을 주고받습니다.

 

3. 실행화면

3.1. 로그인

 

3.2. 친구목록



3.3. 프로필 수정

 

3.4. 친구추가 및 채팅 시작

 

3.5. 채팅목록

 

3.6. 채팅

 

 

서비스 목적이 아닌 공부용으로 제작하였기 때문에 기능이 많진 않습니다. 기본적인 기능만 구현을 해둔 상태이니 참고만 부탁드립니다.


감사합니다.

최근에 올라온 글
Total
Today
Yesterday