Skip to content

Commit 20d2398

Browse files
committed
feat/5-delete-teamchat
1 parent 8d137b0 commit 20d2398

2 files changed

Lines changed: 165 additions & 147 deletions

File tree

src/app/components/TeamInviteModal.tsx

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from "react";
22
import type { ReactNode } from "react";
33
import { AlertCircle, ChevronRight, Copy, Folder, Globe2, Link2, Lock, Play, Trash2, X } from "lucide-react";
4+
import { AnimatePresence, motion } from "motion/react";
45

56
interface TeamInviteModalProps {
67
isOpen: boolean;
@@ -33,6 +34,7 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
3334
const [inviteValue, setInviteValue] = useState("");
3435
const [members, setMembers] = useState(initialMembers);
3536
const [copyState, setCopyState] = useState<"idle" | "copied">("idle");
37+
const [confirmTarget, setConfirmTarget] = useState<TeamMember | null>(null);
3638

3739
if (!isOpen) return null;
3840

@@ -74,8 +76,15 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
7476
);
7577
};
7678

77-
const handleRemoveMember = (memberId: string) => {
78-
setMembers((prev) => prev.filter((member) => member.id !== memberId || member.role === "owner"));
79+
const handleRemoveClick = (member: TeamMember) => {
80+
if (member.role === "owner") return;
81+
setConfirmTarget(member);
82+
};
83+
84+
const handleConfirmRemove = () => {
85+
if (!confirmTarget) return;
86+
setMembers((prev) => prev.filter((member) => member.id !== confirmTarget.id));
87+
setConfirmTarget(null);
7988
};
8089

8190
return (
@@ -232,7 +241,7 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
232241
</select>
233242
<button
234243
type="button"
235-
onClick={() => handleRemoveMember(member.id)}
244+
onClick={() => handleRemoveClick(member)}
236245
className="flex h-8 w-8 items-center justify-center rounded-lg border-0"
237246
style={{
238247
background: "rgba(239, 68, 68, 0.08)",
@@ -271,6 +280,79 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
271280
/>
272281
</div>
273282
</div>
283+
284+
<AnimatePresence>
285+
{confirmTarget && (
286+
<motion.div
287+
className="fixed inset-0 z-[60] flex items-center justify-center px-4"
288+
style={{ background: "rgba(0, 0, 0, 0.55)", backdropFilter: "blur(6px)" }}
289+
initial={{ opacity: 0 }}
290+
animate={{ opacity: 1 }}
291+
exit={{ opacity: 0 }}
292+
onClick={() => setConfirmTarget(null)}
293+
>
294+
<motion.div
295+
className="w-full max-w-[400px] rounded-[22px] px-7 py-7"
296+
style={{
297+
background: "#ffffff",
298+
boxShadow: "0 28px 80px rgba(0, 0, 0, 0.40)",
299+
}}
300+
initial={{ opacity: 0, scale: 0.94, y: 12 }}
301+
animate={{ opacity: 1, scale: 1, y: 0 }}
302+
exit={{ opacity: 0, scale: 0.94, y: 12 }}
303+
transition={{ type: "spring", stiffness: 380, damping: 30 }}
304+
onClick={(e) => e.stopPropagation()}
305+
>
306+
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl" style={{
307+
background: "rgba(239, 68, 68, 0.10)",
308+
border: "1px solid rgba(239, 68, 68, 0.25)"
309+
}}>
310+
<Trash2 size={22} style={{ color: "#ef4444" }} />
311+
</div>
312+
313+
<h3 className="m-0 mb-2 tracking-tight" style={{ color: "#111827", fontSize: 18, fontWeight: 900 }}>
314+
정말 추방하시겠어요?
315+
</h3>
316+
<p className="m-0 mb-6 tracking-tight" style={{ color: "#6b7280", fontSize: 14, fontWeight: 700, lineHeight: 1.6 }}>
317+
<span style={{ color: "#111827", fontWeight: 900 }}>{confirmTarget.name}</span>을(를) 팀에서
318+
제거합니다. 이 작업은 되돌릴 수 없습니다.
319+
</p>
320+
321+
<div className="flex gap-3">
322+
<button
323+
type="button"
324+
onClick={() => setConfirmTarget(null)}
325+
className="flex-1 rounded-xl border-0 py-3 tracking-tight transition-all hover:scale-[1.02]"
326+
style={{
327+
background: "#f3f4f6",
328+
color: "#374151",
329+
cursor: "pointer",
330+
fontSize: 14,
331+
fontWeight: 900
332+
}}
333+
>
334+
취소
335+
</button>
336+
<button
337+
type="button"
338+
onClick={handleConfirmRemove}
339+
className="flex-1 rounded-xl border-0 py-3 tracking-tight transition-all hover:scale-[1.02]"
340+
style={{
341+
background: "linear-gradient(135deg, #ef4444, #b91c1c)",
342+
color: "#ffffff",
343+
cursor: "pointer",
344+
fontSize: 14,
345+
fontWeight: 900,
346+
boxShadow: "0 4px 16px rgba(239, 68, 68, 0.30)"
347+
}}
348+
>
349+
추방하기
350+
</button>
351+
</div>
352+
</motion.div>
353+
</motion.div>
354+
)}
355+
</AnimatePresence>
274356
</div>
275357
);
276358
}

src/app/components/TeamPanel.tsx

Lines changed: 80 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ const initialTeamMembers: TeamMember[] = [
190190
export function TeamPanel({ onInvite, onOpenChannel }: TeamPanelProps) {
191191
const [members, setMembers] = useState<TeamMember[]>(initialTeamMembers);
192192
const [activeRoomId, setActiveRoomId] = useState(teamRooms[1].id);
193-
const [notice, setNotice] = useState("팀원 역할 수정, 삭제, 채팅방 이동이 가능합니다.");
193+
const [notice, setNotice] = useState("팀원 역할 수정 및 삭제가 가능합니다.");
194+
const [confirmTarget, setConfirmTarget] = useState<TeamMember | null>(null);
194195
const activeRoom = teamRooms.find((room) => room.id === activeRoomId) ?? teamRooms[0];
195196
const onlineCount = members.filter((member) => member.online).length;
196197
const activityItems = useMemo(() => {
@@ -219,15 +220,19 @@ export function TeamPanel({ onInvite, onOpenChannel }: TeamPanelProps) {
219220
setNotice(`${target?.name ?? "팀원"} 역할을 ${nextRole}(으)로 변경했습니다.`);
220221
};
221222

222-
const handleDeleteMember = (memberId: string) => {
223-
const target = members.find((member) => member.id === memberId);
224-
if (!target || target.protected) {
223+
const handleDeleteClick = (member: TeamMember) => {
224+
if (member.protected) {
225225
setNotice("팀 리드는 현재 화면에서 삭제할 수 없습니다.");
226226
return;
227227
}
228+
setConfirmTarget(member);
229+
};
228230

229-
setMembers((prev) => prev.filter((member) => member.id !== memberId));
230-
setNotice(`${target.name} 팀원을 삭제했습니다.`);
231+
const handleConfirmDelete = () => {
232+
if (!confirmTarget) return;
233+
setMembers((prev) => prev.filter((member) => member.id !== confirmTarget.id));
234+
setNotice(`${confirmTarget.name} 팀원을 추방했습니다.`);
235+
setConfirmTarget(null);
231236
};
232237

233238
return (
@@ -247,7 +252,7 @@ export function TeamPanel({ onInvite, onOpenChannel }: TeamPanelProps) {
247252
248253
</h2>
249254
<p className="m-0 mt-2 tracking-tight" style={{ color: "var(--muted)", fontSize: 14, fontWeight: 800 }}>
250-
{members.length}명 · {onlineCount}명 접속 중 · 채팅방 {teamRooms.length}
255+
{members.length}명 · {onlineCount}명 접속 중
251256
</p>
252257
</div>
253258

@@ -282,142 +287,6 @@ export function TeamPanel({ onInvite, onOpenChannel }: TeamPanelProps) {
282287
{notice}
283288
</div>
284289

285-
<section
286-
className="mb-5 overflow-hidden rounded-[26px]"
287-
style={{
288-
background: "rgba(8, 17, 31, 0.74)",
289-
border: "1px solid rgba(32, 227, 255, 0.18)",
290-
boxShadow: "0 20px 56px rgba(0, 0, 0, 0.30), inset 0 1px 0 rgba(255,255,255,0.06)",
291-
backdropFilter: "blur(18px) saturate(160%)"
292-
}}
293-
>
294-
<div className="flex flex-wrap items-center justify-between gap-3 px-5 py-4" style={{ borderBottom: "1px solid rgba(32, 227, 255, 0.12)" }}>
295-
<div>
296-
<h3 className="m-0 tracking-tight" style={{ color: "var(--white)", fontSize: 18, fontWeight: 950 }}>
297-
팀 채팅방
298-
</h3>
299-
<p className="m-0 mt-1 tracking-tight" style={{ color: "var(--muted)", fontSize: 13, fontWeight: 800 }}>
300-
팀 탭에서도 바로 채팅방을 확인하고 이동할 수 있습니다.
301-
</p>
302-
</div>
303-
<span className="inline-flex items-center gap-2 rounded-full px-3 py-1.5" style={{
304-
background: "rgba(57, 255, 136, 0.10)",
305-
border: "1px solid rgba(57, 255, 136, 0.20)",
306-
color: "var(--matrix-green)",
307-
fontSize: 12,
308-
fontWeight: 950
309-
}}>
310-
<Users size={14} />
311-
{onlineCount}명 온라인
312-
</span>
313-
</div>
314-
315-
<div className="grid gap-0 xl:grid-cols-[minmax(0,1fr)_360px]">
316-
<div className="grid gap-3 p-5 md:grid-cols-2">
317-
{teamRooms.map((room) => {
318-
const Icon = room.icon;
319-
const isActive = room.id === activeRoomId;
320-
321-
return (
322-
<button
323-
key={room.id}
324-
type="button"
325-
onClick={() => handleOpenRoom(room)}
326-
className="group rounded-2xl p-4 text-left transition-all hover:translate-y-[-2px]"
327-
style={{
328-
background: isActive ? `${room.accent}18` : "rgba(234, 247, 255, 0.045)",
329-
border: isActive ? `1px solid ${room.accent}66` : "1px solid rgba(234, 247, 255, 0.10)",
330-
boxShadow: isActive ? `0 0 26px ${room.accent}1f` : "none",
331-
color: "var(--white)",
332-
cursor: "pointer"
333-
}}
334-
>
335-
<div className="mb-4 flex items-start justify-between gap-3">
336-
<div className="flex items-center gap-3">
337-
<span className="flex h-11 w-11 items-center justify-center rounded-2xl" style={{
338-
background: `${room.accent}18`,
339-
border: `1px solid ${room.accent}44`,
340-
color: room.accent
341-
}}>
342-
<Icon size={20} />
343-
</span>
344-
<div className="min-w-0">
345-
<p className="m-0 truncate tracking-tight" style={{ color: "var(--white)", fontSize: 15, fontWeight: 950 }}>
346-
{room.name}
347-
</p>
348-
<p className="m-0 mt-1 truncate tracking-tight" style={{ color: "var(--muted)", fontSize: 12, fontWeight: 800 }}>
349-
{room.description}
350-
</p>
351-
</div>
352-
</div>
353-
{room.unread > 0 && (
354-
<span className="rounded-full px-2 py-0.5" style={{
355-
background: "var(--neon-cyan)",
356-
color: "#021014",
357-
fontSize: 11,
358-
fontWeight: 950
359-
}}>
360-
{room.unread}
361-
</span>
362-
)}
363-
</div>
364-
365-
<p className="m-0 line-clamp-2 tracking-tight" style={{ color: "var(--soft-mint)", fontSize: 13, fontWeight: 850, lineHeight: 1.55 }}>
366-
{room.lastMessage}
367-
</p>
368-
<div className="mt-4 flex items-center justify-between gap-3">
369-
<span className="inline-flex items-center gap-1.5" style={{ color: "var(--muted)", fontSize: 12, fontWeight: 800 }}>
370-
<Clock3 size={13} />
371-
{room.updatedAt}
372-
</span>
373-
<span className="inline-flex items-center gap-1.5" style={{ color: room.accent, fontSize: 12, fontWeight: 950 }}>
374-
입장
375-
<ArrowRight size={14} className="transition-transform group-hover:translate-x-1" />
376-
</span>
377-
</div>
378-
</button>
379-
);
380-
})}
381-
</div>
382-
383-
<aside className="p-5 xl:border-l" style={{ borderColor: "rgba(32, 227, 255, 0.12)" }}>
384-
<div className="rounded-2xl px-4 py-4" style={{
385-
background: "rgba(5, 11, 20, 0.46)",
386-
border: "1px solid rgba(32, 227, 255, 0.14)"
387-
}}>
388-
<div className="mb-4 flex items-center justify-between gap-3">
389-
<div>
390-
<p className="m-0 tracking-tight" style={{ color: "var(--white)", fontSize: 15, fontWeight: 950 }}>
391-
{activeRoom.name} 미리보기
392-
</p>
393-
<p className="m-0 mt-1 tracking-tight" style={{ color: "var(--muted)", fontSize: 12, fontWeight: 800 }}>
394-
{activeRoom.online}명 대화 중
395-
</p>
396-
</div>
397-
<span className="h-3 w-3 rounded-full" style={{ background: activeRoom.accent, boxShadow: `0 0 16px ${activeRoom.accent}` }} />
398-
</div>
399-
400-
<div className="grid gap-3">
401-
{roomPreviewMessages.map((message) => (
402-
<div key={`${message.author}-${message.time}`} className="rounded-2xl px-3 py-3" style={{
403-
background: "rgba(234, 247, 255, 0.055)",
404-
border: "1px solid rgba(234, 247, 255, 0.08)"
405-
}}>
406-
<div className="mb-1 flex items-center justify-between gap-2">
407-
<span style={{ color: "var(--white)", fontSize: 12, fontWeight: 950 }}>{message.author}</span>
408-
<span className="font-mono" style={{ color: "var(--muted)", fontSize: 10, fontWeight: 800 }}>{message.time}</span>
409-
</div>
410-
<p className="m-0 tracking-tight" style={{ color: "var(--soft-mint)", fontSize: 12, fontWeight: 800, lineHeight: 1.55 }}>
411-
{message.text}
412-
</p>
413-
</div>
414-
))}
415-
</div>
416-
</div>
417-
</aside>
418-
</div>
419-
</section>
420-
421290
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
422291
{members.map((member) => (
423292
<article
@@ -469,7 +338,7 @@ export function TeamPanel({ onInvite, onOpenChannel }: TeamPanelProps) {
469338
</span>
470339
<button
471340
type="button"
472-
onClick={() => handleDeleteMember(member.id)}
341+
onClick={() => handleDeleteClick(member)}
473342
disabled={member.protected}
474343
className="flex h-8 w-8 items-center justify-center rounded-lg transition-all hover:scale-105"
475344
style={{
@@ -568,6 +437,73 @@ export function TeamPanel({ onInvite, onOpenChannel }: TeamPanelProps) {
568437
})}
569438
</div>
570439
</section>
440+
441+
{confirmTarget && (
442+
<div
443+
className="fixed inset-0 z-50 flex items-center justify-center px-4"
444+
style={{ background: "rgba(0, 0, 0, 0.65)", backdropFilter: "blur(8px)" }}
445+
onClick={() => setConfirmTarget(null)}
446+
>
447+
<div
448+
className="w-full max-w-[420px] rounded-[24px] px-8 py-8"
449+
style={{
450+
background: "linear-gradient(135deg, rgba(11, 22, 40, 0.98), rgba(5, 11, 20, 0.98))",
451+
border: "1px solid rgba(255, 107, 107, 0.35)",
452+
boxShadow: "0 28px 80px rgba(0, 0, 0, 0.55), 0 0 40px rgba(255, 107, 107, 0.10)",
453+
}}
454+
onClick={(e) => e.stopPropagation()}
455+
>
456+
<div className="mb-5 flex h-14 w-14 items-center justify-center rounded-2xl" style={{
457+
background: "rgba(255, 107, 107, 0.12)",
458+
border: "1px solid rgba(255, 107, 107, 0.30)"
459+
}}>
460+
<Trash2 size={26} style={{ color: "#FF6B6B" }} />
461+
</div>
462+
463+
<h3 className="m-0 mb-2 tracking-tight" style={{ color: "var(--white)", fontSize: 20, fontWeight: 950 }}>
464+
정말 추방하시겠어요?
465+
</h3>
466+
<p className="m-0 mb-7 tracking-tight" style={{ color: "var(--muted)", fontSize: 14, fontWeight: 800, lineHeight: 1.6 }}>
467+
<span style={{ color: "var(--white)", fontWeight: 950 }}>{confirmTarget.name}</span> 팀원을 워크스페이스에서
468+
추방합니다. 이 작업은 되돌릴 수 없습니다.
469+
</p>
470+
471+
<div className="flex gap-3">
472+
<button
473+
type="button"
474+
onClick={() => setConfirmTarget(null)}
475+
className="flex-1 rounded-xl border-0 py-3 tracking-tight transition-all hover:scale-[1.02]"
476+
style={{
477+
background: "rgba(234, 247, 255, 0.07)",
478+
border: "1px solid rgba(234, 247, 255, 0.14)",
479+
color: "var(--muted)",
480+
cursor: "pointer",
481+
fontSize: 15,
482+
fontWeight: 900
483+
}}
484+
>
485+
취소
486+
</button>
487+
<button
488+
type="button"
489+
onClick={handleConfirmDelete}
490+
className="flex-1 rounded-xl border-0 py-3 tracking-tight transition-all hover:scale-[1.02]"
491+
style={{
492+
background: "linear-gradient(135deg, #FF6B6B, #cc3333)",
493+
border: "none",
494+
color: "#fff",
495+
cursor: "pointer",
496+
fontSize: 15,
497+
fontWeight: 950,
498+
boxShadow: "0 0 24px rgba(255, 107, 107, 0.30)"
499+
}}
500+
>
501+
추방하기
502+
</button>
503+
</div>
504+
</div>
505+
</div>
506+
)}
571507
</div>
572508
);
573509
}

0 commit comments

Comments
 (0)