💬 실시간 채팅
WebSocket Gateway로 파트너 상태/읽지 않은 메시지 카운트/알림을 실시간 처리.
- 상태 동기화, 타이핑 인디케이터
- 메시지 타입: 텍스트/알림
- 룸 단위 전송 & 영속화
실시간 채팅 · 포토앨범 · 캘린더를 하나로. React + Electron + NestJS로 구현된 크로스플랫폼 데스크톱/웹 하이브리드 앱입니다.
Partner: 오늘 저녁 7시 어때? You: 좋아! 캘린더에 일정 추가했어 📅 System: 새로운 사진 3장이 앨범에 업로드되었습니다.
WebSocket Gateway로 파트너 상태/읽지 않은 메시지 카운트/알림을 실시간 처리.
Multer 기반 업로드, 앨범/사진 코멘트, 갤러리 창(Electron) 연동.
기념일/일정 공유, 지도(카카오/구글) 연동, 오늘 일정 빠른 보기.
일정·사진·채팅 앱을 번갈아 쓰며 소통 맥락이 끊기고, 실시간 동기화 지연으로 사용자 만족도가 하락.
데스크톱 친화 UI와 WebSocket 게이트웨이로 채팅·앨범·캘린더를 하나의 상태 모델로 통합.
메시지/사진 공유 동선 단축, 실시간 동기화 체감 향상(정량 수치로 교체 권장: 예) 평균 응답시간 120ms → 60ms).
React(Vite) + Electron ↔ Socket.IO(Client) ↔ NestJS(WebSocket/REST) ↔ MySQL(TypeORM)
ChatGateway
, Album/Calendar API
React (Vite) ├─ Router / Hooks ├─ Socket.IO Client └─ Electron (Main/Preload) │ ▼ NestJS (REST + WS) ├─ Auth / Chat Gateway ├─ Album / Calendar └─ TypeORM → MySQL
// WithU 디렉토리 구조(요약) withu/ ├── be/ # NestJS 백엔드 │ ├── src/ │ │ ├── auth/ # 인증 및 파트너 매칭 │ │ ├── chat/ # WebSocket 게이트웨이 │ │ ├── album/ # 포토앨범 │ │ ├── calendar/ # 캘린더 │ │ └── main.ts └── fe/ # React + Electron ├── electron/ (main/preload) └── src/ ├── components/pages (Home/Message/PhotoAlbum/Calendar) ├── common/{customhooks,layout} └── Socket.ts
// 시간대 정규화 + 오늘 일정 필터 export const useCalendar = () => { const [schedules, setSchedules] = useState<Schedule[]>([]); const loadSchedules = async () => { try { const res = await request({ url: '/u/calendar/events', method: RequestMethod.GET }); const normalized = (res || []).map(s => ({ ...s, startDate: s.startDate?.split('T')[0] + 'T' + s.startDate?.split('T')[1]?.split('.')[0] })); setSchedules(normalized); } catch { /* ... */ } }; const getToday = () => { /* ... */ }; return { schedules, loadSchedules, getToday }; };
@WebSocketGateway({ cors: { origin: 'http://localhost:5173', credentials: true } }) export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; handleConnection(client: Socket){ /* ... */ } handleDisconnect(client: Socket){ /* ... */ } @SubscribeMessage('sendPartnerMessage') handleSendPartnerMessage(data: { roomCd:string; senderCd:number; content:string; type:'message'|'alarm' }){ const payload = { id: uuidv4(), message: data.content, sender: String(data.senderCd), ts: new Date(), type: data.type==='alarm'?'alarm':'text' }; this.server.to(data.roomCd).emit('newPartnerMessage', payload); } }
import { app, BrowserWindow, ipcMain } from 'electron'; function createWindow(){ const win = new BrowserWindow({ width:1200, height:800, titleBarStyle: 'hidden', titleBarOverlay:{ color:'#bd2b2b', symbolColor:'#fff' } }); process.env.NODE_ENV==='development' ? win.loadURL('http://localhost:5173') : win.loadFile('dist/index.html'); } app.whenReady().then(createWindow);
{ "name": "withu", "main": "dist-electron/main.js", "proxy": "http://<server-ip-or-domain>:3000", "scripts": { "dev": "concurrently "npm:dev:react" "npm:dev:ts" "npm:dev:electron"" }, "dependencies": { "react": "^19.1.0", "electron": "^37.2.0", "socket.io-client": "^4.8.1" } }
협업과 코드 리뷰를 좋아합니다. 필요 시 실사용 데모·코드 워크스루 제공 가능합니다.