Files
vexplor_dev/docs/yc/MSN[계획]-메신저기능.md
2026-03-30 17:52:57 +09:00

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 방 이름/설정 수정

예상 문제

  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 또는 최상단 마운트)