8.3 KiB
8.3 KiB
MSN[계획] 메신저 기능 개발
개요
벡스플로어 ERP에 내장 메신저 기능을 추가한다. Gmail 편지쓰기 스타일의 우측 하단 플로팅 모달로 동작하며, Socket.IO 기반 실시간 통신을 제공한다. 모든 화면에서 접근 가능하고, 동일 company_code 내 사용자끼리 1:1 DM / 그룹 채팅 / 채널 대화를 지원한다.
현재 동작
- 메신저 기능 없음
- 사내 커뮤니케이션 수단 부재
변경 후 동작
- 모든 화면 우측 하단에 메신저 FAB 버튼 고정 (z-index: 9999)
- FAB 클릭 시 Gmail 편지쓰기 스타일 모달 팝업 (우측 하단)
- 모달 좌측: 채팅방 목록 (DM / 그룹 / 채널 탭)
- 모달 우측: 채팅 영역 (메시지 입력, 파일 첨부, 이모지, 멘션, 스레드)
- Socket.IO로 실시간 메시지 수신
- 읽지 않은 메시지 수 FAB 배지 표시
- 토스트 알림 on/off 토글 (메신저 설정 내)
시각적 예시
┌─────────────────────────────────────────────────────┐
│ 벡스플로어 화면 │
│ │
│ │
│ ┌────────────────────┐ │
│ │ 채팅방 목록 │ 채팅창 │ │
│ │─────────────────── │ │
│ │ DM 그룹 채널 │ │
│ │ ───────────────── │ │
│ │ 👤 김민호 ● │ │
│ │ 👥 개발팀 │ │
│ │ # 공지사항 │ │
│ └────────────────────┘ │
│ [💬 3] │
└─────────────────────────────────────────────────────┘
아키텍처
graph TB
subgraph Frontend
FAB[MessengerFAB] --> Modal[MessengerModal]
Modal --> RoomList[RoomList 좌측 240px]
Modal --> ChatPanel[ChatPanel 우측 480px]
ChatPanel --> MessageList[MessageList]
ChatPanel --> MessageInput[MessageInput]
MessageInput --> FileUpload[파일 첨부]
MessageInput --> EmojiPicker[이모지]
MessageInput --> MentionDropdown[멘션 @]
end
subgraph SocketIO
SocketClient[socket.io-client] <--> SocketServer[messengerSocket.ts]
end
subgraph Backend
SocketServer --> MessengerService[messengerService.ts]
Route[messengerRoutes.ts] --> Controller[messengerController.ts]
Controller --> MessengerService
MessengerService --> DB[(PostgreSQL)]
FileRoute[파일 업로드] --> Multer[multerMessengerConfig.ts]
end
Frontend <--> SocketIO
Frontend <--> Route
변경 파일
신규 생성
| 파일 | 역할 |
|---|---|
backend-node/src/types/messenger.ts |
TypeScript 인터페이스 |
backend-node/src/services/messengerService.ts |
비즈니스 로직 |
backend-node/src/controllers/messengerController.ts |
HTTP 핸들러 |
backend-node/src/routes/messengerRoutes.ts |
REST API 라우트 |
backend-node/src/socket/messengerSocket.ts |
Socket.IO 이벤트 핸들러 |
backend-node/src/config/multerMessengerConfig.ts |
파일 업로드 설정 |
db/migrations/messenger_tables.sql |
DB 테이블 생성 |
frontend/components/messenger/MessengerFAB.tsx |
플로팅 버튼 |
frontend/components/messenger/MessengerModal.tsx |
메인 모달 컨테이너 |
frontend/components/messenger/RoomList.tsx |
채팅방 목록 |
frontend/components/messenger/ChatPanel.tsx |
채팅 영역 |
frontend/components/messenger/MessageItem.tsx |
메시지 단일 아이템 |
frontend/components/messenger/MessageInput.tsx |
입력창 |
frontend/components/messenger/NewRoomModal.tsx |
방 생성 모달 |
frontend/components/messenger/UserAvatar.tsx |
프로필 아바타 |
frontend/hooks/useMessenger.ts |
메신저 상태 훅 |
frontend/hooks/useMessengerSocket.ts |
Socket.IO 훅 |
frontend/contexts/MessengerContext.tsx |
전역 메신저 상태 |
수정
| 파일 | 변경 내용 |
|---|---|
backend-node/src/app.ts |
Socket.IO 서버 초기화, messengerRoutes 등록 |
frontend/app/(main)/layout.tsx |
<MessengerProvider>, <MessengerFAB /> 추가 |
코드 설계
DB 스키마
-- 채팅방
messenger_rooms (
room_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company_code VARCHAR(50) NOT NULL,
room_type VARCHAR(10) NOT NULL CHECK (room_type IN ('dm', 'group', 'channel')),
room_name VARCHAR(100),
created_by VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
-- 참여자
messenger_participants (
room_id UUID REFERENCES messenger_rooms(room_id),
user_id VARCHAR(100) NOT NULL,
company_code VARCHAR(50) NOT NULL,
joined_at TIMESTAMP DEFAULT NOW(),
last_read_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (room_id, user_id)
)
-- 메시지
messenger_messages (
message_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID REFERENCES messenger_rooms(room_id),
company_code VARCHAR(50) NOT NULL,
sender_id VARCHAR(100) NOT NULL,
content TEXT,
message_type VARCHAR(10) DEFAULT 'text' CHECK (message_type IN ('text', 'file', 'system')),
parent_message_id UUID REFERENCES messenger_messages(message_id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
is_deleted BOOLEAN DEFAULT FALSE
)
-- 이모지 리액션
messenger_reactions (
message_id UUID REFERENCES messenger_messages(message_id),
user_id VARCHAR(100) NOT NULL,
emoji VARCHAR(10) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (message_id, user_id, emoji)
)
-- 파일 첨부
messenger_files (
file_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID REFERENCES messenger_messages(message_id),
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
)
Socket.IO 이벤트
| 이벤트 | 방향 | 설명 |
|---|---|---|
join_rooms |
Client→Server | 참여 중인 방 전체 구독 |
send_message |
Client→Server | 메시지 전송 |
new_message |
Server→Client | 새 메시지 수신 |
message_read |
Client→Server | 읽음 처리 |
user_online |
Server→Client | 온라인 상태 변경 |
typing_start/stop |
Client↔Server | 타이핑 표시 |
add_reaction |
Client→Server | 이모지 리액션 추가 |
reaction_updated |
Server→Client | 리액션 업데이트 |
REST API
| Method | URL | 설명 |
|---|---|---|
| GET | /api/messenger/rooms |
내 채팅방 목록 |
| POST | /api/messenger/rooms |
채팅방 생성 |
| GET | /api/messenger/rooms/:roomId/messages |
메시지 히스토리 |
| POST | /api/messenger/rooms/:roomId/read |
읽음 처리 |
| POST | /api/messenger/files/upload |
파일 업로드 |
| GET | /api/messenger/files/:fileId |
파일 다운로드 |
| GET | /api/messenger/users |
회사 내 사용자 목록 |
| PUT | /api/messenger/rooms/:roomId |
방 이름/설정 수정 |
예상 문제
- Socket.IO + Next.js: Next.js는 기본적으로 HTTP 서버를 추상화하므로, Express HTTP 서버에 Socket.IO를 붙이고 프론트엔드는 백엔드 포트로 직접 연결
- JWT 인증 with Socket.IO: handshake 시 Authorization 헤더 또는 auth 옵션으로 토큰 전달, 서버에서 미들웨어로 검증
- 채널 분리 가능성:
room_type = 'channel'쿼리 조건으로만 필터링하므로, 채널 UI/라우트만 제거하면 기능 완전 제거 가능
설계 원칙
- 기존 MVC + Service 패턴 준수
- 모든 쿼리에
company_code필터 적용 (멀티테넌시) - 채널은
room_type값으로만 분리 — 코드 결합도 최소화 - FAB/모달은 기존 레이아웃 DOM에 독립적으로 렌더링 (Portal 또는 최상단 마운트)