오락실 공식 계정을 확장하는 과정에서, 서비스 안에서 문의를 받고 답하고 싶다는 오락실의 요구가 생겼다. 이용자는 업장에 질문을 남기고, 담당자는 관리 화면에서 이를 확인해 답할 수 있어야 했다. 담당자가 바뀐 뒤에도 이전 대화를 이어서 볼 수 있다는 점이 개인 간 메시지와 다른 조건이었다.
뮤즈캣의 현재 Preview는 개인 DM, 오락실 문의와 뮤즈캣 팀 문의에 공통 메시지 엔진을 사용한다. 여기서 엔진은 메시지를 저장하고 순서를 정하며 끊긴 대화를 복구하는 내부 처리부를 뜻한다. 주고받는 글의 형식은 함께 사용하되, 참여할 수 있는 사람과 공식 답장의 명의, 문의의 처리 상태는 대화의 성격에 맞게 설계했다.
개인 DM은 두 이용자만 참여하는 비공개 대화다. 새 메시지를 보내려면 상대방의 수신 설정과 양쪽의 차단 관계를 확인해야 한다. 운영자에게도 DM 전체를 열람하는 권한을 주지 않으며, 이미 참여한 대화의 이력을 읽는 조건과 앞으로 메시지를 받을 조건을 따로 적용한다.
공식 문의는 이용자가 개인 담당자에게 보내는 글보다 업장의 수신함에 남기는 질문에 가깝다. 예를 들어 담당 계정이 바뀌어도 업장에 접수된 문의는 새 담당자가 응대할 수 있어야 한다. 따라서 과거 담당자를 영구 참여자로 복사하는 대신, 조회와 전송 때마다 현재의 공식 계정 관계와 운영 역할을 확인한다. 뮤즈캣 팀 문의에도 해당 수신함의 권한을 적용한다.
오락실은 새 문의를 사이트에서 받거나 외부 연락처로 안내하거나 접수를 닫을 수 있다. 이 설정이 기존 문의를 볼 자격까지 바꾸지는 않는다. 메시지 저장을 공통으로 구현하면서 수신함마다 이런 조건을 적용하면, 재전송이나 첨부 처리를 중복해서 만들지 않고도 각 대화의 공개 범위를 유지할 수 있다.
저장 구조의 중심에는 대화가 있고 그 아래에 메시지가 쌓인다. 대화는 종류와 마지막 메시지 순서를 가지며, 서버가 각 메시지에 부여하는 sequence가 표시 순서의 기준이 된다. 읽음 위치는 이용자별로 저장하므로 담당자 한 명이 확인한 내용을 다른 담당자도 읽었다고 표시하지 않는다.
공식 문의에는 요청자, 수신함과 처리 상태를 보관하는 자료가 추가된다. 개인 DM에는 필요하지 않은 업무 정보를 문의에만 붙이는 방식이다. 오락실 관리 대시보드도 별도의 문의 사본을 만드는 화면이 아니라 이 대화를 찾아 들어가는 진입점이다.
대시보드에서는 접수 설정과 대기 중인 문의를 확인할 수 있다. 공지 관리나 영업 상태와 함께 표시되더라도 각 기능의 권한은 따로 검사한다. 대화의 읽음, 문의의 처리 단계와 오락실의 공개·폐점 여부는 각각 설명하는 대상이 다르기 때문이다.
이용자에게 오락실 이름으로 보이는 답장도 실제로는 로그인한 사람이 작성한다. 담당자가 바뀐 뒤 과거 답변을 확인하려면 당시의 작성자와 응대한 명의를 모두 알아야 한다. 서버는 실제 계정을 sender_user에, 대신 말하는 주체를 actingAs에 저장하고 당시 이름과 역할도 남긴다.
DM에서는 본인 명의만 쓸 수 있다. 문의에서는 요청자와 공식 응대자의 자격을 확인하며, 한 사람이 두 자격을 모두 가졌다면 어느 쪽으로 말할지 선택하게 한다. 화면이 서버에서 가능한 명의와 상태 변경을 받아 표시한 뒤에도, 전송 시 서버가 같은 권한을 다시 확인한다.
첨부 사진은 대화를 읽을 수 있는 사람에게 제공한다. 브라우저의 로그인 세션을 같은 출처의 API를 통해 서버 인증으로 연결하고 대화·메시지·파일의 소속을 검사한 뒤 보호된 저장소에서 읽는다. 파일명이나 메시지 URL만 알고 있는 요청에는 사진을 내주지 않는다. 업로드의 실제 파일 형식과 크기도 서버에서 검증한다.
전송 버튼을 누른 뒤 응답을 받지 못하면 이용자는 저장 여부를 알 수 없다. 예를 들어 서버에는 이미 저장됐지만 응답을 기다리다 연결이 끊긴 경우, 다시 누른 전송을 새 메시지로 만들면 같은 문장이 두 번 나타난다. 이를 막기 위해 화면은 작성한 항목에 clientMessageId를 붙이고 재시도에서도 ID와 내용을 유지한다.
서버는 대화·실제 작성자·클라이언트 ID의 조합으로 기존 요청을 찾는다. 본문과 사진의 해시가 같으면 저장된 메시지를 반환하고, 같은 ID에 다른 내용이나 명의를 사용하면 충돌로 거절한다. 새 전송은 순서 증가, 메시지 저장, 마지막 메시지 정보, 문의 상태, 수신자의 알림함과 발송 대기열까지 하나의 데이터베이스 트랜잭션에서 처리한다. 실패하면 이 묶음을 취소하고, 외부 푸시와 대화 변경 이벤트는 커밋 성공 후 처리한다.
화면에는 전송 중인 임시 항목을 표시한다. 응답을 받은 뒤에도 조회 이력에서 같은 클라이언트 ID를 확인할 때까지 이어 두고 실제 메시지와 합친다. 실패한 항목은 재전송하거나 지울 수 있다. 다만 이 임시 전송함은 화면 메모리에 있으므로 브라우저를 닫은 뒤에도 남는 영구 오프라인 전송함은 아니다.
대화를 열어 둔 사람에게 새 메시지를 알리는 데는 SSE를 사용한다. SSE는 서버가 브라우저에 변경 신호를 보내는 연결이다. 페이지의 공용 Provider가 로그인 계정의 연결 하나를 유지하고 메시지 목록과 알림함이 이를 함께 사용하므로, 창마다 별도 연결을 만들지 않는다.
conversation.changed 신호에는 대화 ID와 최신 순서가 들어 있다. 화면은 가지고 있는 마지막 순서 이후의 메시지를 API로 조회한 뒤 ID로 합치고 sequence 순으로 정렬한다. 첫 조회 도중 신호가 오더라도 뒤이어 빠진 구간을 가져온다. 신호 자체에 대화 전체를 의존하지 않으므로 잠든 탭이 돌아오거나 연결이 끊겼을 때도 서버 이력으로 복구할 수 있다.
재연결과 재동기화 외에 보완 조회도 사용한다. 정상 연결에서는 30초마다, 화면이 보이면서 연결이 끊긴 동안에는 5초마다 이력을 확인한다. 따라잡기 한 번의 작업량은 제한하고 남은 부분은 다음 갱신으로 이어 간다. 세션이 만료되거나 접근 권한이 없어지면 해당 대화의 이력과 임시 전송 항목을 비워, 예전에 받아 둔 비공개 내용을 계속 표시하지 않는다.
문의에 답장을 남겼다고 해서 현장의 문제까지 바로 해결되는 것은 아니다. 예를 들어 담당자가 기체 상태를 확인하겠다고 답한 경우, 이용자는 답장을 읽었더라도 수리 결과를 기다릴 수 있다. 이런 상황을 표현하려면 대화를 어디까지 읽었는지와 문의가 어느 단계에 있는지를 따로 관리해야 한다.
읽음 위치는 각 이용자가 확인한 마지막 메시지의 순번이다. 서버는 이를 lastReadSequence로 보관하고 이미 확인한 순번보다 뒤로 내리지 않는다. 화면은 대화가 활성 상태로 보이고 마지막 메시지까지 확인하고 있을 때 위치를 보낸다. 백그라운드 조회나 이벤트 도착만으로 읽음 처리하지 않는 이유다.
문의에는 대기·확인·완료 상태를 둔다. 요청자의 본인 명의 메시지는 대기로, 공식 명의의 답장은 확인으로 바꾼다. 요청자는 대기와 완료를 선택할 수 있고, 현재 응대 권한을 가진 담당자는 세 상태를 선택할 수 있다. 완료한 문의에도 요청자가 후속 질문을 쓰면 다시 대기가 되어 새 응대 대상으로 돌아온다.
새 메시지의 알림도 대화의 역할을 따른다. 오락실 문의에서 요청자가 쓴 일반 메시지는 현재 공식 담당자에게, 공식 답장은 요청자에게 알린다. 뮤즈캣 팀을 명시적으로 언급한 경우에는 플랫폼 담당자도 받는다. 이렇게 하면 같은 전송과 복구 기능을 사용하면서 담당자의 조회 자격, 알림 대상과 문의 진행 상황을 각각 설명할 수 있다.
관련 글과 자료: 알림함과 기기 푸시