본문으로 건너뛰기

ChatManager

채팅 시스템의 싱글톤 매니저 클래스입니다. 채팅 서버 연결 및 메시지 송수신을 관리합니다.

URL 확인

이 API는 service-api.playnanoo.com 도메인을 사용합니다.

API 정보

  • 서버 목록 URL: https://service-api.playnanoo.com/chat/v20211101/server
  • 필터 URL: https://service-api.playnanoo.com/chat/v20211101/filter
  • Method: PUT
  • 인증 필요: 아니오

주요 기능

메서드설명
Connect채팅 서버에 연결
IsConnected연결 상태 확인
Subscribe채널 구독 (이전 메시지 개수 설정 가능)
Unsubscribe채널 구독 해제
GetChannels채널 목록 조회
GetPlayersOnline플레이어 온라인 상태 조회
SendPublicMessage공개 메시지 전송
SendPrivateMessage비공개 메시지 전송
FetchFilterWords금칙어 목록 가져오기
Filter메시지에 금칙어 필터 적용

Unreal C++ 구현

헤더 파일 (ChatManager.h)

// ChatManager.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Http.h"
#include "ChatClient.h"
#include "ChatModels.h"
#include "ChatManager.generated.h"

UCLASS()
class YOURPROJECT_API AChatManager : public AActor
{
GENERATED_BODY()

public:
// 싱글톤 인스턴스
static AChatManager* GetInstance();

AChatManager();

virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

// Public API
void Connect(IChatListener* InListener);
bool IsConnected() const;
void Subscribe(const FString& Channel, int32 PrevMessageCount = 0);
void Unsubscribe(const FString& Channel);
void GetChannels();
void GetPlayersOnline(const TArray<FString>& UserIds);
void SendPublicMessage(const FString& Channel, const FString& Text);
void SendPrivateMessage(const FString& TargetUserId, const FString& Text);

// 금칙어 필터
void FetchFilterWords();
FString Filter(const FString& Message, TCHAR Separator = TEXT('*'));

private:
static const FString CHAT_SERVER_URL;
static const FString CHAT_FILTER_URL;

static AChatManager* Instance;

TUniquePtr<FChatClient> ChatClient;
IChatListener* Listener;
TArray<FString> FilterWords;

void FetchServerListAndConnect();
};

구현 파일 (ChatManager.cpp)

// ChatManager.cpp
#include "ChatManager.h"
#include "PlayNANOOHelper.h"

const FString AChatManager::CHAT_SERVER_URL = TEXT("https://service-api.playnanoo.com/chat/v20211101/server");
const FString AChatManager::CHAT_FILTER_URL = TEXT("https://service-api.playnanoo.com/chat/v20211101/filter");

AChatManager* AChatManager::Instance = nullptr;

AChatManager* AChatManager::GetInstance()
{
return Instance;
}

AChatManager::AChatManager()
: Listener(nullptr)
{
PrimaryActorTick.bCanEverTick = true;
}

void AChatManager::BeginPlay()
{
Super::BeginPlay();

if (Instance != nullptr && Instance != this)
{
Destroy();
return;
}
Instance = this;
}

void AChatManager::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

// Unreal WebSocket은 자동으로 이벤트를 처리하므로
// Unity처럼 Service() 호출이 필요하지 않습니다.
}

void AChatManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (Instance == this)
{
Instance = nullptr;
}

if (ChatClient.IsValid())
{
ChatClient->Disconnect();
ChatClient.Reset();
}

Super::EndPlay(EndPlayReason);
}

void AChatManager::Connect(IChatListener* InListener)
{
Listener = InListener;

// 사용자 닉네임 설정 (DataManager에서 가져온다고 가정)
FString Nickname = UGameDataManager::Get()->GetNickname();
ChatClient = MakeUnique<FChatClient>(Listener, Nickname);

FetchServerListAndConnect();
}

void AChatManager::FetchServerListAndConnect()
{
// 플레이어 정보가 포함된 요청 바디 생성
TSharedPtr<FJsonObject> Body = FPlayNANOOHelper::CreateRequestBody();
FString JsonBody = FPlayNANOOHelper::ToJsonString(Body);

// HTTP 요청
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(CHAT_SERVER_URL);
Request->SetVerb(TEXT("PUT"));
FPlayNANOOHelper::SetCommonHeaders(Request, false); // 인증 토큰 불필요
Request->SetContentAsString(JsonBody);

Request->OnProcessRequestComplete().BindLambda(
[this](FHttpRequestPtr Req, FHttpResponsePtr Res, bool bSuccess)
{
if (bSuccess && Res.IsValid() && Res->GetResponseCode() >= 200 && Res->GetResponseCode() < 300)
{
FChatServerResponse ServerResponse;
if (ServerResponse.FromJson(Res->GetContentAsString()))
{
if (ServerResponse.Servers.Num() > 0)
{
// 랜덤 서버 선택
int32 Index = FMath::RandRange(0, ServerResponse.Servers.Num() - 1);
ChatClient->Connect(ServerResponse.Servers[Index]);
}
else if (Listener)
{
Listener->OnError(TEXT("SERVER_LIST_FAILED"), TEXT("No servers available"));
}
}
else if (Listener)
{
Listener->OnError(TEXT("SERVER_LIST_FAILED"), TEXT("Failed to parse server list"));
}
}
else if (Listener)
{
Listener->OnError(TEXT("SERVER_LIST_FAILED"), TEXT("HTTP request failed"));
}
});

Request->ProcessRequest();
}

bool AChatManager::IsConnected() const
{
return ChatClient.IsValid() && ChatClient->GetIsConnected();
}

void AChatManager::Subscribe(const FString& Channel, int32 PrevMessageCount)
{
if (ChatClient.IsValid())
{
ChatClient->Subscribe(Channel, PrevMessageCount);
}
}

void AChatManager::Unsubscribe(const FString& Channel)
{
if (ChatClient.IsValid())
{
ChatClient->Unsubscribe(Channel);
}
}

void AChatManager::GetChannels()
{
if (ChatClient.IsValid())
{
ChatClient->GetChannels();
}
}

void AChatManager::GetPlayersOnline(const TArray<FString>& UserIds)
{
if (ChatClient.IsValid())
{
ChatClient->GetPlayersOnline(UserIds);
}
}

void AChatManager::SendPublicMessage(const FString& Channel, const FString& Text)
{
if (ChatClient.IsValid())
{
ChatClient->SendPublicMessage(Channel, Text);
}
}

void AChatManager::SendPrivateMessage(const FString& TargetUserId, const FString& Text)
{
if (ChatClient.IsValid())
{
ChatClient->SendPrivateMessage(TargetUserId, Text);
}
}

void AChatManager::FetchFilterWords()
{
// 플레이어 정보가 포함된 요청 바디 생성
TSharedPtr<FJsonObject> Body = FPlayNANOOHelper::CreateRequestBody();
FString JsonBody = FPlayNANOOHelper::ToJsonString(Body);

// HTTP 요청
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(CHAT_FILTER_URL);
Request->SetVerb(TEXT("PUT"));
FPlayNANOOHelper::SetCommonHeaders(Request, false); // 인증 토큰 불필요
Request->SetContentAsString(JsonBody);

Request->OnProcessRequestComplete().BindLambda(
[this](FHttpRequestPtr Req, FHttpResponsePtr Res, bool bSuccess)
{
if (bSuccess && Res.IsValid() && Res->GetResponseCode() >= 200 && Res->GetResponseCode() < 300)
{
FChatFilterResponse FilterResponse;
if (FilterResponse.FromJson(Res->GetContentAsString()))
{
FilterWords = FilterResponse.FilterWords;
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[Chat] Failed to fetch filter words"));
}
});

Request->ProcessRequest();
}

FString AChatManager::Filter(const FString& Message, TCHAR Separator)
{
if (FilterWords.Num() == 0)
{
return Message;
}

FString Result = Message;
for (const FString& Word : FilterWords)
{
if (!Word.IsEmpty())
{
FString Replacement;
for (int32 i = 0; i < Word.Len(); i++)
{
Replacement.AppendChar(Separator);
}
Result = Result.Replace(*Word, *Replacement, ESearchCase::IgnoreCase);
}
}
return Result;
}

사용 예제

// ChatExample.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ChatManager.h"
#include "ChatExample.generated.h"

UCLASS()
class YOURPROJECT_API AChatExample : public AActor, public IChatListener
{
GENERATED_BODY()

public:
virtual void BeginPlay() override;

// IChatListener 구현
virtual void OnConnected() override;
virtual void OnDisconnected() override;
virtual void OnError(const FString& Code, const FString& Message) override;
virtual void OnChannels(const TArray<FChatChannelInfo>& Channels) override;
virtual void OnSubscribed(const FChatUserInfo& User) override;
virtual void OnUnSubscribed(const FChatUserInfo& User) override;
virtual void OnPublicMessage(const FChatUserInfo& Sender, const FString& Message) override;
virtual void OnPrivateMessage(const FChatUserInfo& Sender, const FString& Message) override;
virtual void OnNotifyMessage(const FChatUserInfo& Sender, const FString& Message) override;
virtual void OnPlayerOnline(const TArray<FChatPlayerInfo>& Players) override;

void SendMessage();
};

// ChatExample.cpp
#include "ChatExample.h"

void AChatExample::BeginPlay()
{
Super::BeginPlay();
AChatManager::GetInstance()->Connect(this);
}

void AChatExample::OnConnected()
{
UE_LOG(LogTemp, Log, TEXT("채팅 서버 연결 성공"));
// 금칙어 목록 가져오기
AChatManager::GetInstance()->FetchFilterWords();
// 이전 채팅 내역 10개 가져오기
AChatManager::GetInstance()->Subscribe(TEXT("global"), 10);
}

void AChatExample::OnDisconnected()
{
UE_LOG(LogTemp, Log, TEXT("채팅 서버 연결 해제"));
}

void AChatExample::OnError(const FString& Code, const FString& Message)
{
UE_LOG(LogTemp, Error, TEXT("채팅 오류: [%s] %s"), *Code, *Message);
}

void AChatExample::OnChannels(const TArray<FChatChannelInfo>& Channels)
{
for (const auto& Ch : Channels)
{
UE_LOG(LogTemp, Log, TEXT("채널: %s, 인원: %d"), *Ch.channel, Ch.count);
}
}

void AChatExample::OnSubscribed(const FChatUserInfo& User)
{
UE_LOG(LogTemp, Log, TEXT("%s님이 입장했습니다."), *User.visitorName);
}

void AChatExample::OnUnSubscribed(const FChatUserInfo& User)
{
UE_LOG(LogTemp, Log, TEXT("%s님이 퇴장했습니다."), *User.visitorName);
}

void AChatExample::OnPublicMessage(const FChatUserInfo& Sender, const FString& Message)
{
// 금칙어 필터 적용
FString FilteredMsg = AChatManager::GetInstance()->Filter(Message);
UE_LOG(LogTemp, Log, TEXT("[%s]: %s"), *Sender.visitorName, *FilteredMsg);
}

void AChatExample::OnPrivateMessage(const FChatUserInfo& Sender, const FString& Message)
{
// 금칙어 필터 적용
FString FilteredMsg = AChatManager::GetInstance()->Filter(Message);
UE_LOG(LogTemp, Log, TEXT("[귓속말][%s]: %s"), *Sender.visitorName, *FilteredMsg);
}

void AChatExample::OnNotifyMessage(const FChatUserInfo& Sender, const FString& Message)
{
FString FilteredMsg = AChatManager::GetInstance()->Filter(Message);
UE_LOG(LogTemp, Log, TEXT("[알림]: %s"), *FilteredMsg);
}

void AChatExample::OnPlayerOnline(const TArray<FChatPlayerInfo>& Players)
{
for (const auto& P : Players)
{
UE_LOG(LogTemp, Log, TEXT("플레이어: %s, 온라인: %s"), *P.userUniqueId, *P.online);
}
}

void AChatExample::SendMessage()
{
// 메시지 전송 (서버에 원본 그대로 전송)
AChatManager::GetInstance()->SendPublicMessage(TEXT("global"), TEXT("안녕하세요!"));
}