세션 유지 및 중복 로그인 체크
주기적으로 서버에 세션 유지 신호를 보내고, 다른 기기에서 중복 로그인 여부를 확인하는 API입니다.
예제 코드 안내
이 문서의 예제 코드는 참고용으로 제공됩니다. 예제에서는 FTimerHandle을 사용한 타이머 기반 구현을 보여주지만, 프로젝트의 아키텍처와 코딩 컨벤션에 맞게 자유롭게 수정하여 사용하시기 바랍니다.
URL 확인
이 API는 service-account.playnanoo.com 도메인을 사용합니다.
API 정보
- URL:
https://service-account.playnanoo.com/api/v20240401/alive - Method:
PUT - 인증 필요: 예
요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| platform | string | 필수 | 플랫폼 (예: "aos", "ios") |
| device_id | string | 필수 | 기기 고유 ID |
| device_model | string | 필수 | 기기 모델명 |
| device_os | string | 필수 | 기기 OS |
| device_language | string | 필수 | 기기 언어 (예: "KO", "EN") |
DeviceInfo 상속
이 API의 Req 클래스는 DeviceInfo를 상속받습니다. DeviceInfo의 모든 속성이 자동으로 포함됩니다.
응답 데이터
정상 응답
| 필드 | 타입 | 설명 |
|---|---|---|
| Status | string | 상태 (정상: "success") |
에러 응답
| 에러 코드 | 설명 |
|---|---|
| 30006 | DuplicatedDeviceException - 다른 디바이스에서 이미 인증되었습니다 |
Unreal C++ 구현
CheckAlive 클래스
// CheckAlive.h
#pragma once
#include "CoreMinimal.h"
#include "Http.h"
#include "Json.h"
#include "CheckAlive.generated.h"
USTRUCT(BlueprintType)
struct FCheckAliveResponse
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
FString Status;
UPROPERTY(BlueprintReadOnly)
FString ErrorCode;
UPROPERTY(BlueprintReadOnly)
FString Message;
};
DECLARE_DELEGATE_OneParam(FOnCheckAliveSuccess, const FCheckAliveResponse&);
DECLARE_DELEGATE_OneParam(FOnCheckAliveError, const FCheckAliveResponse&);
UCLASS()
class YOURPROJECT_API UCheckAlive : public UObject
{
GENERATED_BODY()
public:
void Send(FOnCheckAliveSuccess OnSuccess, FOnCheckAliveError OnError);
private:
static const FString Path;
};
// CheckAlive.cpp
#include "CheckAlive.h"
#include "YourHelper.h"
const FString UCheckAlive::Path = TEXT("https://service-account.playnanoo.com/api/v20240401/alive");
void UCheckAlive::Send(FOnCheckAliveSuccess OnSuccess, FOnCheckAliveError OnError)
{
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->SetVerb(TEXT("PUT"));
Request->SetURL(Path);
// 공통 헤더 설정 (인증 토큰 필요)
FYourHelper::SetCommonHeaders(Request, true);
// 요청 바디 생성
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetStringField(TEXT("platform"), FYourHelper::GetPlatform());
JsonObject->SetStringField(TEXT("device_id"), FYourHelper::GetDeviceId());
JsonObject->SetStringField(TEXT("device_model"), FYourHelper::GetDeviceModel());
JsonObject->SetStringField(TEXT("device_os"), FYourHelper::GetDeviceOS());
JsonObject->SetStringField(TEXT("device_language"), FYourHelper::GetDeviceLanguage());
FString RequestBody;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
Request->SetContentAsString(RequestBody);
Request->OnProcessRequestComplete().BindLambda(
[OnSuccess, OnError](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess)
{
FCheckAliveResponse Result;
if (bSuccess && Response.IsValid())
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
{
JsonObject->TryGetStringField(TEXT("Status"), Result.Status);
JsonObject->TryGetStringField(TEXT("ErrorCode"), Result.ErrorCode);
JsonObject->TryGetStringField(TEXT("Message"), Result.Message);
if (Result.ErrorCode.IsEmpty())
{
OnSuccess.ExecuteIfBound(Result);
}
else
{
OnError.ExecuteIfBound(Result);
}
}
}
else
{
Result.ErrorCode = TEXT("HTTP_ERROR");
Result.Message = TEXT("Request failed");
OnError.ExecuteIfBound(Result);
}
}
);
Request->ProcessRequest();
}
싱글턴 매니저 클래스에 추가
싱글턴 매니저에서 주기적으로 CheckAlive를 호출하고 중복 로그인을 감지합니다.
// YourSingleton.h에 추가
DECLARE_DELEGATE_OneParam(FOnDuplicateDetected, bool);
private:
bool bIsDuplicate = false;
FTimerHandle AliveTimerHandle;
FOnDuplicateDetected DuplicateCallback;
public:
// 세션 유지 시작 (최소 300초)
UFUNCTION(BlueprintCallable, Category = "YourProject|Session")
void CheckAliveStart(int32 DelayTime = 300);
// 세션 유지 중지
UFUNCTION(BlueprintCallable, Category = "YourProject|Session")
void CheckAliveStop();
// 중복 로그인 체크 콜백 등록
void CheckDuplicate(FOnDuplicateDetected Callback);
// YourSingleton.cpp에 추가
void UYourSingleton::CheckAliveStart(int32 DelayTime)
{
if (AliveTimerHandle.IsValid())
{
return;
}
int32 Timer = FMath::Max(DelayTime, 300);
GetWorld()->GetTimerManager().SetTimer(
AliveTimerHandle,
[this]()
{
if (DataManager->GetAccessToken().IsEmpty())
{
return;
}
UCheckAlive* CheckAlive = NewObject<UCheckAlive>();
CheckAlive->Send(
FOnCheckAliveSuccess::CreateLambda([](const FCheckAliveResponse& Response)
{
// 정상 응답 - 중복 아님
UE_LOG(LogTemp, Log, TEXT("Session alive: %s"), *Response.Status);
}),
FOnCheckAliveError::CreateLambda([this](const FCheckAliveResponse& Response)
{
// 30006: DuplicatedDeviceException - 다른 디바이스에서 이미 인증됨
if (Response.ErrorCode == TEXT("30006"))
{
bIsDuplicate = true;
DuplicateCallback.ExecuteIfBound(true);
}
})
);
},
Timer,
true // 반복 실행
);
}
void UYourSingleton::CheckAliveStop()
{
if (AliveTimerHandle.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(AliveTimerHandle);
AliveTimerHandle.Invalidate();
}
}
void UYourSingleton::CheckDuplicate(FOnDuplicateDetected Callback)
{
DuplicateCallback = Callback;
}
사용 예제
기본 사용법
void AYourGameMode::BeginPlay()
{
Super::BeginPlay();
// 로그인 성공 후 세션 유지 시작 (300초마다)
UYourSingleton::Get()->CheckAliveStart(300);
// 중복 로그인 체크 콜백 등록
UYourSingleton::Get()->CheckDuplicate(
FOnDuplicateDetected::CreateLambda([](bool bIsDuplicate)
{
if (bIsDuplicate)
{
UE_LOG(LogTemp, Error, TEXT("Duplicate connection has been detected."));
// 강제 로그아웃 처리
// 예: 로그인 화면으로 이동, 토큰 삭제 등
}
})
);
}
로그아웃 시 세션 유지 중지
void AYourGameMode::Logout()
{
// 세션 유지 중지
UYourSingleton::Get()->CheckAliveStop();
// 기타 로그아웃 처리...
}
동작 흐름
- 로그인 성공 후
CheckAliveStart(300)호출 - 300초마다 서버에 alive 요청 전송
- 정상 응답: 세션 유지됨
- 에러 30006: 다른 기기에서 로그인됨 →
bIsDuplicate = true - 콜백 호출: 중복 감지 시 등록된 콜백 실행
- 콜백 처리: 강제 로그아웃, 알림 표시 등
주의사항
중복 호출 금지
CheckAliveStart()는 앱 전체에서 단 한 번만 호출해야 합니다.
- 여러 레벨이나 액터에서 중복으로 호출하면 안 됩니다.
- 중복 호출 시 서버에서 비정상적인 요청 패턴으로 감지되어 오탐(false positive)으로 중복 로그인 에러(30006)가 발생할 수 있습니다.
- 싱글턴 패턴을 사용하여 한 곳에서만 세션 유지를 관리하세요.
- 이미 타이머가 실행 중인 경우 내부적으로 중복 호출을 방지하지만, 구조적으로 단일 호출 지점을 유지하는 것이 좋습니다.