🏗️배드민턴 매칭 알고리즘 기술 구현 가이드

📋 목차

🏗️아키텍처 개요

핵심 클래스 구조

BracketGenerator ├── generateBracket() // 메인 진입점 ├── createMatching() // 매칭 방식 분기 ├── generateSelfTournamentMatches() // 자체대회 매칭 ├── selectBestMatch() // 최적 매치 선택 ├── optimizeMercenaries() // 용병 최적화 └── assignRoundsAndCourts() // 라운드/코트 배정 MatchingUtils ├── shuffleArray() // Fisher-Yates 셔플 ├── calculateTeamStrength() // 팀 실력 계산 ├── balanceTeams() // 팀 밸런스 조정 └── groupPlayersBySkill() // 실력별 그룹화 DateUtils ├── calculateAge() // 나이 계산 ├── getAgeGroup() // 나이대 분류 └── convertBirthYear() // 출생년도 변환

🔧핵심 알고리즘 구현

1. 메인 매칭 생성 함수

async generateBracket(tournament, players) { try { // 1. 입력값 검증 this.validateInputs(tournament, players); // 2. 선수 정보 완전성 검증 const completePlayers = await this.validatePlayerData(players); // 3. 선수 수 조정 (4의 배수) const adjustedPlayers = this.adjustPlayerCount(completePlayers); // 4. 성별 필터링 const filteredPlayers = this.applyGenderFilter(adjustedPlayers, tournament.sameGenderOnly); // 5. 매칭 생성 const matches = this.createMatching(filteredPlayers, tournament); // 6. 라운드 및 코트 배정 const brackets = this.assignRoundsAndCourts(matches, tournament); // 7. 용병 최적화 const optimizedBrackets = this.optimizeMercenaries(brackets, completePlayers, tournament); return optimizedBrackets; } catch (error) { console.error('대진표 생성 실패:', error); throw error; } }

2. 선수 수 조정 알고리즘

adjustPlayerCount(players) { const playerCount = players.length; const remainder = playerCount % 4; if (remainder === 0) { console.log('선수 수가 이미 4의 배수입니다.'); return players; } const mercenariesNeeded = 4 - remainder; console.log(`${mercenariesNeeded}명의 용병이 필요합니다.`); // 성별 분석 const genderCount = this.analyzeGenderDistribution(players); const mercenaries = this.generateMercenaries(mercenariesNeeded, genderCount); return [...players, ...mercenaries]; } analyzeGenderDistribution(players) { const count = { male: 0, female: 0 }; players.forEach(player => { if (player.gender === '남성') count.male++; else if (player.gender === '여성') count.female++; }); return count; } generateMercenaries(count, genderCount) { const mercenaries = []; const totalPlayers = genderCount.male + genderCount.female; for (let i = 0; i < count; i++) { // 성별 균형을 위한 로직 let gender; if (genderCount.male < genderCount.female) { gender = '남성'; genderCount.male++; } else if (genderCount.female < genderCount.male) { gender = '여성'; genderCount.female++; } else { // 동일한 경우 번갈아가며 gender = i % 2 === 0 ? '남성' : '여성'; } mercenaries.push({ id: `mercenary_${Date.now()}_${i}`, name: `용병${i + 1}`, gender: gender, skill: 'B', grade: 'B', birthYear: '90', isMercenary: true }); } return mercenaries; }

3. 자체대회 매칭 생성 알고리즘

generateSelfTournamentMatches(players, tournament) { const { gamesPerPlayer, matchingType } = tournament; const matches = []; const playerGameCount = {}; const usedPairings = new Set(); // 초기화 players.forEach(player => { playerGameCount[player.id] = 0; }); console.log(`자체대회 매칭 시작: ${players.length}명, 선수당 ${gamesPerPlayer}게임`); // 1단계: 이상적인 매치 생성 let attempts = 0; const maxAttempts = players.length * gamesPerPlayer * 3; while (attempts < maxAttempts) { attempts++; // 게임이 필요한 선수들 필터링 const needsGames = players.filter(p => playerGameCount[p.id] < gamesPerPlayer); if (needsGames.length < 4) break; // 최적의 4명 선택 const selectedPlayers = this.selectBestMatch(needsGames, matchingType, usedPairings); if (selectedPlayers.length === 4) { const match = { team1: [selectedPlayers[0], selectedPlayers[1]], team2: [selectedPlayers[2], selectedPlayers[3]], type: 'self_tournament' }; matches.push(match); // 게임 수 업데이트 selectedPlayers.forEach(p => { playerGameCount[p.id]++; }); // 사용된 페어링 기록 this.recordUsedPairings(selectedPlayers, usedPairings); } } // 2단계: 강제 매치 생성 (게임 수 부족한 선수들을 위해) this.generateForceMatches(players, matches, playerGameCount, gamesPerPlayer); return matches; }

4. 최적 매치 선택 알고리즘

selectBestMatch(players, matchingType, usedPairings) { const shuffled = MatchingUtils.shuffleArray(players); let bestCombination = []; let minPairingOverlap = Infinity; // 모든 4명 조합을 검사 for (let i = 0; i < shuffled.length - 3; i++) { for (let j = i + 1; j < shuffled.length - 2; j++) { for (let k = j + 1; k < shuffled.length - 1; k++) { for (let l = k + 1; l < shuffled.length; l++) { const candidates = [shuffled[i], shuffled[j], shuffled[k], shuffled[l]]; const overlap = this.countPairingOverlap(candidates, usedPairings); if (overlap < minPairingOverlap) { minPairingOverlap = overlap; bestCombination = candidates; } // 완벽한 조합을 찾으면 즉시 반환 if (minPairingOverlap === 0) { return this.balanceMatch(bestCombination, matchingType); } } } } } return this.balanceMatch(bestCombination, matchingType); } countPairingOverlap(players, usedPairings) { let overlap = 0; for (let i = 0; i < players.length; i++) { for (let j = i + 1; j < players.length; j++) { const pairKey = this.getPairKey(players[i].id, players[j].id); if (usedPairings.has(pairKey)) { overlap++; } } } return overlap; } getPairKey(id1, id2) { // 일관된 페어 키 생성 (순서 무관) return id1 < id2 ? `${id1}-${id2}` : `${id2}-${id1}`; }

5. 팀 밸런스 조정 알고리즘

// MatchingUtils.js static balanceTeams(team1, team2) { const maxIterations = 100; let bestTeam1 = [...team1]; let bestTeam2 = [...team2]; let bestDifference = this.calculateBalanceDifference(team1, team2); let currentTeam1 = [...team1]; let currentTeam2 = [...team2]; for (let i = 0; i < maxIterations; i++) { // 각 팀에서 랜덤하게 선수 선택 const idx1 = Math.floor(Math.random() * currentTeam1.length); const idx2 = Math.floor(Math.random() * currentTeam2.length); // 선수 교환 시뮬레이션 const newTeam1 = [...currentTeam1]; const newTeam2 = [...currentTeam2]; [newTeam1[idx1], newTeam2[idx2]] = [newTeam2[idx2], newTeam1[idx1]]; // 새로운 밸런스 계산 const newDifference = this.calculateBalanceDifference(newTeam1, newTeam2); // 더 좋은 밸런스라면 업데이트 if (newDifference < bestDifference) { bestTeam1 = newTeam1; bestTeam2 = newTeam2; bestDifference = newDifference; currentTeam1 = newTeam1; currentTeam2 = newTeam2; } // 완벽한 밸런스를 찾으면 조기 종료 if (bestDifference === 0) { break; } } return { team1: bestTeam1, team2: bestTeam2, difference: bestDifference }; } static calculateTeamStrength(team) { return team.reduce((total, player) => { if (player.isMercenary) { return total + 2.5; // 용병은 평균 실력 (B급 = 3점의 중간값) } return total + this.getSkillScore(player.skill); }, 0); } static getSkillScore(skill) { const skillMap = { 'S': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, '초심': 0 }; return skillMap[skill] || 0; }

🚀성능 최적화 기법

1. 조기 종료 최적화

// 완벽한 매치를 찾으면 즉시 반환 if (minPairingOverlap === 0) { return this.balanceMatch(bestCombination, matchingType); } // 완벽한 밸런스를 찾으면 조기 종료 if (bestDifference === 0) { break; }

2. 메모리 효율적인 자료구조

// Set을 사용한 O(1) 검색 const usedPairings = new Set(); // Map을 사용한 효율적인 키-값 저장 const playerGameCount = new Map(); const playerSchedule = new Map();

3. 배치 처리 최적화

// 대량 데이터 처리 시 청크 단위로 처리 const CHUNK_SIZE = 100; const chunks = this.chunkArray(matches, CHUNK_SIZE); for (const chunk of chunks) { await this.processMatchChunk(chunk); // 다음 청크 처리 전 잠시 대기 (UI 블로킹 방지) await this.sleep(1); }