Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions components/ChatInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const ChatInput = forwardRef(
setShowMentionList = () => {},
setMentionFilter = () => {},
setMentionIndex = () => {},
room = null, // room prop 추가
room = null,
},
ref
) => {
Expand All @@ -45,11 +45,11 @@ const ChatInput = forwardRef(
const messageInputRef = ref || internalInputRef;
const filesRef = useRef([]);
const [files, setFiles] = useState([]);
const uploading = false;
const uploadProgress = 0;
// [수정] 로컬 uploading 상태 제거 (props로 전달받은 externalUploading 사용)
const [uploadError, setUploadError] = useState(null);
const [isDragging, setIsDragging] = useState(false);

// ... (handleFileValidationAndPreview, handleFileRemove, handleFileDrop 로직 동일)
const handleFileValidationAndPreview = useCallback(
async (file) => {
if (!file) return;
Expand Down Expand Up @@ -114,36 +114,44 @@ const ChatInput = forwardRef(
async (e) => {
e?.preventDefault();

// [수정] await 및 성공 여부 확인 후 초기화
if (files.length > 0) {
try {
const file = files[0];
if (!file || !file.file) {
throw new Error("파일이 선택되지 않았습니다.");
}

onSubmit({
const success = await onSubmit({
type: "file",
content: message.trim(),
fileData: file,
});

setMessage("");
setFiles([]);
if (success) {
setMessage("");
setFiles([]);
}
} catch (error) {
console.error("File submit error:", error);
setUploadError(error.message);
}
} else if (message.trim()) {
onSubmit({
const success = await onSubmit({
type: "text",
content: message.trim(),
});
setMessage("");

if (success) {
setMessage("");
}
}
},
[files, message, onSubmit, setMessage]
);

// ... (useEffect, handleInputChange, handleMentionSelect, handleKeyDown 등 나머지 로직 동일)

useEffect(() => {
const handleClickOutside = (event) => {
if (
Expand Down Expand Up @@ -233,12 +241,7 @@ const ChatInput = forwardRef(

setShowMentionList(false);
},
[
onMessageChange,
setMentionFilter,
setShowMentionList,
setMentionIndex,
]
[onMessageChange, setMentionFilter, setShowMentionList, setMentionIndex]
);

const handleMentionSelect = useCallback(
Expand Down Expand Up @@ -334,7 +337,7 @@ const ChatInput = forwardRef(
setMentionIndex,
setShowMentionList,
setShowEmojiPicker,
room, // room 의존성 추가
room,
]
);

Expand Down Expand Up @@ -370,7 +373,8 @@ const ChatInput = forwardRef(
setShowEmojiPicker((prev) => !prev);
}, [setShowEmojiPicker]);

const isDisabled = disabled || uploading || externalUploading;
// [수정] externalUploading(props)만 사용
const isDisabled = disabled || externalUploading;

return (
<>
Expand Down Expand Up @@ -399,8 +403,8 @@ const ChatInput = forwardRef(
<Box className="absolute bottom-full left-0 right-0 mb-2 z-1000">
<FilePreview
files={files}
uploading={uploading}
uploadProgress={uploadProgress}
uploading={externalUploading} // [수정] externalUploading 전달
uploadProgress={0} // 진행률은 별도 prop으로 받거나 단순 로딩 처리
uploadError={uploadError}
onRemove={handleFileRemove}
onRetry={() => setUploadError(null)}
Expand Down
39 changes: 30 additions & 9 deletions components/ChatMessages.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useCallback, useMemo } from "react";
import React, { useCallback, useMemo, useEffect, useRef } from "react";
import { Text, VStack } from "@vapor-ui/core";
import SystemMessage from "./SystemMessage";
import FileMessage from "./FileMessage";
import UserMessage from "./UserMessage";
import { useInfiniteScroll } from "../hooks/useInfiniteScroll";
import { useAutoScroll } from "../hooks/useAutoScroll";
import { useInfiniteScroll } from "../hooks/useInfiniteScroll";

const LoadingIndicator = React.memo(() => (
<div className="loading-messages">
Expand Down Expand Up @@ -49,12 +49,7 @@ const ChatMessages = ({
onLoadMore = () => {},
socketRef,
}) => {
// 무한 스크롤 훅
const { sentinelRef } = useInfiniteScroll(
onLoadMore,
hasMoreMessages,
loadingMessages
);
const initialScrollDoneRef = useRef(false);

// 자동 스크롤 훅 (스크롤 복원 기능 포함)
const { containerRef, scrollToBottom } = useAutoScroll(
Expand All @@ -63,6 +58,33 @@ const ChatMessages = ({
loadingMessages,
100 // 하단 100px 이내면 자동 스크롤
);

// 무한 스크롤 훅 - 스크롤 컨테이너를 root로 지정하여 상단 sentinel 관찰
const { sentinelRef } = useInfiniteScroll(
onLoadMore,
hasMoreMessages,
loadingMessages,
{
rootRef: containerRef,
// 약간의 여유를 두고 호출하여 빠르게 이전 메시지를 불러올 수 있도록 설정
rootMargin: "20px",
threshold: 0,
}
);

// 초기 로드/재접속 후 메시지가 준비되면 바로 최신 위치로 스크롤
useEffect(() => {
if (loadingMessages) return;
if (!containerRef?.current) return;
if (messages.length === 0) {
initialScrollDoneRef.current = false;
return;
}
if (!initialScrollDoneRef.current) {
initialScrollDoneRef.current = true;
scrollToBottom("auto");
}
}, [messages.length, loadingMessages, scrollToBottom, containerRef]);
const isMine = useCallback(
(msg) => {
if (!msg?.sender || !currentUser?.id) return false;
Expand All @@ -82,7 +104,6 @@ const ChatMessages = ({
return messages;
}, [messages]);


const renderMessage = useCallback(
(msg, idx) => {
if (!msg) return null;
Expand Down
9 changes: 6 additions & 3 deletions components/ChatRoomInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const ChatRoomInfo = ({ room, connectionStatus }) => {
const maxVisibleAvatars = 3;
const remainingCount = Math.max(0, participants.length - maxVisibleAvatars);

const getParticipantKey = (participant, index) =>
participant?._id || participant?.id || participant?.email || `participant-${index}`;

return (
<Collapsible.Root>
<HStack
Expand All @@ -47,7 +50,7 @@ const ChatRoomInfo = ({ room, connectionStatus }) => {
<div className="flex -space-x-2">
{participants.slice(0, maxVisibleAvatars).map((participant, index) => (
<div
key={participant._id}
key={getParticipantKey(participant, index)}
className="ring-1 rounded-full"
style={{ zIndex: maxVisibleAvatars - index }}
>
Expand Down Expand Up @@ -109,9 +112,9 @@ const ChatRoomInfo = ({ room, connectionStatus }) => {
참여자 목록
</Text>
<div className="max-h-64 overflow-y-auto">
{participants.map((participant) => (
{participants.map((participant, index) => (
<HStack
key={participant._id}
key={getParticipantKey(participant, index)}
gap="$200"
alignItems="center"
className="px-2 py-2 hover:bg-background-contrast-100 rounded-lg transition-colors"
Expand Down
13 changes: 3 additions & 10 deletions components/CustomAvatar.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback, forwardRef } from 'react';
import { Avatar } from '@vapor-ui/core';
import { generateColorFromEmail, getContrastTextColor } from '@/utils/colorUtils';
import fileService from '@/services/fileService';

/**
* CustomAvatar 컴포넌트
Expand Down Expand Up @@ -38,17 +39,9 @@ const CustomAvatar = forwardRef(({

// 프로필 이미지 URL 생성 (memoized)
const getImageUrl = useCallback((imagePath) => {
// src prop이 직접 제공된 경우
if (src) return src;

if (!imagePath) return null;

// 이미 전체 URL인 경우
if (imagePath.startsWith('http')) {
return imagePath;
}
// API URL과 결합 필요한 경우
return `${process.env.NEXT_PUBLIC_API_URL}${imagePath}`;
const resolved = fileService.getFileUrl(imagePath);
return resolved || null;
}, [src]);

// persistent 모드: 프로필 이미지 URL 처리
Expand Down
Loading
Loading