AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@f69a4eb 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.
관련 소스 파일
이 페이지는 사용자가 채팅방 목록에서 방을 선택하고, REST API로 기존 메시지를 조회한 뒤, STOMP 구독을 통해 새 메시지·이미지·읽음 상태를 실시간으로 받는 흐름을 설명합니다. 메시지 전송은 STOMP SEND가 아니라 REST POST /chat/rooms/{roomId}/messages를 기본 경로로 사용하며, STOMP는 방 토픽 구독과 실시간 브로드캐스트 수신에 사용합니다.
이미지 전송에서는 ChatImageService가 발급한 S3 presigned URL로 파일을 업로드하고, 반환된 업로드 식별자를 메시지 요청에 포함하는 서버 측 검증 흐름을 다룹니다. 메시지 신고에서는 사용자 신고 요청의 실제 컨트롤러가 원문에 포함되어 있지 않으므로, 확인 가능한 관리자 처리 흐름과 ChatReportStatus를 중심으로 설명합니다.
다음 페이지와 함께 읽으면 전체 구조를 파악할 수 있습니다.
이 페이지는 채팅 화면의 실제 말풍선 컴포넌트 구현, 사용자 신고 작성 화면, 데이터베이스 마이그레이션 정의는 다루지 않습니다.
출처: 1D1S-client/src/app/chat/page.tsx:1-6, 1D1S-client/src/app/chat/rooms/[roomId]/page.tsx:1-17, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:22-34
1D1S-client/src/app/chat/page.tsx의 ChatPage는 /chat 경로에서 ChatRoomListScreen을 호출합니다. 방 목록 화면의 구체적인 API 호출 코드는 원문에 없지만, 서버에는 현재 회원의 방 목록을 반환하는 GET /chat/rooms가 있습니다.
1D1S-client/src/app/chat/rooms/[roomId]/page.tsx의 ChatRoomPage는 URL의 roomId를 숫자로 변환합니다. 값이 정수가 아니거나 0 이하이면 notFound()를 호출하고, 유효하면 ChatRoomScreen roomId={parsed}를 반환합니다. 따라서 채팅방 화면 진입 전에 클라이언트 라우트 단계에서 기본적인 방 번호 형식 검사가 수행됩니다.
서버의 ChatController는 방 목록, 메시지 내역, 메시지 전송, 읽음 처리 같은 HTTP API를 제공합니다. 실제 저장과 권한 검사는 ChatRoomService, ChatMessageService, ChatImageService, ChatModerationService로 분리되어 있습니다.
특히 ChatMessageService.send는 요청 형식, 중복 메시지, 방 쓰기 권한, 전송 빈도, 이미지 업로드 검증을 순서대로 처리한 뒤 메시지를 저장합니다. 저장이 끝나면 ChatBroadcaster.message가 방 토픽으로 같은 메시지를 전파합니다.
chatSocket.ts는 모듈 스코프의 STOMP Client를 하나만 유지합니다. 여러 화면이 같은 연결을 공유하고, 화면별로 subscribeChatRoom을 호출해 방 구독만 추가하거나 제거합니다. 서버의 ChatWebSocketConfig는 /ws/chat 엔드포인트, /topic·/queue 브로커, /user 사용자 destination을 설정합니다.
출처: 1D1S-client/src/app/chat/page.tsx:1-6, 1D1S-client/src/app/chat/rooms/[roomId]/page.tsx:1-17, 1D1S-client/src/app.feature/chat/socket/chatSocket.ts:13-35, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:22-34, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatMessageService.java:24-77
GET /chat/rooms의 예시 응답에는 다음 정보가 포함됩니다.
| 필드 | 설명 |
|---|---|
roomId | 채팅방 식별자입니다. |
challengeId | 연결된 챌린지 식별자입니다. |
challengeTitle | 챌린지 제목입니다. |
status | 방 상태 예시로 ACTIVE가 제시됩니다. |
myRole | 현재 회원의 방 역할입니다. |
myMembershipStatus | 현재 회원의 참여 상태입니다. |
pushEnabled | 방 알림 활성화 여부입니다. |
unreadCount | 방의 읽지 않은 메시지 수입니다. |
notice | 방 공지입니다. 없으면 null입니다. |
lastMessage | 현재 회원이 볼 수 있는 최신 메시지입니다. |
challengeEnded | 챌린지 기간 종료 여부를 나타냅니다. |
응답 최상위의 hiddenMemberIds는 내가 차단했거나 나를 차단한 회원의 ID 집합입니다. 서버는 목록과 과거 내역에서 차단 관계를 필터링하지만, 방 전체에 동일한 payload를 보내는 실시간 토픽에서는 클라이언트가 senderId를 이 목록과 비교해 필터링해야 합니다.
ChatMessageResponse 예시에는 다음 필드가 있습니다.
| 필드 | 설명 |
|---|---|
id | 메시지 식별자입니다. |
roomId | 메시지가 속한 방입니다. |
senderId | 발신자 식별자입니다. |
senderNickname | 발신자 닉네임입니다. |
clientMessageId | 클라이언트가 생성하는 중복 제거 키입니다. |
type | TEXT, IMAGE 등의 메시지 유형입니다. |
content | 텍스트 또는 이미지 캡션입니다. |
imageUrl | 이미지 메시지의 다운로드 URL입니다. 없으면 null입니다. |
status | 예시로 ACTIVE가 제시됩니다. |
createdAt | 생성 시각입니다. |
메시지 내역은 Pagination<ChatMessageResponse>로 반환되며, pageInfo.limit, pageInfo.hasNextPage, pageInfo.nextCursor를 사용합니다. ChatMessageService.history는 cursor를 복호화해 이전 메시지를 조회하고, 한 페이지보다 하나 더 읽어 다음 페이지 존재 여부를 판단합니다. 차단된 발신자의 메시지는 메모리 필터가 아니라 저장소 조회 단계에서 제외합니다.
메시지 유형별 요청 조건은 다음과 같습니다.
| 유형 | 필수 또는 금지 필드 |
|---|---|
TEXT | content 필수, imageUploadId와 sharedTargetId 금지 |
IMAGE | imageUploadId 필수, sharedTargetId 금지 |
| 공유 유형 | sharedTargetId 필수, 이미지 첨부 금지 |
STATS_SHARE | sharedTargetId와 imageUploadId 금지 |
SYSTEM | 클라이언트가 직접 보낼 수 없음 |
content의 최대 길이는 2,000자입니다. 모든 전송 요청에는 clientMessageId와 type이 필요합니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatApiExamples.java:8-35, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:36-68, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatMessageService.java:27-77, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatMessageService.java:79-118, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatMessageService.java:144-190
/chat에 진입하면 ChatPage가 ChatRoomListScreen을 렌더링합니다./chat/rooms/:roomId 경로의 ChatRoomPage가 실행됩니다.ChatRoomPage가 roomId를 숫자로 검증한 뒤 ChatRoomScreen에 전달합니다.GET /chat/rooms/{roomId}/messages를 호출해 기존 내역을 가져옵니다. 첫 요청에서는 cursor를 생략하고, 이후에는 이전 응답의 pageInfo.nextCursor를 그대로 전달합니다.ChatMessageService.history가 roomService.requireReadable로 접근 권한을 확인한 뒤 메시지, 공유 카드, 링크 프리뷰, 발신자 레벨을 조합합니다.ChatController의 구체적인 매핑은 제공된 원문에 포함되어 있지 않습니다.ChatImageService.issue가 확장자와 MIME 타입을 비교하고 파일 크기를 검사합니다. 허용 확장자는 jpg, jpeg, png, heic, heif이며 최대 크기는 10MB입니다.ChatImageService가 chat/quarantine/... 형식의 S3 object key와 5분 유효한 presigned PUT URL을 만들고 ChatMediaUpload를 ISSUED 상태로 저장합니다.POST /chat/rooms/{roomId}/messages를 호출합니다. 이미지 메시지라면 요청의 imageUploadId를 포함합니다.ChatMessageService.send가 ChatImageService.verify를 호출해 S3의 실제 크기와 MIME 타입을 발급 당시 값과 비교합니다.ChatController.send가 ChatBroadcaster.message를 호출합니다./topic/chat/rooms/{roomId}에 메시지가 브로드캐스트됩니다. 발신자도 토픽을 구독 중이면 HTTP 응답과 브로드캐스트를 모두 받을 수 있으므로 clientMessageId로 중복 표시를 제거해야 합니다.출처: 1D1S-client/src/app/chat/rooms/[roomId]/page.tsx:7-17, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:70-101, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:103-168, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatImageService.java:37-121, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatImageService.java:124-169, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatMessageService.java:44-76
| 메서드와 경로 | 역할 | 서버 처리 |
|---|---|---|
GET /chat/rooms | 내 채팅방 목록 조회 | ChatRoomService.getMyRooms |
GET /chat/rooms/{roomId}/messages | 과거 메시지 cursor 조회 | ChatMessageService.history |
POST /chat/rooms/{roomId}/messages | 메시지 저장 및 전송 | ChatMessageService.send 후 ChatBroadcaster.message |
POST /chat/shares/resolve | URL 공유 가능 여부 확인 | ChatShareService.resolveLink |
PUT /chat/rooms/{roomId}/read | 마지막 읽은 메시지 저장 | ChatMessageService.markRead 후 필요 시 읽음 브로드캐스트 |
GET /chat/rooms/{roomId}/read-states | 방 멤버별 읽음 위치 조회 | ChatRoomService.getReadStates |
PUT /chat/rooms/{roomId}/notifications | 방 알림 설정 변경 | ChatController의 후반부 매핑으로 보이나 구현 본문은 일부만 제공됨 |
GET /admin/chat/reports | 관리자 채팅 신고 목록 조회 | ChatModerationService.reports |
PATCH /admin/chat/reports/{reportId} | 신고를 ACCEPTED 또는 REJECTED로 처리 | ChatModerationService.resolve |
PATCH /admin/chat/messages/{messageId} | 숨김 메시지 차단 해제 | ChatModerationService.updateMessageStatus |
GET /chat/rooms/{roomId}/messages의 기본 size는 30이고, 서버는 1~50으로 보정합니다. 방 목록의 archived query parameter는 생략, true, false로 보관 여부를 구분합니다.
| destination | 방향 | 역할 |
|---|---|---|
/ws/chat | 연결 | STOMP WebSocket 엔드포인트입니다. |
/topic/chat/rooms/:roomId | 서버에서 클라이언트 | 새 메시지 본문을 전달합니다. |
/topic/chat/rooms/:roomId/notice | 서버에서 클라이언트 | 공지 설정·해제 변화를 전달합니다. |
/topic/chat/rooms/:roomId/updates | 서버에서 클라이언트 | 링크 프리뷰 지연 갱신을 전달합니다. |
/topic/chat/rooms/:roomId/read | 서버에서 클라이언트 | 읽음 위치 갱신을 전달합니다. |
/user/queue/chat-errors | 서버에서 해당 사용자 | 구독 거절과 STOMP 처리 오류를 전달합니다. |
/app/chat/rooms/:roomId/messages | 클라이언트에서 서버 | 소켓 메시지 전송 매핑입니다. 다만 현재 클라이언트 주석은 REST 전송을 사용한다고 설명합니다. |
클라이언트의 ChatRoomHandlers는 onMessage, onNotice, onUpdate, onRead, onError, onReconnect 콜백으로 채널별 이벤트를 전달받습니다. 연결이 끊겼다가 다시 붙으면 토픽은 과거 메시지를 재전송하지 않으므로 onReconnect에서 최신 REST 페이지를 다시 받아 누락 구간을 보완합니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:58-101, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:161-169, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:241-290, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/AdminChatController.java:24-64, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/AdminChatController.java:66-105, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/AdminChatController.java:108-157, 1D1S-client/src/app.feature/chat/socket/chatSocket.ts:26-58
subscribeChatRoom(roomId, handlers)가 방별 리스너를 등록합니다.ensureClient가 아직 클라이언트가 없으면 STOMP Client를 생성하고 socketUrl()로 API_BASE_URL의 프로토콜을 ws 또는 wss로 바꾼 뒤 경로를 /ws/chat으로 설정합니다./user/queue/chat-errors를 먼저 구독합니다./notice, /updates, /read 네 채널을 구독합니다.closeChatSocket이 리스너·구독 상태를 비우고 Client.deactivate()를 호출합니다.브라우저 채팅 연결은 HTTP 핸드셰이크에 실린 쿠키로 인증됩니다. ChatStompAuthInterceptor는 핸드셰이크에서 만들어진 principal을 사용하고, 별도 principal이 없으면 STOMP CONNECT의 Authorization 헤더에서 토큰을 추출합니다. CONNECT 성공 시 ChatConnectionRegistry.reserve가 전역 및 회원별 연결 수를 예약합니다.
SUBSCRIBE 요청에서 destination이 /topic/chat/rooms/으로 시작하지 않으면 CHAT_ROOM_NOT_ACCESS가 됩니다. 방 번호를 추출한 뒤 ChatRoomService.canRead가 현재 회원의 읽기 권한을 확인합니다. 권한이 없는 구독은 세션 전체를 끊지 않고 /user/queue/chat-errors로만 오류를 전송합니다.
클라이언트는 오류의 code, message, retryAfterSeconds, x-failed-destination를 파싱합니다. CHAT-015이면 서버가 알려준 재시도 시간 또는 기본 5초 동안 연결을 보류합니다. 방별 구독 오류는 failedRoomId로 방을 식별하고 해당 방 리스너에만 전달합니다.
서버와 클라이언트의 하트비트 주기는 모두 10초입니다. 클라이언트 재접속 지연은 3초이며, 첫 연결이 성공하지 못한 상황에서는 refreshAccessTokenOnce를 한 번 호출한 뒤 다음 핸드셰이크에 갱신된 쿠키를 사용합니다.
출처: 1D1S-client/src/app.feature/chat/socket/chatSocket.ts:63-77, 1D1S-client/src/app.feature/chat/socket/chatSocket.ts:209-257, 1D1S-client/src/app.feature/chat/socket/chatSocket.ts:259-302, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/config/ChatStompAuthInterceptor.java:79-107, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/config/ChatStompAuthInterceptor.java:116-166, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/config/ChatStompAuthInterceptor.java:209-241
채팅방의 종료와 보관은 같은 상태 축이 아닙니다. challengeEnded는 챌린지 기간 종료를 표시하지만 종료 뒤에도 방과 전송이 유지될 수 있습니다. 삭제된 챌린지에 연결된 방은 READ_ONLY가 될 수 있고, 챌린지 종료 후 7일이 지나 보관된 방은 archived=true로 조회할 수 있지만 메시지 전송은 CHAT-017로 거절됩니다. 입력창 잠금 판단에는 canSend와 chatClosesAt이 사용된다고 서버 설명에 명시되어 있습니다.
| 상태 또는 값 | 의미 |
|---|---|
ACTIVE | 방 또는 메시지의 기본 활성 상태 예시입니다. |
READ_ONLY | 삭제된 챌린지로 인해 방이 잠긴 상태입니다. |
archived=true | 보관된 방입니다. 읽기만 가능하고 전송할 수 없습니다. |
challengeEnded=true | 챌린지 기간이 끝났다는 표시입니다. 이것만으로 방이 잠기지는 않습니다. |
PENDING | 신고가 아직 처리되지 않은 상태입니다. |
ACCEPTED | 관리자 신고 처리 결과이며, 신고 메시지를 숨김 처리합니다. |
REJECTED | 신고를 기각한 상태입니다. |
읽음 처리는 PUT /chat/rooms/{roomId}/read에 lastReadMessageId를 전달하는 방식입니다. ChatMessageService.markRead는 기존 위치보다 뒤로 이동하지 않도록 처리하고, 실제로 위치가 전진한 경우에만 ChatReadReceipt를 /read 토픽으로 브로드캐스트합니다. 응답에는 notificationCount, chatCount, totalCount, 구버전 호환 필드인 unreadCount가 포함됩니다.
링크가 메시지 본문에 있으면 ChatLinkPreviewDispatcher가 커밋 이후 비동기로 링크를 확인합니다. 메시지 저장 응답의 linkPreview는 처음에 null일 수 있으며, 결과가 준비되면 /updates 토픽으로 메시지 ID를 포함한 갱신을 보냅니다. 링크 프리뷰 실패는 메시지 전송 실패로 전환되지 않습니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:36-48, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:202-255, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatMessageService.java:121-142, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatLinkPreviewDispatcher.java:12-56
확인 가능한 관리자 신고 흐름은 다음과 같습니다.
GET /admin/chat/reports를 호출합니다.AdminChatController.reports가 page, size, status를 받아 ChatModerationService.reports를 호출합니다.status를 지정하면 해당 ChatReportStatus만 조회하고, 생략하면 처리된 신고를 포함한 전체 신고를 조회합니다.OffsetPagination<ChatReportResponse>로 반환되며, size는 1~100으로 보정됩니다.PATCH /admin/chat/reports/{reportId}에 ChatReportResolveRequest를 보내 신고를 처리합니다.ACCEPTED 또는 REJECTED여야 합니다. ACCEPTED이면 신고된 메시지가 숨김 처리됩니다.PATCH /admin/chat/messages/{messageId}를 호출하고 ChatMessageStatusUpdateRequest.status를 ACTIVE로 지정합니다.ACCEPTED 신고는 REJECTED로 바뀌고 처리자와 처리 시각이 갱신됩니다. 처리되지 않은 PENDING 신고는 그대로 유지됩니다.관리자 조회 응답은 메시지가 HIDDEN이 된 뒤에도 content와 imageUrl 원문을 그대로 포함합니다. 따라서 신고 처리 후에도 관리자가 판단 근거와 번복에 필요한 내용을 확인할 수 있습니다.
다만 제공된 원문에는 사용자가 채팅 메시지를 신고하는 클라이언트 컴포넌트, 사용자용 신고 요청 DTO, 사용자용 신고 POST 엔드포인트가 없습니다. 따라서 “사용자가 신고 버튼을 누른 뒤 어떤 API를 호출하는가”와 신고 사유 필드의 이름은 이 코드만으로 확정할 수 없습니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/AdminChatController.java:24-64, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/AdminChatController.java:66-105, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/AdminChatController.java:108-157, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatApiExamples.java:33-35
| 오류 코드 또는 조건 | 발생 지점 | 클라이언트가 확인할 내용 |
|---|---|---|
CHAT-002 | 방 조회 권한 없음 | REST 응답 오류로 처리합니다. |
CHAT-003 | 메시지 전송 권한 없음 | 방이 쓰기 가능한지 확인합니다. |
CHAT-004 | 메시지 없음 | 읽음 처리 대상이 유효한지 확인합니다. |
CHAT-005 | 메시지 형식 또는 중복 키 오류 | type, content, imageUploadId, clientMessageId 조합을 확인합니다. |
CHAT-013 | 메시지 전송 빈도 초과 | 즉시 반복 전송하지 않습니다. |
CHAT-015 | 동시 WebSocket 연결 제한 | retryAfterSeconds만큼 재연결을 늦춥니다. |
CHAT-017 | 보관된 방에 전송 | 내역 조회만 허용되는 상태로 처리합니다. |
CHAT-012 | 신고 처리 결과가 잘못됨 | ACCEPTED 또는 REJECTED만 사용합니다. |
CHAT-016 | 메시지 상태 변경 불가 | 숨김은 신고 처리 경로로만 수행합니다. |
ChatConnectionRegistry는 전체 연결을 600개, 회원별 연결을 8개로 제한합니다. 오래된 세션 기록은 30분 기준으로 정리하며, 정상적인 끊김은 SessionDisconnectEvent에서 회원별 수를 감소시킵니다. 서버와 클라이언트 하트비트는 10초입니다.
WebSocket transport의 메시지 크기 제한은 16KB, 송신 버퍼 제한은 256KB, 송신 시간 제한은 10초입니다. 단, 현재 클라이언트의 공식 메시지 전송 경로는 REST이므로 이미지 파일 자체를 STOMP 프레임에 넣지 않고 S3 업로드 후 메시지 요청에 업로드 식별자를 전달합니다.
출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/config/ChatConnectionRegistry.java:31-71, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/config/ChatConnectionRegistry.java:74-110, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/config/ChatWebSocketConfig.java:27-28, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/config/ChatWebSocketConfig.java:104-109, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/ChatController.java:119-159
AdminChatController의 관리자 조회·처리 API만 있고, 사용자용 신고 생성 API가 없습니다.ChatImageService.issue를 호출하는 이미지 presign 컨트롤러의 HTTP 경로와 요청·응답 형식은 무엇입니까?ChatRoomScreen, ChatRoomListScreen이 어떤 훅을 통해 GET /chat/rooms, GET /chat/rooms/:roomId/messages, PUT /chat/rooms/:roomId/read를 호출하는지 확인이 필요합니다.ChatMediaUploadStatus의 전체 상태 전이와 업로드 완료 후 ISSUED 상태를 변경하는 코드는 제공된 원문에 없습니다. 업로드 성공 시 상태를 별도로 갱신하는지 확인이 필요합니다.출처: 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/controller/AdminChatController.java:24-157, 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/chat/service/ChatImageService.java:67-122, 1D1S-client/src/app/chat/page.tsx:1-6, 1D1S-client/src/app/chat/rooms/[roomId]/page.tsx:1-17