Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자가 될래요

Riot API 사용해보기(3) 본문

프로젝트

Riot API 사용해보기(3)

Youcan 2024. 9. 17. 19:46

그럼 이제 앞선 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요청이 제한이 걸리게 되어 이 부분은 아직은 구현하지 못했다. 다른 방법을 찾아봐야겠다.


lighthouse 성능은 이렇게 나왔다. 추후에 더 개선해야겠다.