베이스 주소는 https://www.1506games.com 입니다. 요청과 응답은 모두 JSON이고, 실패해도 { "success": false, "message": "..." } 형태는 같습니다.
인증 방식
로그인에 성공하면 astro-session 쿠키가 내려옵니다. 이후 요청에 그 쿠키를 그대로 실어 보내면 됩니다. HttpOnly; Secure; SameSite=Lax라서 HTTPS로만 오갑니다 — 게임 클라이언트도 https 주소로 붙어야 합니다.
클라이언트에서는 쿠키 저장소를 켜 두면 됩니다. Unity UnityWebRequest 는 기본적으로 쿠키를 유지하고, C#의 HttpClient 는HttpClientHandler { UseCookies = true } 가 필요합니다.
POST 는 반드시 JSON으로
Content-Type: application/json 을 빼먹으면 403 이 돌아옵니다. Astro의 CSRF 검사가 content-type 없는 비-GET 요청의 Origin을 따지는데, 이 서버는 프록시 뒤에 있어 그 검사를 통과하지 못합니다. 본문이 없는 요청도{} 를 실어 보내세요.
리더보드 앱 키
/api/leaderboard/* 는 계정 세션이 아니라 앱 키로 인증합니다. 키는 /admin/leaderboard 에서 앱마다 발급합니다. 모든 요청에 x-lb-key 헤더(또는 ?key=)를 실어 보내세요.
키만으로는 "내 앱만" 이 되지 않습니다 — 게임에 박힌 키는 언젠가 뜯깁니다. 그래서 막는 방법이 두 가지 있고, 앱 성격에 따라 고릅니다.
- 네이티브·서버 앱은 비밀키를 숨길 수 있으므로
서명 필요를 켜 둡니다. 점수 등록 요청마다 HMAC 서명을 붙여야 하고, 서명이 없거나 5분 이상 묵은 요청, 이미 쓴 nonce 는 401 입니다. - 브라우저 게임은 무엇도 숨길 수 없으므로 서명을 끄고
허용 Origin을 채웁니다. 브라우저가 붙이는 Origin 헤더는 스크립트가 바꿀 수 없어서, 남의 사이트에 내 키를 복사해 붙이는 경우를 막아 줍니다. 브라우저 밖에서 위조하는 것까지 막지는 못합니다 — 조작이 곤란해야 하는 순위표라면 서명 쪽을 쓰세요.
서명 만드는 법
헤더 네 개를 붙입니다. 서명 대상 문자열은 줄바꿈으로 이어 붙인 다섯 조각입니다.
x-lb-key: <API 키>
x-lb-timestamp: <unix 초>
x-lb-nonce: <요청마다 다른 8~128자 임의 문자열>
x-lb-signature: hex(HMAC-SHA256(비밀키, canonical))
canonical = timestamp + "\n"
+ nonce + "\n"
+ "POST" + "\n"
+ "/api/leaderboard/score" + "\n"
+ sha256hex(본문 바이트 그대로)
Node 로 쓰면 이렇습니다.
const body = JSON.stringify({ game: 'flappy-bird', name: 'player1', score: 1234 });
const ts = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomUUID();
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const canonical = [ts, nonce, 'POST', '/api/leaderboard/score', bodyHash].join('\n');
const signature = crypto.createHmac('sha256', SECRET).update(canonical).digest('hex');
await fetch('https://www.1506games.com/api/leaderboard/score', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-lb-key': API_KEY,
'x-lb-timestamp': ts,
'x-lb-nonce': nonce,
'x-lb-signature': signature,
},
body,
});
본문은 서명할 때 쓴 문자열을 그대로 보내야 합니다. 직렬화를 두 번 하면 키 순서나 공백이 달라져 해시가 어긋납니다.
가입 흐름
POST /api/auth/register — 코드 메일 발송POST /api/auth/verify — 코드 확인, 계정 활성화POST /api/auth/login — 세션 쿠키 수령
계정
게임 클라이언트가 쓰는 기본 흐름.
POST/api/auth/register없음
가입을 시작하고 6자리 인증 코드를 메일로 보낸다. 이 시점의 계정은 미인증 상태다.
요청
{
"email": "player@example.com",
"username": "player1",
"password": "at-least-8-chars",
"passwordConfirm": "at-least-8-chars",
"marketingConsent": false
}
응답
{ "success": true, "message": "Check your email for a 6-digit verification code." }
- marketingConsent 는 선택. 없으면 false 로 본다.
- 이미 있는 미인증 계정에 같은 이메일로 다시 부르면 코드가 갱신된다 — 재발송이 이 호출이다.
- 이미 인증을 마친 이메일/아이디면 409.
POST/api/auth/verify없음
메일로 받은 코드로 계정을 활성화한다. 코드 유효시간은 10분.
요청
{ "email": "player@example.com", "code": "123456" }
응답
{ "success": true, "message": "Email verified — your account is ready." }
POST/api/auth/login없음
로그인. 성공하면 astro-session 쿠키를 내려준다.
요청
{ "loginId": "player1 또는 player@example.com", "password": "..." }
응답
{
"success": true,
"user": { "id": "...", "username": "player1", "email": "player@example.com" }
}
- loginId 는 아이디와 이메일을 모두 받는다.
- 미인증 계정은 403.
GET/api/auth/me세션 쿠키
현재 로그인한 계정. 로그인 상태가 아니면 user 가 null 이고 200 이다 (401 이 아니다).
응답
{
"success": true,
"user": {
"id": "...", "username": "player1", "email": "player@example.com",
"createdAt": "2026-09-11T04:53:31.029Z",
"role": "user", "marketingConsent": false
}
}
POST/api/auth/logout세션 쿠키
세션을 파기한다.
요청
{}
응답
{ "success": true, "message": "Logged out." }
POST/api/auth/marketing-consent세션 쿠키
마케팅 수신 동의를 켜고 끈다. 변경 시점이 함께 기록된다.
요청
{ "marketingConsent": true }
응답
{ "success": true, "marketingConsent": true, "message": "..." }
POST/api/auth/delete-account세션 쿠키
회원 탈퇴. 비밀번호를 다시 확인하고 계정을 지운다.
요청
{ "password": "..." }
응답
{ "success": true, "message": "Your account has been deleted." }
리더보드
일간·주간·월간 순위표. 계정이 아니라 앱 키로 인증한다.
POST/api/leaderboard/score앱 키 (+서명)
점수 등록. 한 번 부르면 일간·주간·월간이 함께 갱신되고, 각 기간의 최고 기록만 남는다.
요청
{
"game": "flappy-bird",
"name": "player1",
"score": 1234,
"playerKey": "device-or-account-id",
"data": "stage3"
}
응답
{
"success": true, "app": "1506arcade", "game": "flappy-bird",
"name": "player1", "score": 1234, "updated": true,
"periods": {
"daily": { "periodKey": "2026-09-11", "updated": true, "rank": 3, "best": 1234 },
"weekly": { "periodKey": "2026-09-07", "updated": true, "rank": 5, "best": 1234 },
"monthly": { "periodKey": "2026-09", "updated": false, "rank": 9, "best": 2000 }
}
}
- playerKey 는 같은 사람의 기록을 한 줄로 묶는 열쇠다. 생략하면 이름을 그 자리에 쓰므로, 같은 이름을 쓰는 다른 사람이 서로의 기록을 덮는다.
- score 는 정수만 받는다. 소수는 400.
- updated 가 false 면 그 기간의 기존 기록이 더 좋다는 뜻이다 — 오류가 아니다.
- data 는 서버가 해석하지 않고 200자까지 그대로 보관한다.
GET/api/leaderboard/top?app=&game=&period=&limit=앱 키 (공개 조회면 없음)
지금 기간의 순위표. period 는 daily·weekly·monthly, limit 은 최대 200(기본 100).
응답
{
"success": true, "app": "1506arcade", "game": "flappy-bird",
"period": "daily", "periodKey": "2026-09-11", "order": "desc",
"periodStart": "2026-09-10T15:00:00.000Z",
"periodEnd": "2026-09-11T15:00:00.000Z",
"count": 2,
"entries": [
{ "rank": 1, "name": "player1", "score": 2000, "data": "", "at": "..." },
{ "rank": 2, "name": "player2", "score": 1234, "data": "", "at": "..." }
]
}
- 앞 기간의 기록은 나오지 않는다. 자정(주는 월요일 0시, 달은 1일 0시)에 순위표가 비워진다.
- periodEnd 가 다음 초기화 시각이다 — "남은 시간" 표시에 쓰면 된다.
- 동점은 같은 등수로 묶인다 (1, 2, 2, 4).
GET/api/leaderboard/rank?app=&game=&playerKey=&period=앱 키 (공개 조회면 없음)
한 사람의 등수. period 를 생략하면 세 기간을 모두 돌려준다.
응답
{
"success": true, "app": "1506arcade", "game": "flappy-bird",
"playerKey": "device-1", "order": "desc",
"periods": {
"daily": { "periodKey": "2026-09-11", "rank": 3, "total": 40, "score": 1234, "at": "..." },
"weekly": { "...": "..." },
"monthly": null
}
}
- 그 기간에 기록이 없으면 null 이다. 404 가 아니다.
GET/api/leaderboard/games?app=앱 키 (공개 조회면 없음)
이 앱이 들고 있는 순위표 목록. 화면이 게임 목록을 코드에 박지 않아도 되게 한다.
응답
{ "success": true, "app": "1506arcade", "open": false,
"games": [ { "id": "flappy-bird", "title": "Flappy Bird", "order": "desc" } ] }
관리자
role 이 admin 인 계정만. 아니면 404 를 돌려준다.
GET/api/admin/users?q=&page=관리자
가입자 목록. q 로 이메일·아이디 부분검색, 한 쪽에 50명.
응답
{ "success": true, "page": 1, "pageSize": 50, "total": 1, "users": [ ... ] }
POST/api/admin/users관리자
권한 변경과 계정 삭제. 자기 자신에게는 쓸 수 없다.
요청
{ "id": "<userId>", "action": "promote | demote | delete" }
GET/api/admin/mail관리자
단체 메일 수신자 수 미리보기.
응답
{ "success": true, "recipients": 0, "consented": 0, "verified": 1, "maxPerSend": 400 }
POST/api/admin/mail관리자
단체 메일 발송. test 가 false 일 때만 실제로 전체에게 나간다.
요청
{ "subject": "...", "body": "<p>HTML</p>", "test": true }
- test 를 생략하면 테스트 발송이다 — 실수로 전체에 나가지 않도록 기본값을 그렇게 뒀다.
- 제목에 (광고) 표시와 본문의 발신자 정보·수신거부 링크는 서버가 붙인다.