# MSN[계획] 메신저 기능 개발 ## 개요 벡스플로어 ERP에 내장 메신저 기능을 추가한다. Gmail 편지쓰기 스타일의 우측 하단 플로팅 모달로 동작하며, Socket.IO 기반 실시간 통신을 제공한다. 모든 화면에서 접근 가능하고, 동일 company_code 내 사용자끼리 1:1 DM / 그룹 채팅 / 채널 대화를 지원한다. --- ## 현재 동작 - 메신저 기능 없음 - 사내 커뮤니케이션 수단 부재 ## 변경 후 동작 - 모든 화면 우측 하단에 메신저 FAB 버튼 고정 (z-index: 9999) - FAB 클릭 시 Gmail 편지쓰기 스타일 모달 팝업 (우측 하단) - 모달 좌측: 채팅방 목록 (DM / 그룹 / 채널 탭) - 모달 우측: 채팅 영역 (메시지 입력, 파일 첨부, 이모지, 멘션, 스레드) - Socket.IO로 실시간 메시지 수신 - 읽지 않은 메시지 수 FAB 배지 표시 - 토스트 알림 on/off 토글 (메신저 설정 내) --- ## 시각적 예시 ``` ┌─────────────────────────────────────────────────────┐ │ 벡스플로어 화면 │ │ │ │ │ │ ┌────────────────────┐ │ │ │ 채팅방 목록 │ 채팅창 │ │ │ │─────────────────── │ │ │ │ DM 그룹 채널 │ │ │ │ ───────────────── │ │ │ │ 👤 김민호 ● │ │ │ │ 👥 개발팀 │ │ │ │ # 공지사항 │ │ │ └────────────────────┘ │ │ [💬 3] │ └─────────────────────────────────────────────────────┘ ``` --- ## 아키텍처 ```mermaid 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` | ``, `` 추가 | --- ## 코드 설계 ### DB 스키마 ```sql -- 채팅방 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` | 방 이름/설정 수정 | --- ## 예상 문제 1. **Socket.IO + Next.js**: Next.js는 기본적으로 HTTP 서버를 추상화하므로, Express HTTP 서버에 Socket.IO를 붙이고 프론트엔드는 백엔드 포트로 직접 연결 2. **JWT 인증 with Socket.IO**: handshake 시 Authorization 헤더 또는 auth 옵션으로 토큰 전달, 서버에서 미들웨어로 검증 3. **채널 분리 가능성**: `room_type = 'channel'` 쿼리 조건으로만 필터링하므로, 채널 UI/라우트만 제거하면 기능 완전 제거 가능 --- ## 설계 원칙 - 기존 MVC + Service 패턴 준수 - 모든 쿼리에 `company_code` 필터 적용 (멀티테넌시) - 채널은 `room_type` 값으로만 분리 — 코드 결합도 최소화 - FAB/모달은 기존 레이아웃 DOM에 독립적으로 렌더링 (Portal 또는 최상단 마운트)