Files
hjjeong 73bd4f2ba7 권한 그룹 회사 필터 제거(COMPANY_16 단독) + 메신저 사용자명 옵셔널 체이닝
- rolesList/RoleFormModal/[id] 페이지에서 회사 선택 필터·라벨·SUPER_ADMIN 회사 컬럼 제거
- MessageInput/NewRoomModal에서 user.userName/deptName.toLowerCase 호출 시 옵셔널 체이닝 적용으로 null 사용자 방어

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:39:13 +09:00

176 lines
6.0 KiB
TypeScript

"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useCompanyUsers, useCreateRoom } from "@/hooks/useMessenger";
import { useMessengerContext } from "@/contexts/MessengerContext";
import { UserAvatar } from "./UserAvatar";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
import type { UserStatus } from "@/hooks/useMessengerSocket";
interface NewRoomModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
userStatuses: Map<string, UserStatus>;
}
export function NewRoomModal({ open, onOpenChange, userStatuses }: NewRoomModalProps) {
const [tab, setTab] = useState<"dm" | "group" | "channel">("dm");
const [search, setSearch] = useState("");
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [roomName, setRoomName] = useState("");
const [channelDesc, setChannelDesc] = useState("");
const { data: users = [] } = useCompanyUsers();
const createRoom = useCreateRoom();
const { selectRoom } = useMessengerContext();
const q = search.toLowerCase();
const filtered = users.filter(
(u) =>
u.userName?.toLowerCase().includes(q) ||
u.deptName?.toLowerCase().includes(q)
);
const toggleUser = (userId: string) => {
if (tab === "dm") {
setSelectedIds([userId]);
} else {
setSelectedIds((prev) =>
prev.includes(userId) ? prev.filter((id) => id !== userId) : [...prev, userId]
);
}
};
const handleCreate = async () => {
try {
const room = await createRoom.mutateAsync({
type: tab,
name: tab === "channel" ? roomName : tab === "group" ? roomName : undefined,
description: tab === "channel" ? channelDesc : undefined,
participantIds: selectedIds,
});
selectRoom(room.id);
onOpenChange(false);
reset();
} catch {
// handled by query
}
};
const reset = () => {
setSearch("");
setSelectedIds([]);
setRoomName("");
setChannelDesc("");
};
const canCreate =
selectedIds.length > 0 &&
(tab === "dm" || tab === "group" || (tab === "channel" && roomName.trim()));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<Tabs value={tab} onValueChange={(v) => { setTab(v as typeof tab); reset(); }}>
<TabsList className="w-full">
<TabsTrigger value="dm" className="flex-1">DM</TabsTrigger>
<TabsTrigger value="group" className="flex-1"></TabsTrigger>
<TabsTrigger value="channel" className="flex-1"></TabsTrigger>
</TabsList>
{(tab === "group" || tab === "channel") && (
<div className="mt-3 space-y-2">
<Input
placeholder={tab === "group" ? "그룹 이름" : "채널 이름"}
value={roomName}
onChange={(e) => setRoomName(e.target.value)}
/>
{tab === "channel" && (
<Input
placeholder="채널 설명 (선택)"
value={channelDesc}
onChange={(e) => setChannelDesc(e.target.value)}
/>
)}
</div>
)}
<TabsContent value={tab} className="mt-3">
<Input
placeholder="사용자 검색..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="mb-2"
/>
{selectedIds.length > 0 && tab !== "dm" && (
<div className="flex flex-wrap gap-1 mb-2">
{selectedIds.map((id) => {
const u = users.find((x) => x.userId === id);
return u ? (
<span
key={id}
onClick={() => toggleUser(id)}
className="inline-flex items-center gap-1 rounded-full bg-primary/10 text-primary text-xs px-2 py-0.5 cursor-pointer hover:bg-primary/20"
>
{u.userName} &times;
</span>
) : null;
})}
</div>
)}
<ScrollArea className="h-60">
{filtered.map((u) => {
const selected = selectedIds.includes(u.userId);
return (
<button
key={u.userId}
onClick={() => toggleUser(u.userId)}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted text-left",
selected && "bg-accent"
)}
>
<UserAvatar photo={u.photo} name={u.userName} size="sm" status={userStatuses.get(u.userId) ?? "offline"} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{u.userName}</div>
{u.deptName && (
<div className="text-xs text-muted-foreground truncate">
{u.deptName} {u.positionName && `/ ${u.positionName}`}
</div>
)}
</div>
{selected && <Check className="h-4 w-4 text-primary shrink-0" />}
</button>
);
})}
</ScrollArea>
</TabsContent>
</Tabs>
<div className="flex justify-end mt-2">
<Button onClick={handleCreate} disabled={!canCreate || createRoom.isPending} size="sm">
{createRoom.isPending ? "생성 중..." : "대화 시작"}
</Button>
</div>
</DialogContent>
</Dialog>
);
}