리더보드
리더보드는 플레이어 순위를 표시하는 기능입니다. Connect Base의 데이터베이스와 함수를 활용하여 구현합니다.
리더보드 데이터 구조
typescript
// JSON 데이터베이스 테이블 구조
interface LeaderboardEntry {
_id: string // 레코드 ID
userId: string // 플레이어 ID
nickname: string // 닉네임
score: number // 점수
rank?: number // 순위 (계산됨)
updatedAt: number // 마지막 업데이트
metadata?: {
wins: number
losses: number
playTime: number
}
}점수 업데이트
typescript
import ConnectBase from 'connectbase-client'
const cb = new ConnectBase({ appId: 'YOUR_APP_ID', publicKey: 'cb_pk_...' })
// 콘솔의 데이터베이스 → 테이블 만들기 후 발급된 ID 사용
const LEADERBOARD_TABLE = 'tbl_leaderboard'
// 점수 업데이트 (게임 종료 시)
async function updateScore(userId: string, newScore: number) {
// 기존 기록 조회 (queryData 로 where 조건)
const result = await cb.database.queryData(LEADERBOARD_TABLE, {
where: { userId },
limit: 1
})
const existing = result.data[0]
if (existing) {
// 점수 업데이트
await cb.database.updateData(LEADERBOARD_TABLE, existing.id, {
data: {
score: newScore,
updatedAt: Date.now()
}
})
} else {
// 새 기록 생성
await cb.database.createData(LEADERBOARD_TABLE, {
data: {
userId,
nickname: await getPlayerNickname(userId),
score: newScore,
updatedAt: Date.now(),
metadata: { wins: 0, losses: 0, playTime: 0 }
}
})
}
}리더보드 조회
typescript
// 상위 N명 조회
async function getTopPlayers(limit = 100) {
const result = await cb.database.queryData(LEADERBOARD_TABLE, {
orderBy: 'score',
orderDirection: 'desc',
limit
})
// 순위 추가
return result.data.map((row, index) => ({
id: row.id,
...(row.data as LeaderboardEntry),
rank: index + 1
}))
}
// 특정 플레이어 주변 순위 조회
async function getPlayerRanking(userId: string) {
// 먼저 전체 조회 후 위치 계산 (대규모에는 비효율적 — aggregate 사용 권장)
const result = await cb.database.queryData(LEADERBOARD_TABLE, {
orderBy: 'score',
orderDirection: 'desc',
limit: 1000
})
const playerIndex = result.data.findIndex(
(row) => (row.data as LeaderboardEntry).userId === userId
)
if (playerIndex === -1) return null
// 앞뒤 5명씩 포함
const start = Math.max(0, playerIndex - 5)
const end = Math.min(result.data.length, playerIndex + 6)
return result.data.slice(start, end).map((row, index) => ({
id: row.id,
...(row.data as LeaderboardEntry),
rank: start + index + 1
}))
}시즌 리더보드
typescript
// 시즌별로 별도 테이블을 만들고 해당 ID 를 매핑
const SEASON_TABLES: Record<number, string> = {
1: 'tbl_leaderboard_season_1',
2: 'tbl_leaderboard_season_2',
3: 'tbl_leaderboard_season_3'
}
function getSeasonTableId(season: number): string {
const id = SEASON_TABLES[season]
if (!id) throw new Error(`시즌 ${season} 테이블이 등록되지 않았습니다`)
return id
}
// 현재 시즌 계산
async function getCurrentSeason(): Promise<number> {
const now = new Date()
const startOfYear = new Date(now.getFullYear(), 0, 1)
const days = Math.floor((now.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000))
return Math.floor(days / 90) + 1 // 90일마다 새 시즌
}
// 시즌 리더보드 조회
async function getSeasonLeaderboard(season?: number) {
const currentSeason = season ?? (await getCurrentSeason())
const tableId = getSeasonTableId(currentSeason)
const result = await cb.database.queryData(tableId, {
orderBy: 'score',
orderDirection: 'desc',
limit: 100
})
return result.data
}실시간 리더보드 업데이트
WebSocket을 활용하여 실시간 순위 변동을 표시합니다:
typescript
// 1) 카테고리 구독
await cb.realtime.connect()
const sub = await cb.realtime.subscribe('leaderboard-updates')
// 2) 메시지 수신 핸들러
type LeaderboardUpdate = { entries: LeaderboardEntry[] }
sub.onMessage<LeaderboardUpdate>((msg) => {
updateLeaderboardUI(msg.data.entries)
})
// 3) 서버리스 함수에서 점수 변경 시 broadcast
// (실제 broadcast 는 백엔드에서 NATS publish 또는 socket-server REST 호출로 수행)
// 함수 코드 예시:
// export default async function handler(request, context) {
// const { userId, newScore } = request.body
// await updateScore(userId, newScore)
// const top10 = await getTopPlayers(10)
// await context.realtime.send('leaderboard-updates', { entries: top10 })
// return { success: true }
// }리더보드 UI 예시
typescript
function LeaderboardComponent() {
const [entries, setEntries] = useState<LeaderboardEntry[]>([])
const [myRank, setMyRank] = useState<number | null>(null)
useEffect(() => {
loadLeaderboard()
subscribeToUpdates()
}, [])
async function loadLeaderboard() {
const top100 = await getTopPlayers(100)
setEntries(top100)
// 내 순위 찾기
const myEntry = top100.find(e => e.userId === myUserId)
setMyRank(myEntry?.rank ?? null)
}
return (
<div className="leaderboard">
<h2>리더보드</h2>
{myRank && <div className="my-rank">내 순위: {myRank}위</div>}
<ul>
{entries.map(entry => (
<li key={entry.userId}>
<span className="rank">#{entry.rank}</span>
<span className="nickname">{entry.nickname}</span>
<span className="score">{entry.score.toLocaleString()}</span>
</li>
))}
</ul>
</div>
)
}