본문 바로가기
REACT/롤 전적 사이트

[4] 스타일 꾸미기

by 쪼꼬에몽 2025. 8. 19.

api를 받아왔으니 스타일을 웹앱 형식으로 꾸미고자 한다.

emtion theme을 이용하여 일관성있게 스타일을 적용했다.

 

global.tsx와 Layout.tsx를 이용해 공통 컴포넌트 스타일을 적용했다.

*,
*::before,
*::after {
  box-sizing: border-box;
}

Layout.tsx의 padding과 margin이 전체 사이즈에 계속 포함되어 결과가 없는 빈 화면에서도 커서가 생성되었다.

padding과 margin을 포함하기 위해 box-sizing을 border-box로 설정하였다.

global.tsx에 설정했다.

 

또한, search Input에 소환사명을 적고 엔터를 눌렀을 때 검색 버튼이 누르도록 설정하고자 했다.

import { SearchButton, SearchNameInput, SearchWrapper } from '@/pages/user/styles/search.styles.ts';

type SearchProps = {
  setUserName: (value: string) => void;
  onClickHandle: () => void;
};

export default function Search({ setUserName, onClickHandle }: SearchProps) {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      onClickHandle();
    }
  };

  return (
    <SearchWrapper>
      <SearchNameInput
        type="text"
        placeholder="소환사 이름을 입력하세요"
        onChange={(e) => setUserName(e.target.value)}
        onKeyDown={handleKeyDown}
      />
      <SearchButton onClick={onClickHandle}>검색하기</SearchButton>
    </SearchWrapper>
  );
}

input에 onKeyDown 이벤트를 설정한다.

e는 키보드 이벤트를 나타낸다. 만약 e.key가 엔터이면 버튼 이벤트를 활성화시킨다.

 

매치 상세 정보 스타일을 적용해야 한다.

그 전에 매치 정보를 정리 해야 한다.

export const MATCH_INFO = (matchId: string) =>
  `https://asia.api.riotgames.com/lol/match/v5/matches/${matchId}?api_key=${LOLAPI}`;

여기서 불러온 데이터가 매우 많기 때문에 관리해야한다.

import { formatDistanceToNow } from 'date-fns';
import { ko } from 'date-fns/locale';
import { formatGameDuration } from '@/hooks/fetch/useTimeModeChange.ts';
import { QUEUE_TYPE_MAP } from '@/constant/map.ts';

export default function useMatchDetail({ matchInfo, puuidData }) {
  // 검색 유저 정보
  const me = matchInfo.info.participants.find((p) => p.puuid === puuidData.puuid);
  console.log("me", me);

  // 아군 팀
  const allyTeam = matchInfo.info.participants.filter(p => p.teamId === me.teamId);
  console.log("allyTeam", allyTeam);

  // 적 팀
  const enemyTeam = matchInfo.info.participants.filter(p => p.teamId !== me.teamId);
  console.log('enenmyTeam', enemyTeam);

  // 승패
  const gameResult = me.win ? '승리' : '패배';
  console.log(gameResult);

  // 게임 종류
  const gameType = QUEUE_TYPE_MAP[matchInfo.info.queueId] || '기타';
  console.log(gameType);

  // 게임 시간
  const gameDuration = formatGameDuration(matchInfo.info.gameDuration);
  console.log(gameDuration);

  // 현재로부터 며칠 전 게임인지
  const timeAgo = formatDistanceToNow(new Date(matchInfo.info.gameEndTimestamp), {
    addSuffix: true,
    locale: ko,
  });
  console.log(timeAgo);

  // kda
  const kda = me.challenges.kda.toFixed(2);
  console.log(kda);

  // 킬 관여율
  const killParticipation = Math.round(me.challenges.killParticipation * 100);
  console.log(killParticipation);

  // cs
  const totalCS = me.totalMinionsKilled + me.neutralMinionsKilled;
  console.log(totalCS);

  // item slot
  const items = [me.item0, me.item1, me.item2, me.item3, me.item4, me.item5, me.item6].filter(
    (id) => id !== 0
  );
  console.log(items);

  return { me, allyTeam, enemyTeam, gameResult, gameDuration, timeAgo, kda, totalCS, items, killParticipation, gameType };
}

 

우선 기본적인 데이터를 가져왔다.

데이터가 많기 때문에 ai를 통해 기본적인 데이터가 무엇인지 추려내었다.

다음 데이터에서 나온 결과들이다.

이제 스펠과 룬에 대한 정보를 가져와야 한다.

 

스펠

ddragon.leagueoflegends.com/cdn/15.15.1/data/ko_KR/summoner.json

 

ddragon.leagueoflegends.com/cdn/15.15.1/data/ko_KR/runesReforged.json

 

스펠에 대한 정보는 다음과 같이 불러온다.

import useFetch from '@/hooks/fetch/useFetch.ts';
import { SPELL_INFO } from '@/api/url.ts';

export default function useSpellInfo() {
  const { data, isLoading, isError } = useFetch({
    key: 'spellInfo',
    value: '',
    url: SPELL_INFO,
    options: {
      enabled: true,
    },
  });

  const findSpellImage = (id: number) => {
    if (!data || !data.data) return undefined;
    return Object.values(data.data).find((spell) => Number(spell.key) === id).image.full;
  };

  return {
    spellData: data,
    spellDataIsLoading: isLoading,
    spellDataIsError: isError,
    findSpellImage,
  };
}

스펠 키와 id가 같은 것을 찾아서 해당 객체에 있는 image.full을 가져오는 형식으로 한다.

스펠은 할만했다.

 

하지만 룬 정보는 주요 룬 배열 안에 또 세부 룬 배열이 있어서 가져오기가 복잡했다.

import useFetch from '@/hooks/fetch/useFetch.ts';
import { RUNES_INFO } from '@/api/url.ts';

export default function useRuneInfo() {
  const { data, isLoading, isError } = useFetch({
    key: 'runeInfo',
    value: '',
    url: RUNES_INFO,
    options: {
      enabled: true,
    },
  });

  const runeArray = data?.data;

  const findRuneImage = (id: number) => {
    if (!runeArray) return null;
    for (const tree of runeArray) {
      for (const slot of tree.slots) {
        const rune = slot.runes.find((r) => r.id === id);
        if (rune) return rune.icon;
      }
    }
    return null;
  };

  const findSecondaryRuneImage = (id: number) => {
    if (!runeArray) return null;
    return runeArray.find((item) => item.id === id)?.icon || null;
  };

  return {
    runeData: data,
    runeDataIsLoading: isLoading,
    runeDataIsError: isError,
    findRuneImage,
    findSecondaryRuneImage,
  };
}

 

일단 한다고 했는데 데이터가 null이 떴다. api에서는 룬 id가 제대로 가져왔으니 데이터를 찾는 과정에서 문제가 발생한 것으로 예상된다.

 

생각해보니 rune 데이터에는 .data가 없는데 .data 처리를 해주었다.

import useFetch from '@/hooks/fetch/useFetch.ts';
import { RUNES_INFO } from '@/api/url.ts';

export default function useRuneInfo() {
  const { data, isLoading, isError } = useFetch({
    key: 'runeInfo',
    value: '',
    url: RUNES_INFO,
    options: {
      enabled: true,
    },
  });

  const findRuneImage = (id: number) => {
    if (!data) return null;
    for (const tree of data) {
      for (const slot of tree.slots) {
        const rune = slot.runes.find((r) => r.id === id);
        if (rune) return rune.icon;
      }
    }
    return null;
  };

  const findSecondaryRuneImage = (id: number) => {
    if (!data) return null;
    return data.find((item) => item.id === id)?.icon || null;
  };

  return {
    runeData: data,
    runeDataIsLoading: isLoading,
    runeDataIsError: isError,
    findRuneImage,
    findSecondaryRuneImage,
  };
}

 

그냥 하니 데이터를 잘 불러왔다.

 

빈 배열을 가져와서 빈 슬롯을 넣으려고 한다.

{Array.from({ length: 6 }).map((_, index) => {
  const itemId = items[index];
  return (
    <ItemBox key={index}>
      {itemId && <img src={ITEM_IMAGE_PNG(itemId)} alt={`item-${itemId}`} />}
    </ItemBox>
  );
})}

 

1. Array.from({ length: 6 })

- 길이 6인 undefined 배열 생성

- 6칸 고정으로 반복

2. .map((_, index_ => {...})

- 배열의 각 요소를 순회하며 JSX를 반환

-_는 배열 요소를 사용하지 않겠다.

3. const itemId = items[index];

- 실제 아이템 배열 items에서 해당 인덱스에 있는 아이템 ID 가져온다.

- 3개면 나머지 3개는 undefined

완성본

 

이제 아군과 적군을 나누면 된다.