메시지 읽은 시간 표시
내 메시지가 상대방에게 읽혔을 때 "읽은 시간" 표시하는 기능입니다.
읽은 시간을 표시하면 좋은 이유는 상대방이 메시지를 확인했는지 명확히 알 수 있기 때문입니다.
읽은 시간 표시를 통해 상대방이 내 메시지를 봤는지 확인 가능하고
“읽음” 표시만으로는 부족할 때 구체적인 시간이 신뢰감을 줍니다.(예: 읽음 · 5분 전)
상대가 응답이 없을 때도, '아, 아직 읽지 않았구나', '읽긴 했지만 바쁜가 보네' 등 합리적인 추측 가능합니다.
그러므로 읽음 시간은 작지만 신뢰감과 사용자 만족도를 높이는 핵심 요소 중 하나입니다.
메시지 읽은 시간 표시
Firestore의 chatRooms 컬렉션의 messages 서브 컬렉션 문서의 readAt 필드를 사용합니다.
메시지를 보낼 때 readAt은 null 로 저장이 되고
채팅룸에 들어갈 때 readAt이 null인 안 읽은 메시지 읽음 처리합니다.
읽음 처리하는 방법은 1:1 채팅이므로 채팅룸의 userIds 배열에서 serderId가 내가 아니고,
readAt이 null인 경우 readAt에 현재 시간 설정하는 것입니다.
채팅룸에서 메시지 목록을 보여줄 때 readAt을 사용하여 읽은 시간을 표시합니다.
chatStore
const sendMessage = async (chatRoomId, senderId, receiverId, text) => {
const messagesRef = collection(db, 'chatRooms', chatRoomId, 'messages');
// 메시지 객체
const messageData = {
text,
senderId,
createdAt: serverTimestamp(),
readAt: null
};
await addDoc(messagesRef, messageData);
};
ChatRoom.vue의 script
- 메시지 읽음 처리
onMounted(async () => {
subscribeToChatRoom(chatRoomId);
// 내가 이 채팅방에 들어오면 안 읽은 메시지를 읽음 처리
watch(messages, async (newMessages) => {
for (const msg of newMessages) {
if (msg.senderId !== auth.user.uid && !msg.readAt) {
const msgRef = doc(db, 'chatRooms', chatRoomId, 'messages', msg.id);
await updateDoc(msgRef, {
readAt: Date.now(),
});
}
}
nextTick(() => {
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
}
});
});
});
ChatRoom.vue의 template
- 메시지 읽음 표시
- 날짜 포맷
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = timestamp;
return format(date, 'HH:mm'); // 24시간 형식 예: 14:05
};
date-fns
읽은 시간 표시 형식을 위하여 date-fns 설치해야 합니다.
date-fns는 JavaScript에서 날짜와 시간을 다룰 수 있도록 도와주는 경량의 날짜 유틸리티 라이브러리입니다.
npm install date-fns
사용예
import { format } from 'date-fns';
const now = new Date();
console.log(format(now, 'yyyy-MM-dd HH:mm')); // 2025-05-13 14:30
ChatRoom.vue
<!-- src/views/ChatRoom.vue -->
<template>
<v-container fluid>
<v-row>
<v-col cols="12" sm="10" md="8" class="mx-auto">
<h2 class="text-h6 my-4">
{{ otherUserName }}님과의 채팅
<v-chip
:color="status === 'online' ? 'green' : 'grey'"
class="ml-2"
small
text-color="white">
{{ status === 'online' ? 'Online' : `마지막 접속: ${timeAgo}` }}
</v-chip>
</h2>
<v-sheet height="60vh" class="overflow-y-auto">
<div ref="chatContainer"
class="chat-scroll-area"
style="height: 100%; overflow-y: auto;">
<div
v-for="msg in messages"
:key="msg.id"
:class="{
'justify-end': msg.senderId === auth.user.uid,
'justify-start': msg.senderId !== auth.user.uid,}"
class="d-flex">
<template v-if="msg.type === 'audio'">
<audio :src="msg.audioUrl" controls class="ma-1" />
</template>
<template v-else>
<v-sheet
max-width="60%"
:color="msg.senderId === auth.user.uid ? 'primary' : '#e1e1e1'"
:class="msg.senderId === auth.user.uid ? 'text-white' : 'text-black'"
class="pa-3 rounded-lg ma-1"
style="white-space: pre-line;"
elevation="1"
>
<div class="text-body-2">{{ msg.text }}</div>
</v-sheet>
</template>
<!-- 읽음 표시 -->
<div
v-if="msg.readAt"
class="text-caption text-grey ml-2 d-flex flex-column justify-end"
>
{{ formatTime(msg.readAt) }}
</div>
</div>
</div>
</v-sheet>
<v-form @submit.prevent="submitMessage" class="mt-4">
<v-textarea
v-model="text"
label="메시지를 입력하세요"
auto-grow
rows="1"
max-rows="5"
outlined
dense
append-inner-icon="mdi-send"
@click:append-inner="submitMessage"
/>
</v-form>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, onMounted, nextTick, watch } from 'vue';
import { db, storage } from '@/firebase';
import { updateDoc, doc,
addDoc, collection, serverTimestamp
} from 'firebase/firestore';
import { useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/authStore';
import { useChatStore } from '@/stores/chatStore';
import { useChat } from '@/composables/useChat';
import { useProfileStatus } from '@/composables/useProfileStatus'
import { useTimeAgo } from '@/composables/useTimeAgo'; // 경로에 맞게!
import { format } from 'date-fns'; // 설치 필요 npm i date-fns
const auth = useAuthStore();
const route = useRoute();
const chatRoomId = route.query.roomId;
const otherUserName = route.query.otherUserName || '상대방';
const otherUserId = route.query.otherUserId
const chat = useChatStore();
const { status, lastSeen } = useProfileStatus(otherUserId)
const { timeAgo } = useTimeAgo(lastSeen);
const text = ref('');
const chatContainer = ref(null);
const { messages, subscribeToChatRoom } = useChat(auth.user.uid);
const submitMessage = async () => {
if (!text.value.trim()) return;
await chat.sendMessage(chatRoomId, auth.user.uid, otherUserId, text.value);
text.value = '';
nextTick(() => {
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
}
});
};
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = timestamp;
return format(date, 'HH:mm'); // 24시간 형식 예: 14:05
};
onMounted(async () => {
subscribeToChatRoom(chatRoomId);
// 내가 이 채팅방에 들어오면 안 읽은 메시지를 읽음 처리
watch(messages, async (newMessages) => {
for (const msg of newMessages) {
if (msg.senderId !== auth.user.uid && !msg.readAt) {
const msgRef = doc(db, 'chatRooms', chatRoomId, 'messages', msg.id);
await updateDoc(msgRef, {
readAt: Date.now(),
});
}
}
nextTick(() => {
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
}
});
});
});
</script>
'Vue 3 + Firebase 기반 실시간 채팅 앱 개발' 카테고리의 다른 글
16. Vue 3 + Firebase 기반 채팅 앱 개발 - 읽지 않은 메시지 수 배지 표시 (0) | 2025.05.16 |
---|---|
15. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 메시지 읽은 시간 표시 (0) | 2025.05.15 |
13. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 채팅 상대의 온라인 상태 (0) | 2025.05.12 |
12. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 채팅룸 (0) | 2025.05.11 |
11. Vue 3 + Firebase 기반 실시간 채팅 앱 v-chat 개발 - 실시간 채팅 (0) | 2025.05.09 |