Chat 대화방 나가기
상태: implemented / local-verified / unpublished / undeployed. 2026-09-06 Auth 5f3a59c, Chat 46b7a39, JS SDK d2f1610의 최종 코드와 대조했다. 검증 범위는 아래 릴리즈 상태에 기록하며 기존 공개 버전의 지원 선언이 아니다.
후속 local PG 구현은 Chat 192f583에서 대조했다. 아래 선행 leave 검증과 구별하며
새 전송·읽음·DB 오류 정책은 PostgreSQL 로컬 계약을 따른다.
운영 source 보존·이관은 **BLOCKED**이며 실제 Auth 전체 E2E와 공개 릴리즈는 미완료다.
공개 문서와 로컬 계약
2026-09-06 KST에 공개 Chat SDK 문서와 공개 Agent 문서를 인증 없이 HTTP 200으로 조회했다. 두 페이지는 0.1.0을 표시하며 leaveRoom과 membership 조회를 제공하는 문서가 아니다. disconnect()는 소켓 연결 종료이지 대화방 탈퇴가 아니다. 공개 문서에 없다는 사실만으로 운영 서버의 모든 기능 부재를 추론하지 않는다. 이번 작업에서는 운영 room을 변경하거나 운영 API의 탈퇴 E2E를 실행하지 않았다.
로컬 작업 시작 시 chat-platform에도 self-leave/status route가 없었다. 아래 계약은 이번 로컬 변경을 위한 계약이며, SDK 발행·서버 배포·공개 문서 동기화는 각각 별도 승인과 검증이 필요하다. 단순 SDK 설치나 기존 공개 endpoint 사용만으로 이 기능을 사용할 수 있다고 가정하지 않는다.
2026-09-06 역할 80의 운영 확인 결과: 현재 Chat은 단일 container/single Node 프로세스이며 REST와 WebSocket이 같은 프로세스에 있다. 배포 image는 578647a, digest는 sha256:2d0e77922bc90f1d511ba9bebca7876b9082e21e0c8efd31ab44becf4384fed1이다. Map 저장소만 사용하고 DB·데이터 mount가 없으며 leave/membership API는 미지원이다. 이는 역할 80이 전달한 운영 증거이며 문서 담당이 운영 탈퇴를 실행한 결과가 아니다.
데이터 보존 대책 없는 재배포·container 강제 재생성 금지: 현재 room/messages/membership은 메모리에만 있어 재시작 시 소실된다. 새 나가기 기능의 로컬 검증 통과를 배포 승인으로 해석하지 않는다. 영속화와 기존 데이터 보존·이관 방법은 별도로 확정해야 하며 이번 작업은 배포하지 않는다.
지원 경계
- 실제 나가기는 현재 인증된 본인의 해당 room membership을
active에서left로 바꾼다. 다른 사용자의 membership을 변경하거나 방 전체를 삭제하지 않는다. left사용자는 참여 중인 방 목록에서 제외되고, 해당 방의 과거·신규 메시지 조회, 송신, read cursor, typing, 새 socket ticket 권한을 잃는다. 연결이 남아 있더라도 해당 방의 새 콘텐츠 fan-out 수신 권한은 없다. 본인에게만 보내는 아래 membership 제어 이벤트는 예외다.listRooms,connect, 소비 앱의ensureDM같은 조회·연결 경로가 탈퇴를 취소하거나 다른 사용자를 자동 재가입시켜서는 안 된다. 공개 SDK에ensureDM은 없다.- 기존 room ID의 1:1 재개는 미지원이다. 본인 재가입·상대 재가입·이전 기록 복구를 제공하는 API가 없다.
- 기존 그룹방 재초대는 미지원이다. inviter 권한/초대 수락/재가입 API를 임의로 추가하거나 있다고 안내하지 않는다. 필요하면 별도 제품 계약을 먼저 합의한다.
- 기존
createDirectRoom({ userId })와createGroupRoom(...)은 새로운 room/conversation을 만든다. 이전 방을 찾거나 되살리는 API가 아니며, 나가기·타임아웃 복구 중 자동 호출하지 않는다. 기존 새 방 생성의 참가자 포함 정책 자체를 이번 작업에서 확장하거나 변경하지 않는다. - 마지막 참여자도 동일하게 나갈 수 있다. 비어 있는 방과 과거 메시지를 이 동작으로 삭제하지 않으며, 마지막 참여자라는 이유로 재요청을 실패시키거나
left_at을 갱신하지 않는다.
room 응답의 participant_user_ids와 SDK participantUserIds는 현재 active 참여자 목록이다.
탈퇴 후 인원수가 줄어도 kind, room/conversation ID, 생성 시각은 변하지 않는다.
1:1/그룹 구분은 현재 인원수가 아니라 kind를 사용한다. 나가기로 시스템 메시지나 소유자 자동 이관을 만들지 않는다.
기록 보존과 접근 권한
기록의 보존과 사용자의 접근 가능 여부는 다르다. 남아 있는 참여자는 기존 메시지를 계속 조회할 수 있지만, 나간 사용자는 서버에 기록이 남아 있어도 조회할 수 없다. 탈퇴 전 메시지 삭제, 보존 기간 변경, GDPR 등 법적 삭제 요청 처리는 별도 기능이다.
선행 leave 구현과 현재 운영 저장소는 InMemoryChatRepository다. 이 경로의 보존은 동일 프로세스에서
leave가 기존 방·메시지를 삭제하지 않는다는 뜻이며 재시작 후 영구 보존을 뜻하지 않는다.
후속 local PG 경로는 room/message/membership/read/dedup를 DB에 저장하지만 아직 운영에 배포하지 않았다.
독립 다중 인스턴스 보장은 미지원이며 초기 PG는 하나의 API authority만 허용한다.
운영 승인에는 기존 원본의 완전한 보존·환경 매핑·복원·전환 증거가 별도로 필요하다.
Chat 탈퇴는 Storage 권한 회수 API가 아니다. 이미 공개된 public_read URL, 메시지 metadata의 public_url, 이미 발급한 유효 다운로드 URL, 브라우저 캐시·이미 다운로드한 파일/텍스트·이미 전달된 이벤트는 회수하지 못한다. 현재 Chat SDK 파일 업로드의 기본값은 public_read이므로 탈퇴를 첨부 기밀성 보장으로 설명하지 않는다. 새 Chat 참조 조회가 거절되어도 사용자가 미리 가진 공개 URL의 읽기 권한은 별개다.
HTTP 계약
Auth token provider에서 받은 service: "chat" app-user token과 X-Spectra-Project-Id를 사용한다.
POST leave는 신규 chat.rooms.leave, GET membership은 chat.rooms.read를 요구한다.
chat.rooms.leave는 app-user token grant 전용이며 Secret Key grant를 추가하지 않는다.
기존 read-only token에 mutation을 허용하는 fallback은 금지한다. SDK 사용자가 scope를 직접 설정하지 않으며,
기존 401/403 처리처럼 SDK가 token provider에 forceRefresh: true로 한 번만 재발급을 요청한다.
Auth가 새 grant를 발급하지 못하면 실패해야 하며 read scope로 우회하지 않는다.
본인 identity는 인증 결과에서 얻고 요청 body로 다른 사용자 ID를 받지 않는다. 서버 Secret Key를 브라우저에 넣지 않는다.
Auth·Chat 서버 배포와 SDK 0.2.0 발행 후 활성화할 계약이며 현재 모두 undeployed / unpublished다.
POST /v1/chat/rooms/{room_id}/leave: body 없음. 성공 HTTP 200, 본인의status: "left"를 반환한다.GET /v1/chat/rooms/{room_id}/membership: 본인 membership의 현재 snapshot. 성공 HTTP 200,status: "active"또는"left"를 반환한다. 과거 참여자도 탈퇴 상태만 조회할 수 있다.
{
"data": {
"room_id": "room_example",
"app_user_id": "app_user_example",
"status": "left",
"left_at": "2026-09-06T00:00:00.000Z"
}
}
active이면 left_at은 null이다. 같은 본인·Project·room에 대한 반복 leave는 HTTP 200과 최초 left_at을 유지한다. 마지막 참여자, 동시 중복 요청, 이미 left인 본인도 같은 규칙이다. 이는 인증 오류·알 수 없는 방까지 무조건 성공으로 변환한다는 뜻이 아니다. Idempotency-Key나 새 room 생성으로 복구할 필요가 없다.
본인 상태 이벤트
서버는 room.membership.updated를 같은 Project·본인의 열린 소켓에 보낸다. schema_version: 1 envelope의 room_id는 나간 방, occurred_at은 최초 left_at, payload는 HTTP 응답의 membership 객체다. 이 제어 이벤트는 다른 참여자에게 보내는 콘텐츠 이벤트가 아니며, 본인의 다른 방 소켓에도 도착할 수 있다. 다른 방의 연결은 닫지 않는다.
반복 leave마다 같은 상태 이벤트가 중복될 수 있으므로 room ID와 상태 기준으로 안전하게 처리한다. 이벤트 수신 횟수를 탈퇴 횟수로 해석하지 않는다. 연결이 끊긴 동안 이벤트 replay나 독립 서버 간 전달은 보장하지 않으므로 membership GET 확인 경로를 유지한다. leave 전에 이미 전달·enqueue된 메시지/이벤트는 회수하지 않는다.
Chat이 알고 있는 방의 call.* 알림도 active membership 기준으로 차단한다. 하지만 Chat 탈퇴는 Call session 종료나 SFU/media 접속 종료가 아니다. Chat이 모르는 legacy Call-owned room은 기존 Call의 Project/audience 계약을 유지하며 이 self-leave 보장 대상으로 확대하지 않는다. 실제 통화 종료가 필요하면 Call의 별도 계약을 따른다.
JS SDK의 대응 이벤트는 on("membership", handler)와 SpectraChatMembershipEvent다.
필드는 roomId, appUserId, status: "left", leftAt, occurredAt이다.
여러 탭/기기 중 같은 runtime의 열린 연결은 본인 탈퇴를 수신할 수 있지만, 오프라인 탭까지 즉시 갱신된다는 보장은 아니다. 이벤트를 놓치면 GET으로 상태를 확인한다.
SDK 사용 예제
아래 예제는 신규 0.2.0 소스용이다. 발행된 0.1.3이나 기존 운영 API에 그대로 연결하면 동작하지 않는다. 공식 baseUrl 설정으로 준비된 로컬 서버에서 검증하며, 운영 데이터를 재생성하거나 앱 임시 endpoint 우회로 해결하지 않는다.
leaveRoom은 SpectraChatLeaveRoomResult, getRoomMembership은 SpectraChatRoomMembership을 반환한다. 결과는 HTTP snake_case 대신 roomId, appUserId, status, leftAt이다. membership은 active/leftAt: null 또는 left/leftAt: string이며 leave 성공은 left 분기만 허용한다.
options의 timeoutMs는 기본 10000, 양의 정수이며 최대 2147483647이다. token 획득·응답 해석을 포함한 요청 제한 시간이다. 선택 signal: AbortSignal로 요청을 취소할 수 있지만 서버에서 이미 완료된 탈퇴를 되돌리지 않는다. 이 두 API는 타임아웃을 자동 성공으로 바꾸지 않는다.
import type {
SpectraChatClient,
SpectraChatLeaveRoomResult,
} from "@spectra-platform/chat-sdk";
// Use the same authenticated client throughout this operation.
export async function leaveAndConfirm(
chat: SpectraChatClient,
roomId: string,
): Promise<SpectraChatLeaveRoomResult> {
try {
return await chat.leaveRoom(roomId, { timeoutMs: 10_000 });
} catch (leaveError) {
try {
const membership = await chat.getRoomMembership(roomId, { timeoutMs: 10_000 });
if (membership.status === "left") return membership;
} catch (confirmationError) {
throw new AggregateError(
[leaveError, confirmationError],
"Leave outcome is unconfirmed; keep both errors for diagnostics.",
);
}
// Active is a snapshot: the original leave may still be in flight.
throw leaveError;
}
}
export function observeOwnLeave(
chat: SpectraChatClient,
currentAppUserId: string,
removeRoomView: (roomId: string) => void,
) {
return chat.on("membership", (event) => {
if (event.appUserId !== currentAppUserId) return;
removeRoomView(event.roomId); // Make this operation safe to repeat.
});
}
소비자는 leaveAndConfirm이 성공했을 때만 나가기 완료 UI를 확정한다. 실패하면 원본 오류를 보존하며 확인 불가 상태를 유지한다. leave 중 Auth 사용자가 바뀌면 새 사용자로 확인/재시도하지 않는다. 사용자 전환 시 해당 작업 UI 결과를 폐기하고, 동일 identity로 다시 확인할 때까지 다른 사용자의 상태로 대체하지 않는다.
SDK는 성공/left 확인 시 해당 요청이 시작될 때의 연결과 현재 방·연결 세대가 같은 경우에만 그 방 연결을 정리한다. 기다리는 동안 다른 방으로 이동했다면 새 연결을 닫지 않는다. 다른 client 인스턴스나 Auth 세션을 logout하지 않는다. membership 이벤트도 나간 방만 정리하며 다른 방 연결을 보존한다. 일반 disconnect()에는 이러한 서버 membership 변경이 없다.
방별 WebSocket 오류는 envelope의 room_id로 대상을 식별한다. 예를 들어 A방 권한 오류가 B방의 연결을 닫아서는 안 된다. room_id가 없는 legacy 오류는 대상 방을 추측해 연결을 종료하지 않는다. CHAT_NOT_FOUND도 탈퇴 완료 증거는 아니며 membership 확인과 구분한다.
타임아웃 확인
응답을 받지 못했거나 응답 해석이 실패했다는 사실만으로 서버의 변경 실패를 단정하지 않는다. 요청이 서버에 도달해 성공했지만 응답만 유실될 수 있다.
1. 같은 Auth 사용자·Project·room ID를 유지한다. UI에서는 확인 중으로 두고 새 방 생성이나 재가입을 수행하지 않는다.
2. membership 조회의 left를 확인하면 나가기 완료로 처리한다.
3. active이면 조회 시점에는 아직 active다. 지연 중인 최초 요청은 이후 처리될 수 있으므로 실패 확정이나 자동 연결 재개 근거로 삼지 않는다. 나가기 의도가 유지되면 동일 leave를 재시도한 뒤 확인한다.
4. 조회도 실패하거나 404면 완료가 아니라 확인 불가다. room 목록 부재, 소켓 종료, 로컬 숨김만으로 성공 처리하지 않는다. 무한 재시도하지 말고 code, status, requestId와 endpoint·버전을 보존해 CS로 전달한다.
오류와 복구
400 CHAT_BAD_REQUEST: leave에 사용자 identity/options 등 비어 있지 않은 body를 보낸 경우다. 문서상 요청은 body 없음이며 현재 서버의 빈{}허용을 다른 사용자 지정 기능으로 해석하지 않는다.404 CHAT_NOT_FOUND: 존재하지 않는 방, 참여한 적 없는 사용자, 다른 Project 방을 구별해 노출하지 않는다. 탈퇴한 사용자의 메시지·read·새 ticket 접근도 404로 거절한다. membership/leave의 404를 성공적인 탈퇴 확인으로 간주하지 않는다.401 CHAT_UNAUTHORIZED: Chat token의 누락·무효·scope/Project 불일치 또는 비활성 grant. 특히 신규chat.rooms.leave가 없는 기존4-scope/read-only token의 POST leave는401이다. token 재발급 후에도 거절되면 우회하지 않는다. membership 상태를 추론하지 말고 인증 경계를 확인한다.503 CHAT_AUTH_UNAVAILABLE: Auth introspection 의존성을 확인할 수 없다. 재시도 가능한 의존성 오류이며 탈퇴 완료 증거가 아니다.- 신규 local PG의
503 CHAT_STORAGE_UNAVAILABLE/CHAT_STORAGE_NOT_READY: DB 또는 schema·이관·authority gate 문제다. 빈 결과나 탈퇴 완료로 해석하지 않으며 PG 계약을 따른다. 401 CHAT_SOCKET_TICKET_INVALID: 탈퇴 전에 발급한 ticket도 handshake 시 현재 membership을 재검사하므로 재사용할 수 없다. socket ticket을 새로 받는 것으로 재가입하지 않는다.- handshake와 연결 등록 사이에 membership을 잃은 경합은 표준 WebSocket close
1008로 연결을 거절할 수 있다. 이를 다른 방 접근 종료나 Auth logout으로 확대하지 않는다. APP_SESSION_UNAUTHORIZED: Auth SDK가 Chat service token을 받기 전 실패한 경우다. Chat room 문제로 단정하지 않고 AuthrequestId를 확인한다.- 네트워크 오류·타임아웃·응답 유실/해석 실패: 결과 불확정이다. 본인 membership 조회로 확인하고 오류 metadata를 보존한다.
- SDK
REQUEST_TIMEOUT,REQUEST_ABORTED,NETWORK_UNAVAILABLE: 제한 시간·취소·네트워크 실패. 서버 결과는 membership 확인 전까지 불확정이다. - SDK
REQUEST_TIMEOUT_INVALID: 유효하지 않은 제한 시간 옵션,ROOM_ID_REQUIRED: 빈 room ID. 입력을 고친다. - SDK
RESPONSE_INVALID: 응답 형식이 계약과 다르거나 leave 결과가active다. 성공으로 간주하지 않고 확인한다. 서버 오류의code,status,requestId,retryable은 제공되는 범위에서 보존한다.
릴리즈 상태
대상 · 확인한 버전/근거 · 상태와 한계
실제 npm 발행본 · GitHub Packages npm registry의 @spectra-platform/chat-sdk latest 0.1.3; 역할70 실제 조회·tarball 확인 · published, leaveRoom/getRoomMembership 없음. npmjs 공개 배포 확인이 아님
현재 공개 문서 · SDK/Agent HTTP200 본문에는 0.1.0 표기 · registry와 문서 drift. 위 버전표로 정정했으나 문서 변경 자체는 undeployed
신규 SDK 소스 · 0.2.0, leave 소스 d2f1610, P4 test/packed 소스 bd9b877; 역할70 unit49/typecheck/build/example build 통과 · local-verified / unpublished. API/버전/의존성 그대로이며 실제 npm 설치의 latest와 다름
신규 Auth grant · grant 5f3a59c, 실제 local P4 producer 97e2419; app-user 전용 chat.rooms.leave · local-verified / undeployed. 실제 HTTP/PG/JWKS 검증, Secret Key grant·기존 token 소급 승격 없음
선행 Chat leave · 46b7a39; 역할50 실제 loopback HTTP/WS 40 tests 및 lint/typecheck/build 통과 · 당시 memory 범위의 local-verified / undeployed. 후속 PG 증거와 별도
후속 Chat PG · 192f583; 역할50 full check 67 pass/2 opt-in skip, 별도 snapshot PG 8/8 및 restore app oracle 1/1 · local-only / undeployed. 합성 PostgreSQL 보존·복원 검증이며 실제 Auth 전체 E2E·기존 운영 이관·다중 인스턴스 보장 아님
최종 local P4 · Chat 8540121, Auth 97e2419, packed Chat bd9b877·Auth SDK 94fb11d; 9 subtests+parent+guard2, 총12 pass/0skip · 실제 HTTP/PG/JWKS와 두 SDK 통합. 초기 session/legacy token은 test-control, 외부 Provider·운영 이관·배포·발행은 미검증/미실행
현재 운영 Chat · 역할80 확인 image 578647a · leave/membership 미지원, Map only/no data mount. 보존 대책 없는 재배포 금지
이 공식 문서 · 로컬 build/계약/예제 검증 · undeployed. 공개 사이트에 반영됐다는 뜻이 아님
Auth 검증은 역할20의 focused57/전체504 tests 통과, live provider smoke1개 skip,
typecheck/lint/build 통과다. in-memory repository/local RS256 및 HTTP service stub 범위이며,
운영 JWT self-check, 실제 Auth와 Chat·SDK를 함께 연결한 E2E 결과가 아니다.
선행 Chat leave 검증은 Auth introspection fixture와 같은 프로세스의 메모리 저장소 범위다.
별도 허브가 동일 repository 객체를 공유하는 검사는 독립 프로세스·분산 DB 검증이 아니다.
역할70은 최종 Chat 46b7a39의 실제 HTTP/WS와 패키징한 SDK0.2.0을 연결해
6개 시나리오(총7개 Node tests)를 통과했다. 응답 유실 후 GET 확인, 새scope token1회갱신,
본인membership 이벤트 및 A방의 지연 오류가 B방을 끊지 않는 회귀(legacy roomless오류 포함)를 검증했다.
이 통합은 Auth fixture를 사용하며 실제 Auth 5f3a59c를 함께 연결한 검증은 아니다.
기존 설치 의존성을 사용한 tarball 검증이며 clean registry install이나 실제 브라우저 UI 검증은 아니다.
SDK의 examples/web/src/leave-room.ts는 독립 사용 예제이며 기존 웹 데모에 나가기 버튼을 추가했다는 뜻이 아니다.
문서 담당은 생성 HTML·링크/앵커·예제 실행 검사4개와 실제SDK소스 대상 예제 strict타입 검사를 수행했다.
위 내용은 선행 leave 작업의 검증이다. 후속 SDK 145952f는 최종 Chat 192f583의 고정 빌드와
새 합성 PG에서 packed 7 subtests/총8을 통과했다. Auth fixture·실제 API PID 교체2회 범위이며,
최종 고정 빌드의 DB container restart나 실제 Auth 전체 E2E는 실행하지 않았다.
문서 후속 검증은 PG 계약·비공개 문서 회귀를 포함한 총5개다.
후속 실제 local P4는 위 표의 최종 Chat/Auth 고정 빌드와 두 packed SDK로 재실행했다.
old/read-only 거절·1회 refresh, timeout 뒤 membership 확인, identity 교체·늦은 응답·타방 연결 유지,
API PID 교체 후 history/read/최초 leftAt/dedup·old ticket 거절을 검증했다.
이는 DB container restart나 다른 binary rollback을 SDK가 실행한 결과가 아니다.
Auth는 같은 project/service에 단일 environment만 활성화한다. 다른 Test/Live 프로젝트와
같은 프로젝트의 환경 변경 거절·원복을 구분했으며 정책을 바꾸지 않았다.
운영·실기기: public Auth/Chat 탈퇴·WebSocket, 외부 Provider/Storage, Modo 적용 검증 미실행.
이 작업은 Modo를 변경하지 않는다. 다음 단계는 별도 장애/호환 rollback 증거와 운영 원본 보존 gate를
구분하는 것이다. 이번에는 push/publish/deploy를 수행하지 않는다.
데이터 보존 대책이 없는 상태에서 운영 배포 승인을 요청하는 단계로 넘어가지 않는다.
Changelog
- 2026-09-06 / Unreleased: 실제 self-leave와 본인 membership 확인 계약을 추가했다. 연결 종료와 구분하고, 반복 요청·타임아웃·마지막 참여자·기록 접근·재개/재초대 미지원·Storage 회수 한계·메모리 저장소 한계를 명시했다.