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
10 changes: 3 additions & 7 deletions src/service/feature/channel/hook/query/useChannelQuery.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import {
getChannelList,
getDMList,
} from '@service/feature/channel/api/channelAPI.ts';
import { Channel } from '@service/feature/channel/types/channel.ts';
import { getChannelList, getDMList } from '@service/feature/channel/api/channelAPI.ts';
import {ChannelResponse} from '@service/feature/channel/types/channel.ts';

// response 형식이 다름. 임시로 설정하여 사용중
export const useChannelListQuery = (serverId: string) => {
return useQuery<Channel>({
return useQuery<ChannelResponse>({
queryKey: ['serverChannels', serverId],
queryFn: () => getChannelList(serverId),
enabled: !!serverId,
Expand Down
56 changes: 27 additions & 29 deletions src/service/feature/channel/types/channel.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
export type ChannelType = 'text' | 'voice' | 'event';

// export interface Channel {
// id: string;
// name: string;
// type: ChannelType;
// category: string;
// [key: string]: unknown;
// }

export interface DMDetail {
channel: Channel2;
channel: Channel;
channelMembers: ChannelMember[];
}

export interface DMList extends Channel2 {
export interface DMList extends Channel {
channelMembers: ChannelMember[];
}

Expand All @@ -26,8 +18,7 @@ export interface ChannelMember {
createdAt: string;
}

// 팀 서버 상세 조회에서 불러오는 channel 타입도 이것. 추후 아래 Channel 타입에서 이걸로 변경해야 할 듯
export interface Channel2 {
export interface Channel {
id: number;
name: string;
position: number;
Expand All @@ -36,29 +27,36 @@ export interface Channel2 {
chatId: string;
}

export interface Channel {
categoriesView: CategoriesView[];
team: Team;
teamMembers: TeamMembers[];
}

export interface CategoriesView {
export interface CategoryView {
category: {
id: number;
name: string;
position: number;
};
}
channels: Channel[];

export interface Team {
id: string;
name: string;
masterId: string;
iconUrl: string;
}

export interface TeamMembers {
id: number;
role: 'OWNER' | 'MEMBER';
memberInfo: ChannelMember;
export interface ChannelResponse {
team: {
id: string;
name: string;
masterId: string;
iconUrl: string;
};
categoriesView: CategoryView[];
teamMembers: {
id: number;
role: 'OWNER' | 'MEMBER';
memberInfo: ChannelMember;
}[];
}

export interface ChannelMember {
id: string;
nickname: string;
name: string;
avatarUrl: string;
state: 'ONLINE' | 'OFFLINE';
createdAt: string;
}
6 changes: 3 additions & 3 deletions src/service/feature/chat/api/chatAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export const fetchChannels = async () => {
return res.data;
};

export const fetchMessages = async (channelId: string) => {
const res = await axios.get(`/channels/${channelId}/messages`);
return res.data;
export const fetchLatestMessages = async (channelId: string | undefined) => {
const res = await axios.get(`/message/latest?chatId=${channelId}`);
return Array.isArray(res.data) ? res.data : [];
};

export const deleteMessage = async (messageId: string) => {
Expand Down
84 changes: 65 additions & 19 deletions src/service/feature/chat/hook/useChat.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,82 @@
import { useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useEffect, useCallback } from 'react';
import { useSocket } from '../context/useSocket';
import { ChatMessage } from '@service/feature/chat/schema/messageSchema.ts';

export const useChat = (onMessage: (msg: ChatMessage) => void) => {
export const useChat = (chatId: string | undefined, onMessage: (msg: ChatMessage) => void) => {
const { client, isConnected } = useSocket();
const chatId = '25ffc7bf-874f-444e-b331-26ed864a76ba';

useEffect(() => {
if (!client || !isConnected) return;
let subscription: any;

const setupSubscription = () => {
if (!client || !isConnected || !chatId) {
console.log('채팅 구독 조건이 충족되지 않음:', { client: !!client, isConnected, chatId });
return;
}

try {
const subscribeUrl = `/sub/message/${chatId}`;
console.log('채팅 구독 시도:', subscribeUrl);

subscription = client.subscribe(subscribeUrl, (message) => {
const parsed: ChatMessage = JSON.parse(message.body);
onMessage(parsed);
});

console.log('채팅 구독 성공');
} catch (error) {
console.error('STOMP 구독 중 오류 발생:', error);
}
};

const subscribeUrl = `/sub/message/${chatId}`;
const subscription = client.subscribe(subscribeUrl, (message) => {
const parsed: ChatMessage = JSON.parse(message.body);
onMessage(parsed);
});
setupSubscription();

return () => {
subscription.unsubscribe();
if (subscription) {
try {
subscription.unsubscribe();
console.log('채팅 구독 해제');
} catch (error) {
console.error('구독 해제 중 오류 발생:', error);
}
}
};
}, [client, isConnected, onMessage]);
}, [client, isConnected, chatId, onMessage]);

const sendMessage = (content: string, attachments?: { type: string; url: string }[]) => {
if (!client || !isConnected) return;
const sendMessage = useCallback(async (content: string, attachments?: { type: string; url: string }[]) => {
if (!client || !isConnected || !chatId) {
console.warn('메시지를 보낼 수 없습니다:', {
client: !!client,
isConnected,
chatId
});
return Promise.reject(new Error('연결 상태가 올바르지 않습니다.'));
}

const tempId = uuidv4();
const sendUrl = `/pub/message/${chatId}`;
const message = { chatId, content, attachments, createdAt: new Date().toISOString()};
const message = {
chatId,
content,
attachments,
createdAt: new Date().toISOString(),
tempId
};

client.publish({
destination: sendUrl,
body: JSON.stringify(message),
return new Promise((resolve, reject) => {
try {
client.publish({
destination: sendUrl,
body: JSON.stringify(message),
});
resolve(tempId);
} catch (error) {
console.error('메시지 전송 중 오류 발생:', error);
reject(error);
}
});
};
}, [client, isConnected, chatId]);

return { sendMessage };
return { sendMessage, isConnected };
};
7 changes: 4 additions & 3 deletions src/service/feature/chat/hook/useMessageHistory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMessages } from '../api/chatAPI';
import { fetchLatestMessages } from '../api/chatAPI';

export const useMessageHistory = (channelId: string) => {
export const useMessageHistory = (channelId: string | undefined) => {
return useQuery({
queryKey: ['messages', channelId],
queryFn: () => fetchMessages(channelId),
queryFn: () => fetchLatestMessages(channelId),
enabled: !!channelId,
staleTime: 1000*30
});
};
12 changes: 7 additions & 5 deletions src/service/feature/chat/schema/messageSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from 'zod';

const senderSchema = z.object({
memberId: z.string(),
username: z.string(),
name: z.string(),
avatarUrl: z.string(),
});

Expand All @@ -12,13 +12,15 @@ const attachmentSchema = z.object({
});

export const messageSchema = z.object({
chatId: z.string(),
messageId: z.number(),
sender: senderSchema,
content: z.string().min(1, '메시지를 입력해주세요'),
createdAt: z.string().datetime({ message: '올바른 날짜/시간 형식이 아닙니다' }),
isUpdated: z.boolean(),
isDeleted: z.boolean(),
attachments: z.array(attachmentSchema).optional(),
createdAt: z
.string()
.datetime({ message: '올바른 날짜/시간 형식이 아닙니다' }),
status: z.enum(['pending', 'sent', 'error']).optional(),
tempId: z.string().optional(),
});

export type ChatMessage = z.infer<typeof messageSchema>;
28 changes: 28 additions & 0 deletions src/service/feature/chat/type/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// "messageId": 13,
// "sender": {
// "memberId": "fc810ff3-a156-410c-80db-939440507dc3",
// "name": "최승은",
// "avatarUrl": ""
// },
// "content": "ccc",
// "createdAt": "2025-06-03T22:34:07.542118",
// "isUpdated": false,
// "isDeleted": false,
// "attachments": []
// },

export interface ChatMessage {
messageId : number;
sender: {
memberId: string;
name: string;
avatarUrl: string;
},
content: string,
createdAt: string,
isUpdated: boolean,
isDeleted: boolean,
attachments?: { type: string; url: string }[];
status?: 'pending' | 'sent' | 'error';
tempId?: string;
}
13 changes: 5 additions & 8 deletions src/view/layout/sidebar/components/channel/ChannelCategory.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { DndContext } from '@dnd-kit/core';
import {DndContext, DragEndEvent} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
Expand All @@ -9,21 +9,17 @@ import { ChevronDown, ChevronRight, Plus } from 'lucide-react';
import ChannelItem from './ChannelItem.tsx';
import { Channel } from '@service/feature/channel/types/channel.ts';

const ChannelCategory = ({
title,
type,
defaultItems,
}: {
const ChannelCategory = ({title, type, defaultItems,}: {
title: string;
type: 'text' | 'voice' | 'event';
defaultItems: Channel[];
}) => {
const [isOpen, setIsOpen] = useState(true);
const [items, setItems] = useState(defaultItems);

const handleDragEnd = (event: any) => {
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
if (over && active.id !== over.id) {
const oldIndex = items.findIndex((i) => i.id === active.id);
const newIndex = items.findIndex((i) => i.id === over.id);
setItems((items) => arrayMove(items, oldIndex, newIndex));
Expand Down Expand Up @@ -53,6 +49,7 @@ const ChannelCategory = ({
id={item.id}
name={item.name}
type={type}
chatId={item.chatId}
/>
))}
</div>
Expand Down
52 changes: 31 additions & 21 deletions src/view/layout/sidebar/components/channel/ChannelItem.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import { useDraggable } from '@dnd-kit/core';
import { Hash, Radio, Volume2 } from 'lucide-react';
import { clsx } from 'clsx';
import { useNavigate, useParams } from 'react-router-dom';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Hash } from 'lucide-react';

const ChannelItem = ({ id, name, type = 'text', selected = false, }: {
id: string;
interface ChannelItemProps {
id: number;
name: string;
type?: 'text' | 'voice' | 'event';
selected?: boolean;
}) => {
const icon =
type === 'voice' ? <Volume2 size={16} /> :
type === 'event' ? <Radio size={16} /> :
<Hash size={16} />;
chatId: string;
}

const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id, });
const ChannelItem = ({ id, name, chatId }: ChannelItemProps) => {
const navigate = useNavigate();
const { serverId } = useParams<{ serverId: string }>();

const {attributes, listeners, setNodeRef, transform, transition,} = useSortable({ id });

const style = transform
? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, opacity: 0.8, }
: undefined;
const style = {
transform: CSS.Transform.toString(transform),
transition,
};

const handleClick = () => {
navigate(`/channels/${serverId}/${chatId}`);
};

return (
<div ref={setNodeRef} style={style}{...listeners}{...attributes}
className={clsx(
'flex items-center gap-2 px-2 py-1 text-sm text-[#b9bbbe] rounded hover:bg-[#3A3C41] cursor-pointer select-none',
selected && 'bg-[#393C43] text-white', isDragging && 'opacity-50')}>
{icon}
<span className="truncate">{name}</span>
<div
ref={setNodeRef}
style={style}{...attributes}{...listeners}
className="px-2 py-1 mx-2 text-[#949ba4] hover:text-white hover:bg-[#393C43] rounded cursor-pointer"
onClick={handleClick}
>
<div className="flex items-center gap-1.5">
<Hash size={18} />
<span className="text-sm font-medium">{name}</span>
</div>
</div>
);
};
Expand Down
Loading
Loading