- 회원가입
- 휴대폰 인증
- 로그인
- 로그아웃
- 아이디/비밀번호 찾기
- 프로필 변경
- 자기소개 음성 녹음
- 신고
- 회원 탈퇴
- 친구 추가
- 친구 삭제
- 쪽지 보내기/받기
- 친구 목록(온·오프라인 표시)
- 쪽지 알림
- 쪽지 읽음/안읽음 표시
- 채팅 신청
- 채팅 수락/거절
- 채팅 보내기/받기
- 채팅 알림
- 채팅 읽음/안읽음 표시
- 음성 채팅
- 사진 전송
- 상대방 평가
- 광장 생성
- 광장 설정 변경
- 마우스 클릭 이동
- 키보드 조작키 이동
- 백그라운드 탭일 때 유저 아이콘 블러 처리
- 채팅
- 방장 위임
- 강제 퇴장
- 충전(카카오페이, 토스페이먼츠, 휴대폰결제)
- 결제 내역 확인
- 결제 취소
- 이용권 구매
- 매칭 신청
- 매칭 수락/거절
- 매칭 상태 표시
- 이상형 선택
- 연애관 테스트
- 내 위치 주변, 이상형, 성향 및 관심사 매칭, 랜덤 매칭 추천
- 게시물 작성, 조회, 수정, 삭제
- 댓글 작성, 조회, 수정, 삭제
- 문의 작성, 조회
- 답변 상태 표시
- 답변 시 메일 알림
- 후기 작성, 조회, 수정, 삭제
- 등록일순, 별점순 조회
- 최근 일주일 신규 가입자 수 선 그래프
- 운영진 테이블
- 남녀 성비 원 그래프
- 최근 일주일 매칭 성공률 막대 그래프
- 회원 정보 조회
- 아이디, 닉네임 검색
- 회원 삭제
- 게시물 조회
- 게시물 삭제
- 내용, 작성자 닉네임 검색
- 후기 조회
- 후기 삭제
- 내용, 작성자 닉네임 검색
- 결제 내역 조회
- 결제 취소
- 결제 번호, 주문자 닉네임 검색
- 문의 조회
- 문의 삭제
- 문의 답변
- 문의 제목, 내용, 작성자 닉네임 검색
- 신고 내역 조회
- 정지/영구 정지 조치
- 신고된 채팅방 확인
- 공지 등록, 수정, 조회, 삭제
<form id="sendChatForm" autocomplete="off">
<div class="card" id="imagePreviewForm">
<div class="card-body" id="imagePreview"></div>
</div>
<textarea type="text" id="msgContent" class="form-control"></textarea>
<div class="d-flex justify-content-between align-items-center m-2">
<a th:href="@{/chat/list}" style="padding:3px 5px;"><i class="bi bi-box-arrow-right"
style="font-size: 30px;"></i></a>
<a href="javascript:void(0)" id="chatImageBtn">
<i class="bi bi-images" style="font-size: 30px;"></i>
</a>
<a href="javascript:void(0)" id="voiceChatBtn">
<i class="bi bi-telephone-fill" style="font-size: 30px;"></i>
</a>
<a href="javascript:void(0)" id="chatReportBtn">
<img th:src="@{/image/etc/siren.png}" alt=""
style="height: 30px; width: 30px; margin-bottom: 10px;">
</a>
<a href="javascript:void(0)" id="rateBtn" th:if="${rate == null && room.possible == true}">
<i class="bi bi-pencil-square" style="font-size: 30px;"></i>
</a>
<i class="fas fa-smile" id="emoji-icon" data-bs-toggle="collapse" data-bs-target="#emojiCollapse"
aria-expanded="false" aria-controls="emojiCollapse" type="button" style="font-size: 30px;"></i>
<button type="submit" style="border:0;">
<i class="bi bi-send-fill" style="font-size: 30px; color: rgb(146, 132, 168)"></i>
</button>
</div>
<div class="collapse" id="emojiCollapse" style="max-height: 200px; overflow: auto;">
<div class="border-top p-2" id="emoji"></div>
</div>
</form>
const socket = new SockJS("/ws");
const stompClient = Stomp.over(socket);
$("#sendChatForm").submit(function (event) {
event.preventDefault();
// 이미지 파일과 채팅 내용을 포함하는 formData 생성
var files = $("#chatImageInput")[0].files;
var chatContent = {
content: $("#msgContent").val(),
writer: user,
code: roomCode,
target: target
};
if (files.length > 0) {
var formData = new FormData();
// 이미지 파일 처리
for (var i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
formData.append('chatContainer', JSON.stringify(chatContent));
// 이미지 업로드를 위한 Ajax 요청
$.ajax({
// ...
});
} else {
// 텍스트 채팅 메시지 전송
if ($("#msgContent").val().trim() !== "") {
stompClient.send("/app/send", {}, JSON.stringify(chatContent));
}
}
// 폼 초기화
$("#msgContent").val("");
$("#msgContent").focus();
});
stompClient.connect({}, function (frame) {
// 브라우저의 푸시 알림 권한을 요청
if (Notification.permission !== "granted") {
Notification.requestPermission().then(r => {
if (r === "granted") alert("푸시 알림이 설정되었습니다.");
});
}
// 개별 사용자에 대한 메시지 구독
stompClient.subscribe("/topic/all/" + $("#hiddenUserName").val(), function (message) {
const data = JSON.parse(message.body);
const chatToast = new bootstrap.Toast($('#chatToast'), {
autohide: false
});
if (data.type === "chatRequest") {
$("#requestUsername").val(data.username);
$("#chatRequestModal .modal-title").text(data.content);
$("#chatRequestModal .nickName").text(data.nickname);
$("#chatRequestModal .userAge").text(data.age);
$("#chatRequestModal .introduce").text(data.introduce);
$("#chatRequestModal .profileImage").attr("src", data.image);
$("#chatRequestModal").modal("show");
} else if (data.type === "refuse")
alert(data.content);
else if (data.type === "acceptChat") {
alert(data.nickname + "님이 수락했습니다. 채팅방으로 이동합니다.");
window.location.href = "/chat/join/" + data.content;
} else if (data.type === "sendChat" && !window.location.pathname.includes(data.target)) {
$("#chatToast .profile").attr("src", data.image);
$("#chatToast .nickName").text(data.nickname);
$("#chatToast .toast-body > a").text(data.content);
$("#chatToast .toast-body > a").attr("href", "/chat/join/" + data.target);
chatToast.show();
}
@MessageMapping("/send")
public void sendMessage(ChatDto chatDto) {
SiteUser user = userService.getByUsername(chatDto.getWriter());
SiteUser target = userService.getByUsername(chatDto.getTarget());
Room room = roomService.get(chatDto.getCode());
Chat chat = chatService.create(room, user, target, chatDto.getContent(), room.getRecentDate());
roomService.setRecent(room, chat.getCreateDate());
roomService.setConfirm(room, target.getUsername());
chatDto.setWriterNick(user.getNickName());
chatDto.setWriterImage(user.getImage());
chatDto.setCreateDate(chat.getCreateDate());
MessageDto messageDto = new MessageDto();
messageDto.setType("sendChat");
messageDto.setNickname(user.getNickName());
messageDto.setContent(chatDto.getContent());
messageDto.setImage(user.getImage());
messageDto.setTarget(chatDto.getCode());
this.roomService.saveChatNumbers(room);
Notification notification = new Notification("채팅 알림",
user.getNickName() + "님이 채팅를 보냈습니다.", user.getUsername(), target.getUsername(),
"/chat/join/" + chatDto.getCode());
simpMessagingTemplate.convertAndSend("/topic/chat/" + chatDto.getCode(), chatDto);
simpMessagingTemplate.convertAndSend("/topic/all/" + target.getUsername(), messageDto);
simpMessagingTemplate.convertAndSend("/topic/notification/" + target.getUsername(), notification);
}
음성 채팅 요청 후 수락 시 음성 채팅 연결
음성 채팅 1분 제한(1분 초과 시 자동으로 연결 해제)
$("#voiceChatBtn").on('click', function () {
// 사용자가 보이스 채팅 요청을 확인하면, 서버로 요청 메시지를 전송
if (confirm(targetNick + "님에게 보이스 채팅을 요청하시겠습니까?")) {
stompClient.send("/app/all/" + target, {}, JSON.stringify({
type: "requestVoice",
sender: user,
senderNick: nickName
}));
}
});
// 서버로부터의 메시지를 수신하고, 각 메시지 유형에 따라 적절한 처리 수행
stompClient.subscribe("/topic/all/" + $("#hiddenUserName").val(), function (message) {
const data = JSON.parse(message.body);
// 다른 사용자로부터의 보이스 채팅 요청 처리
if (data.type === "requestVoice") {
if (confirm(data.senderNick + "님이 보이스 채팅을 요청했습니다. 수락하시겠습니까?")) {
// 보이스 채팅 수락: WebRTC 연결 설정 및 서버에 수락 메시지 전송
stompClient.send("/app/all/" + data.sender, {}, JSON.stringify({
type: "acceptVoiceRequest",
sender: $("#hiddenUserName").val()
}));
targetPeer = data.sender;
pc = new RTCPeerConnection(iceServers);
pc.addEventListener('icecandidate', function (event) {
if (event.candidate) {
stompClient.send("/app/peer/candidate/" + targetPeer, {}, JSON.stringify(event.candidate));
}
});
pc.ontrack = function (event) {
if (event.track.kind === 'audio') {
const remoteAudio = document.getElementById('voiceChatPlayer');
remoteAudio.srcObject = event.streams[0];
}
};
} else {
// 보이스 채팅 거절: 서버에 거절 메시지 전송
stompClient.send("/app/all/" + data.sender, {}, JSON.stringify({
type: "refuse",
content: data.senderNick + "님이 보이스 채팅 요청을 거절하였습니다."
}));
}
}
// 기타 보이스 채팅 관련 메시지 처리
});
<div class="card" id="plazaBoard">
<div th:each="online : ${onlinePlazaUsers}" th:class="|avatar ${online.username}|"
th:classappend="${online.plazaFocus.equals('focus')} ? 'statusFocus' : 'statusBlur'">
<div class="talkBox card" th:style="|background:${online.chatBackground}; color:${online.chatColor}|"></div>
<img class="userImage" th:src="${online.image}">
<div class="d-flex">
<div class="nickname" th:text="${online.nickName}"></div>
</div>
<div class="status" th:classappend="${online.plazaFocus.equals('focus')} ? 'focus' : 'blur'"></div>
<input type="hidden" th:value="${online.locationTop}">
<input type="hidden" th:value="${online.locationLeft}">
</div>
<div id="plazaChatContainer">
<div id="plazaChatBoard"></div>
<form id="plazaChatForm">
<input type="text" class="form-control" id="plazaChatParam">
</form>
</div>
</div>
<script th:inline="javascript">
const user = [[${user.username}]];
const nick = [[${user.nickName}]];
const manager = [[${plaza.manager.username}]];
const code = [[${plaza.code}]];
let background = [[${plaza.background}]];
</script>
const socket = new SockJS("/ws");
const stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
// 사용자가 광장에 참여할 때의 처리
stompClient.subscribe("/topic/plaza/join/" + code, function (message) {
const data = JSON.parse(message.body);
if ($("." + data.username).length === 0) {
createAvatar(data);
createParticipantList(data);
}
});
// 사용자가 광장에서 나갈 때의 처리
stompClient.subscribe("/topic/plaza/exit/" + code, function (message) {
$("." + message.body).remove();
$(".p_" + message.body).remove();
});
// 기타 이벤트 구독
});
function moveLocation(data) {
const element = $("." + data.sender);
const parentWidth = parseInt(element.parent().css("width"));
const parentHeight = parseInt(element.parent().css("height"));
const top = parseInt(data.top) * parentHeight / 100 + 5;
const left = parseInt(data.left) * parentWidth / 100 + 5;
element.css({
top: top,
left: left
});
}
function showMessage(data) {
const element = $("." + data.sender + " > .talkBox");
const boardMessage = $("<p class='normalMessage'></p>");
boardMessage.text(data.nick + ": " + data.content);
element.text(data.content);
element.show();
if (timer.has(data.sender)) clearTimeout(timer.get(data.sender));
timer.set(data.sender, setTimeout(function () {
element.hide();
}, 3000));
$("#plazaChatBoard").append(boardMessage);
const chatBoard = document.getElementById("plazaChatBoard");
chatBoard.scrollTop = chatBoard.scrollHeight;
}
$(window).blur(function () {
stompClient.send("/app/plaza/focus/" + code, {}, JSON.stringify({
sender: user,
status: "blur"
}));
});
$(window).focus(function () {
stompClient.send("/app/plaza/focus/" + code, {}, JSON.stringify({
sender: user,
status: "focus"
}));
});
$("#plazaBoard").click(function (e) {
const offset = $(this).offset();
const clickX = e.pageX - offset.left;
const clickY = e.pageY - offset.top;
sendLocationOnClick(clickX - 100, clickY - 25);
});
$("#participant-tab-pane").on("click", ".fire", function () {
if (confirm($(this).data("nick") + "님을 강퇴하시겠습니까?")) {
stompClient.send("/app/plaza/fire/" + code, {}, $(this).data("value"));
}
});
$("#plazaManageBtn").click(function () {
$("#plazaManageModal").modal("show");
});
$('.araQBtn').on('click', function () {
var nickname = $(this).data("nick");
var username = $(this).data("user");
// 사용자에게 아라큐 요청 전송 확인
if (confirm(nickname + "님에게 아라큐 요청을 보냅니다!")) {
// 서버에 아라큐 요청 전송
$.ajax({
url: "/like/request",
type: "POST",
headers: {
[csrfHeader]: csrfToken
},
contentType: "text/plain",
data: username,
success: function (response) {
// 요청 결과에 대한 알림
alert(response);
location.reload();
},
error: function (error) {
// 오류 처리
console.error(error);
}
});
}
});
@PostMapping("/request")
@ResponseBody
public String like(@RequestBody String username, Principal principal) {
// 현재 사용자와 '좋아요' 대상 사용자 정보 조회
SiteUser user = this.userService.getByUsername(principal.getName());
SiteUser likedUser = this.userService.getByUsername(username);
// 이미 요청이 있었는지 확인
if (this.likeService.getListForCheck(likedUser, user) != null) {
return "이미 " + likedUser.getNickName() + "님께 아라큐 요청을 받았습니다. 매칭 상태를 확인해주세요.";
}
else if (this.likeService.getListForCheck(user, likedUser) != null) {
return "이미 " + likedUser.getNickName() + "님께 아라큐 요청을 보냈습니다. 매칭 상태를 확인해주세요.";
}
// 아라큐 신청권 확인 및 사용
else if (user.getAraQPass() == 0) return "아라큐 신청권이 필요합니다.";
this.likeService.likeUser(user, likedUser);
this.userService.useAraQPass(user);
// 알림 생성 및 전송
Notification notification = new Notification("아라큐 요청", user.getNickName() + "님이 아라큐 요청을 보냈습니다.",
user.getUsername(), username, "#");
simpMessagingTemplate.convertAndSend("/topic/notification/" + username, notification);
return "성공적으로 요청되었습니다.";
}