티스토리 뷰
이전글들에 이어 화면 소스작성 진행해 보겠습니다. 이번소스 역시 분량이 많기 때문에 자세한 소스는 아래 Github 참고 부탁드립니다.
- 환경설정 : [SpringBoot] Thymeleaf를 이용한 카카오톡 클론 코딩 - 1
- 서버 소스작성 : [SpringBoot] Thymeleaf를 이용한 카카오톡 클론 코딩 - 2
- 화면 소스작성 : [SpringBoot] Thymeleaf를 이용한 카카오톡 클론 코딩 - 3
- Github : 카카오톡 클론 코딩 Git
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 += ' <i class="fa fa-star" onclick="bookmarkOnOff(' + "'N', '" + friend.friendName + "', '"+friend.friendId+"'"+')"></i>';
} else {
str += ' <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. 채팅
서비스 목적이 아닌 공부용으로 제작하였기 때문에 기능이 많진 않습니다. 기본적인 기능만 구현을 해둔 상태이니 참고만 부탁드립니다.
감사합니다.
'프레임워크 > SpringBoot' 카테고리의 다른 글
[SpringBoot] openNLP를 이용해 고객센터 챗봇 만들기 (0) | 2024.08.20 |
---|---|
[SpringBoot] 외장톰캣 배포 404 오류 (0) | 2024.08.08 |
[SpringBoot] Thymeleaf를 이용한 카카오톡 클론 코딩 - 2 (0) | 2024.08.07 |
[SpringBoot] Thymeleaf를 이용한 카카오톡 클론 코딩 - 1 (0) | 2024.08.07 |
[SpringBoot+Next.js] JWT 토큰 인증 예제 (0) | 2024.08.01 |
최근에 올라온 글
- Total
- Today
- Yesterday