개발자가 될래요
Riot API 사용해보기(3) 본문
그럼 이제 앞선 API들을 연계하여 게임 전적을 가져오도록 하자.
1 ) /riot/account/v1/accounts/by-riot-id/{gameName}/{tagLine} API에 닉네임과 태그를 전송 -> puuid를 반환
2) puuid를 /lol/summoner/v4/summoners/by-puuid/{encryptedPUUID} API에 전송 -> accountId, profileIconId, id, summonerLevel을 반환
3) /lol/match/v5/matches/by-puuid/{puuid}/ids 에 puuid를 넣고 게임전적 Id를 반환 받는다. (KR_7284824953, ...)
4) 게임전적 id를 /lol/match/v5/matches/{matchId} 에 넣어주고 해당 게임에 대한 기록을 반환 받는다.
위의 4개의 과정을 닉네임을 검색했을 때 순차적으로 진행하도록 한다.
// src/app/riot/accounts/page.tsx
const getUuid = async (nickname: string, tagLine: string) => {
const encodedName = encodeURIComponent(nickname);
try {
const response = await fetch(
`/api/riot/uuid?encodedName=${encodedName}&tagLine=${tagLine}`
);
const playerData = await response.json();
setPlayerData(playerData);
if (playerData) {
setPuuid(playerData.puuid);
const responsePlayer = await fetch(
`/api/riot/summoner?encryptedPUUID=${playerData.puuid}`,
{
method: "GET",
}
);
const data = await responsePlayer.json();
setSummonerData(data);
const responseMatches = await fetch(
`/api/riot/matches?puuid=${encodeURIComponent(playerData.puuid)}`,
{
method: "GET",
}
);
setMatches(await responseMatches.json());
}
} catch (error) {
console.error("API 에러:", error);
}
};
그리고 동적 라우팅을 이용하여 검색된 닉네임이 주소창에 적용되도록 수정하였다.
// src/app/riot/accounts/[nickname]/page.tsx
"use client";
import GameListComponent from "@/components/riot/gameList";
import SummonerComponent from "@/components/riot/summoner";
import React, { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
export default function RiotAccountPage() {
const [puuid, setPuuid] = useState<string>("");
const [summonerData, setSummonerData] = useState<any>(null); // 소환사정보
const [playerData, setPlayerData] = useState<any>(null); // 계정정보
const [matches, setMatches] = useState<string[]>([]); // 매치 ID 리스트 상태
const [userNickname, setUserNickname] = useState<string>(""); // 입력된 닉네임
const [tagLine, setTagLine] = useState<string>("kr1"); // 기본 태그라인
const params = useParams(); // URL에서 닉네임 가져오기
const router = useRouter();
const handleNicknameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
const [nickname, tag] = inputValue.split("#"); // #을 기준으로 분리
setUserNickname(nickname || ""); // 닉네임 설정
setTagLine(tag || "kr1"); // 태그라인 설정
};
const handleSearch = () => {
if (userNickname) {
router.push(
`/riot/accounts/${encodeURIComponent(userNickname + "-" + tagLine)}`
);
}
};
const getUuid = async (nickname: string, tagLine: string) => {
const encodedName = encodeURIComponent(nickname);
try {
const response = await fetch(
`/api/riot/uuid?encodedName=${encodedName}&tagLine=${tagLine}`
);
const playerData = await response.json();
setPlayerData(playerData);
if (playerData) {
setPuuid(playerData.puuid);
const responsePlayer = await fetch(
`/api/riot/summoner?encryptedPUUID=${playerData.puuid}`,
{
method: "GET",
}
);
const data = await responsePlayer.json();
setSummonerData(data);
const responseMatches = await fetch(
`/api/riot/matches?puuid=${encodeURIComponent(playerData.puuid)}`,
{
method: "GET",
}
);
setMatches(await responseMatches.json());
}
} catch (error) {
console.error("API 에러:", error);
}
};
// 페이지 로드 시 닉네임을 이용해 데이터 가져오기
useEffect(() => {
const nicknameParam = params.nickname;
if (typeof nicknameParam === "string") {
const [nickname, tagLine] = nicknameParam.split("-");
if (nickname) {
getUuid(nickname, tagLine || "kr1"); // URL에서 닉네임과 태그라인 가져와서 검색
}
}
}, [params.nickname]); // 닉네임이 변경될 때마다 호출
return (
<div className="flex flex-col items-center justify-center min-h-screen p-6 bg-gray-100 min-w-[496px]">
<h1 className="text-4xl font-bold mb-6 text-indigo-700">
Riot Account 조회
</h1>
{/* 검색을 위한 입력 필드와 버튼 */}
<div className="w-full max-w-2xl bg-white p-6 rounded-lg shadow-lg space-y-4">
<input
type="text"
onChange={handleNicknameInput}
placeholder="Nickname#TagLine 입력"
className="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button
onClick={handleSearch}
className="w-full py-3 bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors duration-200"
>
소환사 정보 가져오기
</button>
</div>
{/* 소환사 정보 및 게임 리스트 표시 */}
<div className="w-full max-w-2xl bg-white p-6 rounded-lg shadow-lg space-y-4">
{summonerData && (
<div className="mt-6 p-4 bg-gray-50 rounded shadow-inner">
<SummonerComponent
summonerData={summonerData}
playerData={playerData}
/>
</div>
)}
<GameListComponent puuid={puuid} matches={matches} />
</div>
</div>
);
}
이 코드는 src/app/riot/accounts/page.tsx와 동일한 코드이다.
해당 아이디의 랭크 기록 가져오기
닉네임을 검색하면 해당 아이디의 솔로랭크 기록과 자유랭크 기록을 보여주려고 한다.
// src/components/riot/summoner.tsx
import Image from "next/image";
import { useEffect, useState } from "react";
import TierComponent from "./TierComponent";
type SummonerData = {
id: string;
accountId: string;
puuid: string;
profileIconId: number;
revisionDate: number;
summonerLevel: number;
};
type PlayerData = {
gameName: string;
puuid: string;
tagLine: string;
};
export type LeagueEntry = {
leagueId: string;
summonerId: string;
queueType: string;
tier: string;
rank: string;
leaguePoints: number;
wins: number;
losses: number;
hotStreak: boolean;
veteran: boolean;
freshBlood: boolean;
inactive: boolean;
};
export default function SummonerComponent({
summonerData,
playerData,
}: {
summonerData: SummonerData;
playerData: PlayerData;
}) {
const { id, profileIconId, summonerLevel } = summonerData;
const { gameName, tagLine } = playerData;
const [leagueSoloInfo, setLeagueSoloInfo] = useState<LeagueEntry | null>(
null
);
const [leagueFlexInfo, setLeagueFlexInfo] = useState<LeagueEntry | null>(
null
);
const iconUrl = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/profile-icons/${profileIconId}.jpg`;
// 티어 정보를 불러오는 함수
const fetchLeagueInfo = async (summonerId: string) => {
try {
const response = await fetch(
`/api/riot/league/tier?encryptedSummonerId=${summonerId}`
);
const data: LeagueEntry[] = await response.json();
// 각 데이터에 대해 queueType에 맞는 정보를 설정
data.forEach((entry) => {
if (entry.queueType === "RANKED_SOLO_5x5") {
setLeagueSoloInfo(entry); // 솔로 랭크 정보를 저장
} else if (entry.queueType === "RANKED_FLEX_SR") {
setLeagueFlexInfo(entry); // 다인 랭크 정보를 저장
}
});
} catch (error) {
console.error("리그 정보 불러오기 실패:", error);
}
};
// 컴포넌트 렌더링 시 리그 정보 불러오기
useEffect(() => {
if (id) {
fetchLeagueInfo(id);
}
}, [id]);
// 티어에 따라 이미지 반환 함수
return (
<>
<div className="flex items-center gap-4 justify-center min-w-full">
<div>
<Image
src={iconUrl}
alt="Summoner Profile Icon"
width={64}
height={64}
className="rounded-2xl"
/>
<span className="flex justify-center font-bold">
Lv.{summonerLevel}
</span>
</div>
<div className="flex">
<p className="text-xl font-bold">{gameName}</p>
<p className="text-lg">#{tagLine}</p>
</div>
</div>
<div className="flex justify-center gap-8 ">
<div className="flex-col flex border-2 mt-4 p-4 bg-indigo-100 rounded-lg min-w-60 max-w-60 overflow-x-auto">
<h2 className="flex justify-center ">개인 / 2인 랭크 게임</h2>
{leagueSoloInfo ? (
<TierComponent type="Solo" LeagueEntry={leagueSoloInfo} />
) : (
<h2 className="flex justify-center my-auto text-2xl font-bold">
Unranked
</h2>
)}
</div>
<div className="flex-col flex border-2 mt-4 p-4 bg-indigo-100 rounded-lg min-w-60 max-w-60 overflow-x-auto">
<h2 className="flex justify-center min-w-48"> 자유 랭크 게임</h2>
{leagueFlexInfo ? (
<TierComponent type="Flex" LeagueEntry={leagueFlexInfo} />
) : (
<h2 className="flex justify-center my-auto text-2xl font-bold">
Unranked
</h2>
)}
</div>
</div>
</>
);
}
유저가 설정해둔 프로필 아이콘 이미지는 너무 다양하기 때문에 데이터가 잘 정리되어 있는 사이트에서 가져오도록 했다. 티어 별 이미지는 public에 저장하고 불러왔다.
const iconUrl = `https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/profile-icons/${profileIconId}.jpg`;


주소창에 직접 입력하거나 새로고침을 해도 랭크 기록이 잘 나온다.
게임 전적
게임 전적 20개를 배열로 받아오고, 이 1개의 게임ID로 API요청을 보냈다.
그리고 반환받은 게임 기록에서 내가 검색한 닉네임과 동일한 puuid를 가진 데이터만 이용한다.(해당 데이터는 반환받은 데이터의 info.participants에 담겨있다.)
해당 데이터에서 승패 여부, Kills, Deaths, Assists 와 item0 ~ item6 그리고 사용한 ChampionId를 가져왔다.
챔피언 및 아이템 일러스트의 경우, Riot Docs에서 일러스트를 다운받아 pubilc에 넣어주었다.
그리고 챔피언과 아이템에 관한 json 파일 또한 저장해두고, 챔피언 및 item의 값과 동일한 파일을 이미지로 불러왔다.
(ChampionId, Item0~6은 모두 숫자로 넘어온다)
// src/components/riot/Match.tsx
"use client";
import { useEffect, useState } from "react";
import { getRelativeTime } from "@/constant/Date";
import PlayerListComponent from "./PlayerList";
import Image from "next/image";
import spells from "@/constant/spells.json";
import champions from "@/constant/champions.json";
import ItemListComponent from "./ItemList";
export default function MatchComponent({
matchId,
puuid,
}: {
matchId: string;
puuid: string;
}) {
const [gameData, setGameData] = useState<any>(null);
const players = gameData && gameData.metadata.participants;
useEffect(() => {
const getMatchInfo = async (matchId: string) => {
const response = await fetch(`/api/riot/match?matchId=${matchId}`, {
method: "GET",
});
if (!response.ok) {
console.error("Error fetching match details:", await response.json());
return;
}
const matchDetails = await response.json();
setGameData(matchDetails);
};
if (matchId) {
getMatchInfo(matchId);
}
console;
}, [matchId]);
console.log(gameData);
const gameType =
gameData && gameData.info.gameMode != "CLASSIC"
? "무작위 총력전"
: "소환사의 협곡";
const playTime =
gameData &&
Math.floor(gameData.info.gameDuration / 60) +
"분" +
" " +
(gameData.info.gameDuration % 60) +
"초";
const playDate =
gameData && getRelativeTime(new Date(gameData.info.gameCreation).getTime());
const winCheck = (puuid: string) => {
if (gameData) {
for (let i = 0; i < gameData.info.participants.length; i++) {
if (puuid === gameData.info.participants[i].puuid) {
return gameData.info.participants[i].win;
}
}
}
};
const bgColor = winCheck(puuid) ? "bg-blue-100" : "bg-red-100";
const isWin = winCheck(puuid) ? (
<p className="text-blue-600 font-bold pt-2"> 승리</p>
) : (
<p className="text-red-600 font-bold pt-2"> 패배 </p>
);
const myChampionId = (puuid: string) => {
if (gameData) {
for (let i = 0; i < gameData.info.participants.length; i++) {
if (puuid === gameData.info.participants[i].puuid) {
return gameData.info.participants[i].championId;
}
}
}
};
const spellCheck = (puuid: string) => {
if (gameData) {
for (let i = 0; i < gameData.info.participants.length; i++) {
if (puuid === gameData.info.participants[i].puuid) {
const spells = [
gameData.info.participants[i].summoner1Id,
gameData.info.participants[i].summoner2Id,
];
return spells;
}
}
}
};
const kdaScore = (puuid: string) => {
if (gameData) {
for (let i = 0; i < gameData.info.participants.length; i++) {
if (puuid === gameData.info.participants[i].puuid) {
const kills = gameData.info.participants[i].kills;
const deaths = gameData.info.participants[i].deaths;
const assists = gameData.info.participants[i].assists;
const KDA = [kills, deaths, assists];
return KDA;
}
}
}
};
const championId = myChampionId(puuid); // 숫자 형태의 championId
const championName = getChampionIdByKey(championId); // 챔피언 이름 (Aatrox 등)
const champImg = `/champions/tiles/${championName}_0.jpg`;
return (
<>
<div className={`flex min-h-20 ${bgColor} rounded-l-lg`}>
<div
className={`min-w-2 ${
winCheck(puuid) ? "bg-blue-500" : "bg-red-500"
} rounded-l-full`}
/>
<div className="flex flex-col mx-4 text-sm justify-center ">
{isWin}
<p className="pt-1">{gameType}</p>
<p className="pb-1">{gameData && playDate.toString()}</p>
<hr />
<p className="py-2">{playTime}</p>
</div>
<div className="flex min-w-[378px] mx-2 my-5">
<div>
{champImg && (
<Image
src={champImg}
alt="thumb"
width={48}
height={48}
className="rounded-full"
/>
)}
</div>
<div className="flex">
{gameData && (
<div className="flex flex-col space-y-1 mx-2">
<Image
src={`${getSpellIcon(spellCheck(puuid)![0])}`}
alt="Summoner 1"
width={24}
height={24}
className="rounded-md"
/>
<Image
src={`${getSpellIcon(spellCheck(puuid)![1])}`}
alt="Summoner 2"
width={24}
height={24}
className="rounded-md"
/>
</div>
)}
<div className="text-lg font-bold pt-1 flex flex-col justify-center items-center pb-4 px-2">
<p>{kdaScore(puuid)?.join("/")}</p>
<p className="text-sm">
평점
{gameData &&
(
(kdaScore(puuid)![0] + kdaScore(puuid)![2]) /
kdaScore(puuid)![1]
).toFixed(2)}
: 1
</p>
<div className="flex">
{gameData && (
<ItemListComponent gameData={gameData} puuid={puuid} />
)}
</div>
</div>
</div>
<div className="">
<PlayerListComponent players={players} gameData={gameData} />
</div>
</div>
</div>
</>
);
}
const getSpellIcon = (spellId: number) => {
const spell = spells.find((s) => s.id === spellId);
return spell!.iconPath;
};
// myChampionId 함수를 통해 가져온 championId에 해당하는 챔피언 id를 얻는 함수
const getChampionIdByKey = (championId: number) => {
const championData = Object.values(champions.data).find(
(champion: any) => parseInt(champion.key) === championId
);
return championData ? championData.id : null;
};

승패여부, 게임모드, 게임을 한 날짜, 게임 시간, 사용한 챔프와 주문, 아이템목록들을 불러왔다.
PlayerListComponent의 경우, op.gg처럼 해당 게임을 플레이한 유저의 목록을 남기려고 했다.
다만, Participants에는 puuid가 넘어오고 이 puuid로 다른 유저들의 닉네임을 요청해야하다보니
한판당 10명 * 20게임 = 200번의 요청을 닉네임을 검색할 때마다 보내게 되어 API요청이 제한이 걸리게 되어 이 부분은 아직은 구현하지 못했다. 다른 방법을 찾아봐야겠다.

'프로젝트' 카테고리의 다른 글
| [Agent] 코드 리뷰 및 분석 agent 만들기(2) (0) | 2026.05.25 |
|---|---|
| [Agent] 코드 리뷰 및 분석 agent 만들기(1) (0) | 2026.05.24 |
| Riot API 사용해보기(2) (3) | 2024.09.11 |
| Riot API 사용해보기(1) (0) | 2024.09.01 |
| 자체 로그인에 프로필 이미지 추가 (0) | 2024.09.01 |