- 마지막 업데이트
게임 제재
이해하기
부정행위 등으로 특정 이용자의 게임 이용을 막아야 할 때, 운영자는 스토브 파트너스에서 해당 이용자를 제재할 수 있어요.
제재가 등록되면 스토브 플랫폼이 게임 서버로 Kick 이벤트를 전달해요.
게임 서버는 이 이벤트를 받아 제재된 이용자를 게임에서 즉시 내보낼 수 있어요.
제재 시점별 동작 방식
제재는 이용자의 접속 상태에 따라 다르게 적용돼요.
| 상태 | 모바일 | PC |
|---|---|---|
| 접속 중(로그인된) 상태 | 정상 이용 가능 (제재 팝업 없음) 토큰 만료로 재로그인 시 제재 얼럿 노출 |
정상 이용 가능 (제재 팝업 없음) |
| 접속 전(로그인 안된) 상태 | 로그인 시 제재 얼럿 노출 | 런처 게임 시작 시 제재 얼럿 노출 |


접속 중 이용자 인게임 Kick 처리 필요
ㆍ 접속 중(로그인된) 상태에서는 게임이 스토브 제재 상태를 인지하지 못해요.
ㆍ 제재 등록 시 게임 서버에서 Kick, 클라이언트에서 로그아웃·종료 처리를 직접 해야 해요.
제재 이벤트 흐름
| 단계 | 동작 |
|---|---|
| ① 이벤트 전달 | 제재 등록 시 스토브가 게임에 등록된 URL로 Kick 이벤트 전달 |
| ② 게임 처리 | 제재 발생 안내, 인게임 Kick과 스토브 Logout 처리 |
| ③ 재로그인 | 이용자 재로그인 시 팝업으로 상세 제재 사유 확인 |
제재 해소 시 별도 이벤트 없음
ㆍ 게임은 제재 이용자에 대한 Kick 처리만 진행하면 돼요.
연동 전 게임 서버 환경 준비사항
ㆍ 게임 API 서버는 내부망 구성을 권장하며, 스토브 ↔ 게임 서버 호출에는 ACL 설정이 필요해요.
ㆍ Inbound ACL이 있다면 스토브 Event Broker의 환경별 NAT IP를 허용해요. (IP는 스토브 기술 담당자에게 문의)
개발하기
연동 개요
- 이용자 제재(Kick 이벤트) 처리 흐름
- PC 실행 흐름 — 재로그인 이용자 제재 여부 체크
- 모바일 실행 흐름 — 재로그인 이용자 제재 여부 체크
- 우회 이용자에 대한 추가 검증 흐름
Callback API 구성 가이드
CP사(개발사)가 직접 구현해 운영하는 엔드포인트로, STOVE Event Broker가 Kick 이벤트 발생 시 해당 URL을 호출해요.
API 엔드포인트
| 항목 | 내용 |
|---|---|
| URL | CP사 제공 (예: https://game.example.com/stove/kick) |
| Method | POST |
| Protocol | HTTPS (TLS 1.2 이상 필수) |
| Content-Type | application/json |
| 재전송 정책 | HTTP 200 미응답 시 동일 이벤트 최대 3회 재전송 |
Request — Body 명세
| Name | Type | Required | Description |
|---|---|---|---|
| event_reason | String | Y | 이벤트 발생 사유. 게임 제재의 경우 GAME_RESTRICT. |
| event_message | String | Y | 이벤트 메시지. |
| event_time | Long | Y | 이벤트 발생 일시 (milliseconds). |
| target_users | Long Array | Y | Kick 대상 이용자 식별자 목록. |
Sample — Request
curl --location --request POST '{endpoint}' \
--header 'Content-Type: application/json' \
--data-raw '{
"event_reason": "GAME_RESTRICT",
"event_message": "비정상적인 게임 내 거래 행위",
"event_time": 1739145600000,
"target_users": [ 20000000001, 20000000002, 20000000003, 20000000004, 20000000005 ]
}'
Sample — Response (Success)
HTTP/1.1 200 OK
{
"code": 0,
"message": "SUCCESS"
}
Sample — Response (Failure)
응답 코드가 200이 아니면 STOVE Event Broker가 동일 이벤트를 최대 3회 재전송해요.
HTTP/1.1 500 Internal Server Error
{
"code": 500,
"message": "FAILURE"
}
이용자 제재정보 조회 API
게임 서버에서 이용자 토큰 유효성 검증 후, 우회 진입 차단을 위해 호출하는 조회 API예요.
guid와 game_id로 이용자 제재 상태를 확인할 수 있고, 제재 상태인 경우 제재 코드와 제재 기간 정보가 응답돼요. (제재 사유는 응답하지 않아요)
식별자 용어 정리 (TBD — 확정 후 별도 가이드 페이지로 이동 예정)
| memberNo | 스토브 계정의 고유 식별자. 한 이용자에게 하나만 부여돼요. 게임 무관 글로벌 식별자. |
| guid | 플랫폼이 게임별로 발급하는 이용자 식별자. 동일한 memberNo라도 게임마다 서로 다른 guid를 가져요.개념상 memberNo + game_id → guid의 매핑 관계. |
| game_id | 게임을 식별하는 키 (파트너스 등록 시 결정). 본 API의 path 파라미터로 사용. |
※ 본 정의는 현재 정리 중이며, 정식 식별자 가이드 페이지가 준비되면 이 박스는 해당 페이지 링크로 대체될 예정이에요.
API 엔드포인트
| 항목 | 내용 |
|---|---|
| URL | GET /mmember/v1.0/signin/game/status/{guid}/{game_id} |
| Host (Live) | https://api.onstove.com |
| Host (Sandbox) | https://api.gate8.com |
Request — Header
| Name | Type | Required | Description |
|---|---|---|---|
| Content-Type | application/json | Y | 리소스 media type. |
| Authorization | String | Y | 플랫폼 인증 토큰. Bearer {API AccessToken} 형식. |
| caller-id | String | Y | API 호출자 식별 헤더. 정해진 룰은 없으며, 게임 서버가 자유롭게 정의 후 STOVE에 사전 공유해 주세요. 모니터링 지표(호출량·장애·트레이싱) 식별자로 사용돼요. 예시(권고): {service_id}_SERVER, {service_id}_HOME※ service_id는 파트너스에 등록한 Game ID. 한 번 정한 값은 변경하지 않는 것을 권장. |
Request — Path Params
| Name | Type | Required | Description |
|---|---|---|---|
| guid | String | Y | Game User ID. |
| game_id | String | Y | 게임 식별자 (파트너스 등록 Game ID). |
Request — Query Params
| Name | Type | Required | Description |
|---|---|---|---|
| guid_yn | String | Y (guid 게임) | guid 게임인 경우 Y 필수 입력. |
Response — Body
| Name | Type | Required | Description |
|---|---|---|---|
| response_code | int | Y | 응답 코드 (성공: 0). |
| response_message | String | Y | 응답 메시지 (성공: Success). |
| value.RESTRICT | Object | N | 제재 정보. 제재 상태인 경우에만 포함. |
Response — 제재 정보 (value.RESTRICT)
| Name | Type | Example | Description |
|---|---|---|---|
| memberNo | Long | 1234 | 이용자의 고유 번호 (guid). |
| nickname | String | AUTONICK##9515338 | 캐릭터 닉네임. |
| banDay | int | 2 | 제재 남은 기간 (Day). |
| banCd | String | B026 | 제재 코드 (게임별 마스터 데이터에서 정의). |
| banStartDt | Long | 1649053320000 | 제재 시작일 (밀리초). |
| banEndDt | Long | 1649312520000 | 제재 종료일 (밀리초). |
Response — 응답 코드 (response_code)
모든 응답은 HTTP 200으로 내려오며, 비즈니스 결과는 response_code 값으로 분기해 처리해야 해요.
| 코드 | 상수명 | 의미 / 처리 가이드 |
|---|---|---|
| 0 | PALMPLE_OK | 정상 응답. value.RESTRICT가 있으면 제재 상태, 없으면 제재 없음. |
| 10003 | PALMPLE_ERR_NO_DATA | 제재 정보 없음. 정상 이용자로 처리. |
| 10122 | PALMPLE_ERR_BAN_MEMBER | 제재된 이용자. 인게임 Kick 및 STOVE Logout 처리 필요. |
| 90001 | AUTH_ACCESS_TOKEN_INVALID | 토큰 검증 실패. caller-id·Authorization 헤더 또는 path의 guid/game_id 일치 여부 확인 후 재요청. |
Sample — Request
curl --location 'https://api.gate8.com/mmember/v1.0/signin/game/status/12345/{game_id}?guid_yn=Y' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {API accessToken}' \
--header 'caller-id: {caller-id}'
Sample — Response (제재 없음)
{
"response_code": 0,
"response_message": "Success"
}
Sample — Response (제재 있음)
{
"response_code": 0,
"response_message": "Success",
"value": {
"RESTRICT": {
"memberNo": 1234,
"nickname": "AUTONICK##9515338",
"banDay": 2,
"banCd": "B026",
"banStartDt": 1649053320000,
"banEndDt": 1649312520000
}
}
}
트러블슈팅
| 상황 | 원인 | 권장 처리 |
|---|---|---|
| Callback 이벤트 미수신 | Callback URL 미등록 또는 잘못된 URL 등록 | STOVE 기술 담당자에게 등록 URL 재확인 요청 |
| Callback 이벤트 미수신 | ACL/방화벽에서 STOVE NAT IP 차단 | 환경별 NAT IP가 Inbound 허용 목록에 있는지 점검 |
| 동일 이벤트 3회 수신 | Callback API가 200 외 응답 (4xx/5xx/timeout) | 처리 로직 예외 발생 시에도 200 응답 보장 (idempotent 처리) |
조회 API response_code=90001 | caller-id·Authorization 헤더 누락/오타 | 헤더 3개(Content-Type, Authorization, caller-id) 모두 포함되는지 점검 |
조회 API response_code=90001 | path의 guid/game_id가 토큰 권한과 불일치 | API AccessToken 재발급 시 game_id 권한 확인 |
| 한 이용자 중복 Kick 처리 | 다중 Callback 인스턴스에서 동일 이벤트 처리 | event_time+memberNo 키로 dedup 처리 권장 |
| 제재 해제 후에도 Kick 유지 | 제재 해제는 별도 이벤트 미전송 (정상 동작) | 게임 진입 시점에 조회 API 재호출로 최신 상태 확인 |
샘플 코드
본 예시는 게임 서버가 직접 운영하는 영역의 참고 구현이에요. 실제 환경에 맞게 의존성·예외 처리·로깅을 보완해 사용해 주세요.
1) Callback API 수신 — Kick 이벤트 핸들러
@RestController
@RequestMapping("/stove")
public class StoveKickController {
private final UserKickService kickService;
public StoveKickController(UserKickService kickService) {
this.kickService = kickService;
}
/** STOVE Event Broker가 호출하는 Kick 이벤트 수신 엔드포인트 */
@PostMapping(value = "/kick", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> onKickEvent(@RequestBody KickEvent event) {
if (!"GAME_RESTRICT".equals(event.getEventReason())) {
// 알 수 없는 이벤트는 200으로 응답해 재전송 루프를 막아요.
return ResponseEntity.ok(Map.of("code", 0, "message", "IGNORED"));
}
kickService.kickAll(event.getTargetUsers(), event.getEventMessage(), event.getEventTime());
// 정상 수신 → 200 OK 반환 (200 외 응답 시 STOVE Event Broker가 최대 3회 재전송)
return ResponseEntity.ok(Map.of("code", 0, "message", "SUCCESS"));
}
@Getter @Setter
public static class KickEvent {
@JsonProperty("event_reason") private String eventReason;
@JsonProperty("event_message") private String eventMessage;
@JsonProperty("event_time") private Long eventTime;
@JsonProperty("target_users") private List<Long> targetUsers;
}
}
2) 이용자 제재정보 조회 — REST 호출
@Component
public class StoveBanInfoClient {
private static final String HOST_LIVE = "https://api.onstove.com";
private static final String HOST_SANDBOX = "https://api.gate8.com";
private final RestClient restClient;
@Value("${stove.api.access-token}") private String apiAccessToken;
@Value("${stove.caller-id}") private String callerId;
@Value("${stove.profile:live}") private String profile;
public StoveBanInfoClient(RestClient.Builder builder) {
this.restClient = builder.build();
}
/** 제재 상태 조회. 정상 사용자면 Optional.empty() 반환. */
public Optional<BanInfo> getBanInfo(String guid, String gameId) {
String host = "sandbox".equalsIgnoreCase(profile) ? HOST_SANDBOX : HOST_LIVE;
String url = host + "/mmember/v1.0/signin/game/status/" + guid + "/" + gameId + "?guid_yn=Y";
BanResponse res = restClient.get()
.uri(url)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiAccessToken)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header("caller-id", callerId)
.retrieve()
.body(BanResponse.class);
if (res == null) return Optional.empty();
switch (res.responseCode) {
case 0: // 정상
return res.value != null ? Optional.ofNullable(res.value.RESTRICT) : Optional.empty();
case 10003: // 제재 정보 없음
return Optional.empty();
case 10122: // 제재된 사용자
return res.value != null ? Optional.ofNullable(res.value.RESTRICT) : Optional.empty();
case 90001: // 토큰 검증 실패
throw new IllegalStateException("STOVE accessToken / caller-id / path 불일치 — 설정 점검");
default:
throw new IllegalStateException("Unexpected response_code: " + res.responseCode);
}
}
@Getter @Setter
public static class BanResponse {
@JsonProperty("response_code") public int responseCode;
@JsonProperty("response_message") public String responseMessage;
public ValueBlock value;
}
@Getter @Setter
public static class ValueBlock { public BanInfo RESTRICT; }
@Getter @Setter
public static class BanInfo {
public Long memberNo;
public String nickname;
public int banDay;
public String banCd;
public Long banStartDt;
public Long banEndDt;
}
}
3) 게임 진입 시점에서의 사용 흐름
public class GameEntryService {
private final StoveBanInfoClient banClient;
private final GameSessionService sessionService;
public void onPlayerEnter(String guid, String gameId) {
banClient.getBanInfo(guid, gameId).ifPresentOrElse(
ban -> sessionService.kick(ban.memberNo, ban.banCd, ban.banEndDt),
() -> sessionService.allow(guid)
);
}
}