[RAPID-micro] 새 대화 모달 z-index 메신저 위로 상향 (10000/10001)

[RAPID-fix] 메신저 사용자 목록 회사 전환 시 캐시 격리

- useRooms/useCompanyUsers queryKey에 companyCode 포함
- 회사 전환 시 다른 회사 사용자가 캐시에서 노출되던 문제 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

[RAPID-fix] 메신저 버그 수정 (8건)

- 방 생성 후 자동 입장 + 커서 포커스
- DM 헤더 상대방 이름, 그룹 "이름1, 이름2 외 N명" 표시
- 채팅방 이름 인라인 수정 기능 추가
- Socket.IO join_rooms 누락 수정 → 실시간 메시지 수신 정상화
- new_message 이벤트 수신 시 React Query 캐시 무효화
- 토스트 알림 stale closure 수정 (ref 패턴 적용)
- 타이핑 이벤트명 백엔드 일치 (user_typing/user_stop_typing)
- 메시지 순서 역전 수정 (.reverse())
- unread queryKey 불일치 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

[RAPID-fix] REST API 메시지 전송 시 Socket.IO broadcast 추가

- socketManager.ts 모듈 생성 (io 전역 공유)
- sendMessage 컨트롤러에서 io.to(room).emit('new_message') broadcast
- 상대방 말풍선 너비 고정 수정 (items-start 추가)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syc0123
2026-03-30 18:41:13 +09:00
parent c9d04f3018
commit 6b3e6cce5e
10 changed files with 166 additions and 40 deletions
+61 -7
View File
@@ -1,9 +1,9 @@
"use client";
import { useEffect, useRef } from "react";
import { MessageSquare } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { MessageSquare, Pencil, Check, X } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useMessages, useMarkAsRead } from "@/hooks/useMessenger";
import { useMessages, useMarkAsRead, useUpdateRoom } from "@/hooks/useMessenger";
import { useAuth } from "@/hooks/useAuth";
import { useMessengerContext } from "@/contexts/MessengerContext";
import { useMessengerSocket } from "@/hooks/useMessengerSocket";
@@ -20,8 +20,12 @@ export function ChatPanel({ room }: ChatPanelProps) {
const { selectedRoomId } = useMessengerContext();
const { data: messages } = useMessages(selectedRoomId);
const markAsRead = useMarkAsRead();
const updateRoom = useUpdateRoom();
const { emitTypingStart, emitTypingStop, typingUsers } = useMessengerSocket();
const bottomRef = useRef<HTMLDivElement>(null);
const [isEditingName, setIsEditingName] = useState(false);
const [editName, setEditName] = useState("");
const editInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (selectedRoomId) {
@@ -60,14 +64,64 @@ export function ChatPanel({ room }: ChatPanelProps) {
return prev !== curr;
};
// Compute display name based on room type
const displayName = (() => {
if (room.type === "dm") {
const other = room.participants.find((p) => p.userId !== user?.userId);
return other?.userName ?? room.name;
}
if (room.name) return room.name;
const others = room.participants.filter((p) => p.userId !== user?.userId);
if (others.length <= 2) {
return others.map((p) => p.userName).join(", ");
}
return `${others[0].userName}, ${others[1].userName}${others.length - 2}`;
})();
return (
<div className="flex-1 flex flex-col min-w-0">
{/* Header */}
<div className="border-b px-4 py-2 flex items-center gap-2">
<h3 className="font-semibold text-sm truncate">{room.name}</h3>
<span className="text-xs text-muted-foreground">
{room.participants.length}
</span>
{isEditingName ? (
<form
className="flex items-center gap-1 flex-1 min-w-0"
onSubmit={(e) => {
e.preventDefault();
const trimmed = editName.trim();
if (trimmed && trimmed !== room.name) {
updateRoom.mutate({ roomId: room.id, name: trimmed });
}
setIsEditingName(false);
}}
>
<input
ref={editInputRef}
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="font-semibold text-sm bg-transparent border-b border-primary outline-none flex-1 min-w-0"
autoFocus
/>
<button type="submit" className="p-0.5 hover:bg-muted rounded">
<Check className="h-3.5 w-3.5 text-primary" />
</button>
<button type="button" onClick={() => setIsEditingName(false)} className="p-0.5 hover:bg-muted rounded">
<X className="h-3.5 w-3.5 text-muted-foreground" />
</button>
</form>
) : (
<>
<h3 className="font-semibold text-sm truncate">{displayName}</h3>
<button
onClick={() => { setEditName(room.name); setIsEditingName(true); }}
className="p-0.5 hover:bg-muted rounded shrink-0"
>
<Pencil className="h-3.5 w-3.5 text-muted-foreground" />
</button>
<span className="text-xs text-muted-foreground">
{room.participants.length}
</span>
</>
)}
</div>
{/* Messages */}
@@ -41,6 +41,13 @@ export function MessageInput({ roomId, onTypingStart, onTypingStop }: MessageInp
adjustHeight();
}, [text, adjustHeight]);
// Auto-focus when room changes
useEffect(() => {
if (roomId) {
textareaRef.current?.focus();
}
}, [roomId]);
const handleSend = useCallback(() => {
const trimmed = text.trim();
if (!trimmed) return;
@@ -53,7 +53,7 @@ export function MessageItem({ message, isOwn, showAvatar }: MessageItemProps) {
<div className="w-7" />
)}
<div className={cn("flex flex-col max-w-[70%]", isOwn && "items-end")}>
<div className={cn("flex flex-col max-w-[70%]", isOwn ? "items-end" : "items-start")}>
{showAvatar && !isOwn && (
<span className="text-xs font-medium text-muted-foreground mb-0.5">
{message.senderName}
+4 -4
View File
@@ -59,7 +59,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-999 bg-black/60",
"fixed inset-0 z-[10000] bg-black/60",
className,
)}
{...props}
@@ -134,13 +134,13 @@ const DialogContent = React.forwardRef<
return (
<DialogPortal container={container ?? undefined}>
<div
className={scoped ? "absolute inset-0 z-999 flex items-center justify-center overflow-hidden p-4" : undefined}
className={scoped ? "absolute inset-0 z-[10000] flex items-center justify-center overflow-hidden p-4" : undefined}
style={(hiddenProp || (scoped && !isTabActive)) ? { display: "none" } : undefined}
>
{scoped ? (
<div className="absolute inset-0 bg-black/60" />
) : (
<DialogPrimitive.Overlay className="fixed inset-0 z-999 bg-black/60" />
<DialogPrimitive.Overlay className="fixed inset-0 z-[10000] bg-black/60" />
)}
<DialogPrimitive.Content
ref={mergedRef}
@@ -149,7 +149,7 @@ const DialogContent = React.forwardRef<
className={cn(
scoped
? "bg-background relative z-1 flex w-full max-w-lg max-h-full flex-col gap-4 border p-6 shadow-lg sm:rounded-lg"
: "bg-background fixed top-[50%] left-[50%] z-1000 flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border p-6 shadow-lg sm:rounded-lg",
: "bg-background fixed top-[50%] left-[50%] z-[10001] flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border p-6 shadow-lg sm:rounded-lg",
className,
scoped && "max-h-full",
)}