본문 바로가기

부트캠프

Nalgangdoo_Project 3일차

팀프로젝트 3일차 어느정도 기본 기능 구현 마무리

내 파트
1.랭크 게임 로직 추가
2.랭크게임 승리시 랭크 포인트 변동
3.게임에 존재하는 모든 선수 목록 조회 API
4.특정한 선수 상세 조회 API
5.닉네임/비밀번호 변경API 

 

1. 랭크 게임 로직 추가
2일차에 친선 게임은 상대 사용자의 userId를 직접 URL에 입력하여 매칭을 하는 방식이었지만
랭크 게임은 서로 비슷한 rankPoint를 가진 유저를 랜덤으로 넣어주는 매칭시스템을 이용했다.
매칭시스템 시스템 미들웨어
0. 로그인 계정 점수 접근
 * 1. 모든 계정 조회
 * 2. 로그인한 account rankPoint 기반으로 +- 50점인 계정 필터
 * 2-1. +- 50점인 계정이 없으면 로그인한 계정 다음으로 점수가 낮은 계정과 매칭
 * 2-2. 2-1도 없으면 로그인한 계정 다음으로 점수가 높은 계정과 매칭
 * 3. 필터한 계정에서 랜덤 뽑기
 */
const matchMiddleware = async (req, res, next) => {
  try {
    // 인증 미들웨어에서 받은 유저 아이디
    const { userId } = req.user;

    // 로그인한 유저의 정보 조회
    const account = await prisma.account.findUnique({
      where: {
        userId: userId,
      },
    });
    if (!account) {
      return res.status(404).json({ message: "존재하지 않는 계정입니다." });
    }

    // 0. 로그인 유저 점수
    const loginUserRankPoint = account.rankPoint;

    // 1. 로그인한 계정을 제외한 모든 계정 조회
    const allUsers = await prisma.account.findMany({
      where: {
        userId: {
          not: userId, // 로그인한 계정 제외
        },
      },
    });

    // 2. rankPoint +-50점 범위의 계정 필터
    let filteredUsers = allUsers.filter(
      (enemy) =>
        enemy.rankPoint >= loginUserRankPoint - 50 &&
        enemy.rankPoint <= loginUserRankPoint + 50
    );

    // 2-1. 만약 +-50점 범위 내에 계정이 없다면 다음으로 낮은 점수의 계정 선택
    if (filteredUsers.length === 0) {
      filteredUsers = allUsers
        .filter((user) => user.rankPoint < loginUserRankPoint) // 로그인한 유저보다 낮은 점수의 계정 필터
        .sort((a, b) => b.rankPoint - a.rankPoint); // 내림차순
      // 가장 가까운 낮은 점수의 유저가 존재할 경우만 매칭
      if (filteredUsers.length > 0) {
        filteredUsers = [filteredUsers[0]];
      }

      // 2-2. 낮은 점수의 계정도 없다면 다음으로 높은 점수의 계정 선택
      if (filteredUsers.length === 0) {
        filteredUsers = allUsers
          .filter((user) => user.rankPoint > loginUserRankPoint) // 로그인한 유저보다 높은 점수의 계정 필터
          .sort((a, b) => a.rankPoint - b.rankPoint); // 오름차순
        // 가장 가까운 높은 점수의 유저가 존재할 경우만 매칭
        if (filteredUsers.length > 0) {
          filteredUsers = [filteredUsers[0]];
        }
      }
    }

    // 3. 필터된 계정 중 랜덤으로 하나 선택
    const matchedUser =
      filteredUsers[Math.floor(Math.random() * filteredUsers.length)];

    console.log(
      `매치 유저: ${matchedUser.userName}, 랭킹 점수: ${matchedUser.rankPoint}`
    );
    if (!matchedUser) return res.status(404).json({ message: "매칭 실패" });

    // 매칭된 유저 정보 저장
    req.matchedUser = matchedUser;

    next();
  } catch (err) {
    console.log(err);
    next(err);
  }
};

export default matchMiddleware;​

현재 게임을 플레이하는 유저 즉 본인은 인증 미들웨어로 받아온 userId로 처리하고 본인이 매칭을 돌릴경우
rankPoint가 +- 50 차이가 나는 유저를 account테이블에서 조회하여 매칭시키는 로직이다.
유저중 +-50 범위의 유저가 없다면 본인의rankPoint와 가장 비슷한 rankPoint를 가지는 유저를 매칭시켜준다.
그마저도 없다면 매칭실패를 반환한다.
이 미들웨어를 거친다면 랭크게임 로직으로 넘어간다.

랭크게임에서는 상대를 지정하는게 아니므로 URL은 /games로 처리 
URL 뒤에는 인증미들웨어 =>통과=>매칭시스템=>통과=>랭크게임 시작 순으로 작성함

router.post("/games", authMiddleware, matchMiddleware, async (req, res, next) => {}

현재 사용자의 팀 구성을 확인하기위해 현재 사용자캐릭터정보를 가져오고 현재 보유중인 캐릭터의 isFormation이 true 라면 다음단계로 넘어간다. 상대 사용자도 같은 로직을 사용 여기서 본인userId는 토큰에서 받아오고 상대 userId는 매칭시스템에서 받아온다.

      const currentUserId = req.user.userId;
    const enemyUserId = req.matchedUser;

    
    //현재 사용자의 캐릭터 정보
      const currentUserCharacters = await prisma.account.findFirst({
        where: { userId: currentUserId },
        include: { characters: true },
      });

      //현재 사용자 캐릭터 필터링
      const currentUserCharactersFilter =
        currentUserCharacters.characters.filter(
          (char) => char.isFormation === true
        );

      //현재 사용자 캐릭터 보유현황 체크
      if (currentUserCharacters.characters.length === 0) {
        return res.status(400).json({
          message: "상대 사용자가 캐릭터를 보유하고 있지 않습니다.",
        });
      }

      //현재 사용자 팀 구성 인원 체크
      if (currentUserCharactersFilter.length !== 3) {
        return res
          .status(400)
          .json({ message: "현재 사용자의 팀 구성원이 3명이 아닙니다." });
      }

필터링을 거친 캐릭터가 3명이아니라면 에러코드를 보내준다.

캐릭터를 가져왔다면 승/무/패의 확률을 정하기위해 보유캐릭터의 스탯을 모두 합한다.
캐릭터의 스탯의 총합이 높을수록 승률이 올라감
그 스탯을 합쳐주기위한 스탯 가중치도 설정해준다.

//스텟 가중치
      const statWeight = {
        speed: 0.1,
        goalDetermination: 0.2,
        shootPower: 0.15,
        defense: 0.4,
        stamina: 0.2,
      };
      // 현재 사용자의 캐릭터 ID 배열
      const currentUserCharacterIds = currentUserCharacters.characters.map(
        (char) => char.characterId
      );

      // 상대 사용자의 캐릭터 ID 배열
      const enemyUserCharacterIds = enemyUserCharacters.characters.map(
        (char) => char.characterId
      );

      // 각 팀의 캐릭터 정보 가져오기
      const [currentUserCharactersDetails, enemyUserCharactersDetails] =
        await Promise.all([
          prisma.character.findMany({
            where: { characterId: { in: currentUserCharacterIds } },
          }),
          prisma.character.findMany({
            where: { characterId: { in: enemyUserCharacterIds } },
          }),
        ]);

      // 점수 계산 함수
      function calculateScore(characters) {
        let totalScore = 0;
        for (let i = 0; i < characters.length; i++) {
          const score =
            characters[i].speed * statWeight.speed +
            characters[i].goalDetermination * statWeight.goalDetermination +
            characters[i].shootPower * statWeight.shootPower +
            characters[i].defense * statWeight.defense +
            characters[i].stamina * statWeight.stamina;
          totalScore += score;
        }
        return totalScore;
      }

      const scoreA = calculateScore(currentUserCharactersDetails); //현재 사용자의 팀 점수
      const scoreB = calculateScore(enemyUserCharactersDetails); //상대 사용자의 팀 점수

그렇게 계산한 값을 scoreA와 scoreB에 할당시켜주고 승률을 정하기위해 random*두수의 합을 작성했다.

 //승패 결정
      const maxScore = scoreA + scoreB;
      const rendomWinner = Math.random() * maxScore; //팀 스탯 점수에 비례하여 승률확인

 
무승부의 경우 해당 유저의 아이디의 컬럼에 drowCount를 서로 +1 씩 해주고
승/패의 경우 승리유저는winCount+1 패배유저는 loseCount+1 씩해준다.

//무승부일 경우
      if (scoreA === scoreB) {
        //현재 사용자
        await prisma.account.update({
          where: { userId: currentUserId },
          data: { drowCount: { increment: 1 } },
        });
        //상대 사용자
        await prisma.account.update({
          where: { userId: enemyUserId.userId },
          data: { drowCount: { increment: 1 } },
        });
        return res.status(200).json({ message: "무승부 입니다!" });
      }

      if (rendomWinner < scoreA) {
        //현재 유저 승리 처리
        const aScore = Math.floor(Math.random() * 4) + 2;
        const bScore = Math.floor(Math.random() * Math.min(5, aScore)); //A스코어보다 작은값
        result = `${currentUserId}팀 승리: ${currentUserId} :${aScore} - ${bScore} : ${enemyUserId.userId}`;

        //사용자 게임 승률 OR 랭크 포인트 조정
        //승리팀
        await prisma.account.update({
          where: { userId: currentUserId },
          data: {
            winCount: { increment: 1 },
        
            rankPoint: { increment: 10 },
          },
        });

        //패배팀
        await prisma.account.update({
          where: { userId: enemyUserId.userId },
          data: {
          
            loseCount: { increment: 1 },
            rankPoint: { decrement: 10 },
          },
        });
      } else {
        //상대 유저 승리 처리
        const bScore = Math.floor(Math.random() * 4) + 2;
        const aScore = Math.floor(Math.random() * Math.min(5, bScore)); //B스코어보다 작은값
        result = `${enemyUserId.userId}팀 승리: ${enemyUserId.userId}:${bScore} - ${aScore} : ${currentUserId}`;

        //사용자 게임 승률 OR 랭크 포인트 조정
        //승리팀
        await prisma.account.update({
          where: { userId: enemyUserId.userId },
          data: {
            winCount: { increment: 1 },
          
            rankPoint: { increment: 10 },
          },
        });

        //패배팀
        await prisma.account.update({
          where: { userId: currentUserId },
          data: {
          
            loseCount: { increment: 1 },
            rankPoint: { decrement: 10 },
          },
        });
      }

선수 전체 목록 조회 와 상세 조회

3. 선수 전체 목록 조회
원래 내 파트는 아니었지만 팀원분의 스파르타코딩클럽 탈주로 인해 내가 맡게 되었다...
간단하게 get을 사용하여 character테이블에있는 모든 캐릭터를 조회하여 이름만 출력해주는 방식으로 마무리
/**
 * @desc 선수 전체 목록 조회 API
 * @abstract 게임에 있는 모든 선수의 목록을 보여준다.
 */
router.get("/character", async (req, res, next) => {
  const character = await prisma.character.findMany({
    select: {
      name: true,
    },
  });
  return res.status(200).json(character);
});

4.선수 상세 조회
상세 조회는 특정한 캐릭터를 찾아야 하기 때문에 URL 파라미터로 캐릭터의 아이디를 받아 해당 캐릭터의
이름 스탯을 모두 조회할수 있도록 구현함
router.get("/character/:characterId", async (req, res, next) => {
  try {
    const { characterId } = req.params;
    const character = await prisma.character.findFirst({
      where: { characterId: parseInt(characterId, 10) },
      select: {
        name: true,
        speed: true,
        goalDetermination: true,
        shootPower: true,
        defense: true,
        stamina: true,
      },
    });
    if (!character) {
      return res.status(404).json({ message: "선수를 찾을 수 없습니다." });
    }
    return res.status(200).json(character);
  } catch (err) {
    console.log(err);
    return res.status(500).json({ message: "서버 에러가 발생했습니다." });
  }
});​

비밀번호 / 닉네임 변경 API
기본 구현을 마무리하고 이제는 하고싶은 기능들을 추가하기로했다.
타 사이트와 마찬가지로 비밀번호와 닉네임을 변경할 수 있도록 구현하였다.
어떤 유저가 데이터를 변경 할 것인가 를 나타내기위해 userId를 URL파라미터로 받아오고
해당 유저의 현재 비밀번호가 일치해야만 새로운비밀번호와 새로운닉네임으로 변경할 수 있도록 구현하였다.
특정 데이터만 변경하므로 PATCH를 사용
클라이언트에 입력한 현재 비밀번호가 실제 비밀번호와 일치하지않는다면 에러코드를 출력하도록 작성했다.
router.patch(
  "/user-data-change/:userId",
  authMiddleware,
  async (req, res, next) => {
    const { userId } = req.params;
    const { currentPassword, newPassword, newUserName } = req.body;

    try {
      //사용자 정보 조회
      const user = await prisma.account.findUnique({
        where: { userId: userId },
      });
      //사용자 존재 여부 체크
      if (!user) {
        return res.status(404).json({ message: "사용자를 찾을 수 없습니다." });
      }

      //현재 비밀번호와 일치여부 체크
      const passwordCheck = await bcrypt.compare(
        currentPassword,
        user.password
      );
      if (!passwordCheck) {
        return res
          .status(401)
          .json({ message: "현재 비밀번호와 일치하지 않습니다." });
      }
      const updateData = {};
      //새로운 비밀번호 해시화후 추가
      if (newPassword) {
        updateData.password = await bcrypt.hash(newPassword, 10);
      }
      //새로운 닉네임 추가
      if (newUserName) {
        updateData.userName = newUserName;
      }
      const updateUser = await prisma.account.update({
        where: { userId: userId },
        data: updateData,
      });

      return res.status(200).json({
        message: "사용자 정보를 업데이트 하였습니다.",
        user: updateUser,
      });
    } catch (err) {
      console.log(err);
      return res.status(500).json({ message: "서버 에러가 발생했습니다." });
    }
  }
);​

'부트캠프' 카테고리의 다른 글

Nalgangdoo_Project 5일차  (1) 2024.09.24
Nalgangdoo_Project 4일차  (0) 2024.09.23
Nalgangdoo_Project 2일차  (0) 2024.09.19
Nalgangdoo_project 1일차  (1) 2024.09.13
Item Simulator 과제 정리  (0) 2024.09.12