# 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` | `<MessengerProvider>`, `<MessengerFAB />` 추가 |

---

## 코드 설계

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