넥슨 오픈API로 EA SPORTS FC 온라인 전적을 조회·집계하는 PyQt6 데스크톱 앱(윈도우). 구단주 닉네임을 검색하면 감독모드 전적을 받아와 승률·선수 지표·전술 상성 등을 화면으로 보여준다.
- 형태: 파이썬 3 + PyQt6 GUI 단일 실행 앱. 서버 없음, 설치 없이 소스로 실행하거나 exe로 빌드
- 데이터: 넥슨 오픈API(JSON) + 공식 데이터센터 HTML 스크래핑(오픈API에 없는 순위·구단가치·선수 능력치)
- 저장: 조회할 때마다 로컬 SQLite(
fifa.db)에 누적 — API가 최근 100경기까지만 주는 한계를 넘기 위해 - API 키: 개인 발급 키를
.env에 두고 읽는다(저장소에 포함되지 않음)
| 탭 / 기능 | 내용 |
|---|---|
| 전적 | 최근 경기 목록 — 결과·스코어·상대·점유율·슛 등 |
| 선수 지표 | 선수별 출전·평점·공격력·수비력·기대득점률·선방력(공식 미제공 값 역산) |
| 전술·경기 결과 | 상대 전술(포메이션)별 승률, 결과 분포 |
| 상대 전적 | 구단주별 상성, 포지션별 최다 상대 |
| 승률 추이 | 기간별 승률·등급(디비전) 계단 그래프 |
| 기간별 추이 | 1일/2일/1주/1개월 단위 전적 + 연승·연패 기록 |
| 승부처 분석 | 선제골 승률, 시간대별 득실 분포, 시각대별 승률 |
| 성적 진단 | 상대 등급(디비전)별 승률·평균 득실, 점유율 구간별 승률 |
| 선수별 결정력 | 슛·유효슛·골·전환율·xG·어시스트 랭킹 |
| 선수 조합 | 함께 선발로 나온 선수쌍별 승률 — 잘 맞는/안 맞는 조합 |
| 슛 맵 | 슛 좌표를 하프 피치에 시각화(내 슛 / 상대 슛 실점 위치) |
| 선수 카드 | 능력치·특성·시세·클럽경력 + 강화단계·적응도·팀컬러 시뮬레이터 |
| 랭커 카드 | 감독모드 순위·구단가치·랭킹점수(ELO) |
닉네임을 검색하면 랭커 카드로 요약을 보여주고, "감독모드 분석"에서 탭별 상세로 들어간다.
| 랭커 카드 — 순위·점수·전적·구단가치 요약 |
선수 지표 — 포지션별 출전·평점·공수 지표(공식 미제공 값 역산) |
| 전술·경기 결과 — 상대 포메이션별 승률, 득·실점 유형 |
승률 그래프 — 기간별 승률·등급(디비전) 계단 그래프 |
| 승부처 분석 — 선제골 승률, 시간 구간별 득실, 시각대별 승률 |
슛 맵 — 슛 좌표를 하프 피치에 시각화(골/유효슛/빗나감) |
git clone https://github.com/yim2412/fifa-match-tracker.git
cd fifa-match-tracker
# 1) 패키지 설치
pip install -r requirements.txt
# 2) API 키 설정 — .env.example 을 복사해 .env 로 만들고 키를 넣는다
copy .env.example .env
notepad .env
# 3) API 연결 점검 (GUI 전에 먼저)
python check_api.py 내닉네임
# 4) 앱 실행
python app_main.py
# 5) 파싱·집계 회귀 테스트 (네트워크 없이, 실제 응답 픽스처로)
python tests/test_parsing.py
python tests/test_bot.py
node tests/test_adapter.js
# 6) (선택) 카카오톡 오픈채팅 봇 — 서버는 bot/README.md,
# 카톡에 붙이는 방법은 adapters/README.md
python -m bot.server| 파일 | 역할 |
|---|---|
app_main.py |
PyQt6 UI — 검색 바, 랭커 카드, 분석 탭 15종(선수 지표·전술·승부처 분석·성적 진단·슛 맵·결정력·선수 조합·팀컬러 등), 조회 워커 스레드 |
widgets.py |
화면 부품(위젯) — 랭커 카드·표·승률/등급 그래프·축구장 스쿼드 배치(PitchWidget)·슛 맵·NoScrollComboBox 등 |
theme.py |
색상 상수·전역 스타일시트(QSS) — 배경·승/패·수비 등 색과 버튼·글꼴 스타일을 한 곳에 |
nexon_api.py |
넥슨 오픈API 클라이언트. 엔드포인트 경로·에러코드가 전부 여기 상수에 모여 있다 |
models.py |
매치 상세 JSON → MatchSummary 파싱, Stats·상대 전적·승률 추이 집계 |
stats.py |
여러 경기 집계 — 선수 지표·전술·경기 결과. 역산해서 알아낸 상수가 여기 모여 있다 |
store.py |
조회한 경기를 SQLite(fifa.db)에 누적 — API 100경기 한계 극복 |
ranker.py |
넥슨 데이터센터에서 감독모드 순위·구단가치·ELO 를 긁어온다(오픈API 엔 없음) |
images.py |
넥슨 CDN에서 선수 얼굴 이미지를 받아 디스크에 캐시(오픈API 아닌 정적 CDN, 비공식) |
playerinfo.py |
선수 카드 상세(능력치·특성·시세·클럽경력) — 넥슨 모바일 데이터센터 HTML 스크래핑 |
config.py |
API 키 로드(.env), DB 경로, 매치 종류·조회 개수 기본값 |
check_api.py |
터미널에서 키·엔드포인트·집계·DB 동작 확인용 |
bot/ |
카카오톡 오픈채팅 봇 서버 — 같은 API·DB를 쓰는 별도 진입점(bot/README.md) |
adapters/ |
봇을 카카오톡에 붙이는 어댑터 — 메신저봇R 스크립트(adapters/README.md) |
tests/test_parsing.py |
파싱·집계 회귀 테스트 — 실제 응답 픽스처로 골든값 고정(네트워크 없음) |
tests/test_bot.py |
봇 명령 처리 회귀 테스트 — 채팅 한 줄 → 답장까지(네트워크 없음) |
tests/test_adapter.js |
어댑터 회귀 테스트 — 가짜 java.* 로 메신저봇R 진입점을 그대로 실행(node) |
기본 매치 종류는 감독모드(52) 다 — 이 앱은 감독모드 전적을 본다.
API는 최근 100경기까지만 준다. 이 계정은 21시간에 100경기가 쌓여서(시간당 약 5경기), 하루만 조회를 걸러도 그 사이 경기는 영영 못 가져온다 — 과거를 조회할 수단이 API에 없다.
그래서 조회할 때마다 fifa.db에 쌓고, 화면은 API가 아니라 DB를 그린다.
검색하는 순간이 곧 수집이라, 자주 보는 계정일수록 데이터가 길어진다.
- 저장 기준은
ouid— 구단주명은 바뀌어도 ouid는 안 바뀐다 - 경기 원본은 한 번만 — 한 경기에 두 명이 나오므로
matches/match_players로 분리. 상대를 검색하면 이미 받아 둔 경기가 재활용된다 - 이미 있는 경기는 API를 다시 부르지 않는다 (
store.existing_ids) - 등록 계정 — 조회하면 자동 등록. 목록에서 고르면 바로 조회. 등록 해제해도 경기 기록은 남는다
한계: 과거는 못 되살린다. 처음 조회 시점의 최근 100경기가 시작점이고, 그 이전은 없다. 조회 간격이 100경기(≈20시간)보다 길면 그 사이가 뚫린다.
순위·구단가치·랭킹점수(ELO)는 오픈API(JSON)에 없다. 넥슨 공식 데이터센터 웹페이지에는 있어서 거기서 긁어온다.
https://fconline.nexon.com/datacenter/rank_inner?rt=manager&strCharacterName=<닉네임>
- JSON API 가 아니라 HTML 스크래핑이다. 넥슨이 페이지 구조(class 이름)를 바꾸면
깨진다. 그래서 URL·class·정규식을 전부
ranker.py한 곳에만 둔다 — 깨지면 실제 응답과 대조해 거기만 고친다. - 여기 전적은 감독모드 통산(오픈API 의 최근 3천 경기보다 많다)이라 랭커 카드는 이 값을 우선 쓴다. 단 요약 숫자일 뿐 경기별 상세는 아니다.
- 랭킹은 상위 1만 위까지만. 밖이면 순위·구단가치·점수를 "랭킹 밖"으로 두고 전적은 앱이 받은 경기로 집계한다.
- 호스트:
https://open.api.nexon.com - 인증: 요청 헤더
x-nxopen-api-key - 키 발급: NEXON Open API 로그인 → 마이페이지
사용 중인 엔드포인트 (nexon_api.py 상단 상수):
| 상수 | 경로 | 용도 |
|---|---|---|
EP_ID |
/fconline/v1/id |
닉네임 → ouid |
EP_USER_BASIC |
/fconline/v1/user/basic |
계정 기본 정보 |
EP_MAX_DIVISION |
/fconline/v1/user/maxdivision |
역대 최고 등급 |
EP_USER_MATCH |
/fconline/v1/user/match |
매치 id 목록 |
EP_MATCH_DETAIL |
/fconline/v1/match-detail |
매치 상세 |
메타데이터(매치 종류·선수·시즌·등급)는 인증 없이 https://open.api.nexon.com/static/fconline/meta/{name}.json.
공식 문서가 자바스크립트로 렌더링돼 경로를 자동 대조하지 못했다.
check_api.py가 FAIL이면 공식 문서와 대조해nexon_api.py상단 상수만 고치면 된다.
실제 응답 100경기로 확인한 내용. 상수는 stats.py 상단에 있다.
| 항목 | 내용 | 어떻게 확인했나 |
|---|---|---|
shoot.goalTotal 등이 null |
상대 탈주 등 기록 없는 경기는 키를 빼지 않고 null로 준다. .get(k, 0)은 기본값이 안 먹으니 타입까지 확인해야 한다(models._i) |
30경기 조회 시 TypeError로 재현 |
shootDetail[].result |
3 = 골 |
result==3 개수가 goalTotal 합계와 정확히 일치(126/126) |
shootDetail[].goalTime |
비트 패킹. >>24 = 구간(0 전반/1 후반/2 연장전반/3 연장후반), & 0xFFFFFF = 경과 초 |
구간 0·1은 최대 49.8분(45+추가), 2·3은 19.1분(15+추가)으로 딱 맞음 |
shootDetail[].type |
슛 유형. 1 일반(D) · 2 감아차기(ZD) · 3 헤더 · 6 땅볼(DD) · 7 발리 · 8 프리킥 · 9 페널티킥 | 8·9는 goalFreekick/goalPenaltyKick 집계와 200개 선수-경기 행에서 경기별 정확히 일치. 나머지는 외부 전적 사이트의 유형별 골 수와 합계 일치 |
shootDetail[].result 1 · 2 |
1 = 유효슛(막힘) · 2 = 빗나감(3은 골) | effectiveShootTotal == 골+result1이 1189/1190, shootTotal == shootDetail 총개수가 1190/1190 |
shootDetail[].x · .y |
슛 좌표(0~1 정규화). x=1.0이 상대 골문 쪽(슛이 x>0.48에 몰림), y=0.5가 폭 중앙. assistX/Y·hitPost·inPenalty도 있음 |
실제 캐시 1,190 선수-경기로 범위·방향 확인 |
type 4 · 10 · 13 · 14 |
미상. "알 수 없음"으로 표시 | 근거 없음. 참고한 사이트도 못 읽는다 |
shoot.shootOutScore |
승부차기 점수. 승부차기가 없었으면 양쪽 0 | 2:2 무승부인데 승/패가 갈린 경기에서만 0이 아님 |
spPosition |
28 = SUB(교체 명단). 전술은 GK·SUB 제외하고 수비(1-8)-수미(9-11)-미드(12-16)-공미(17-19)-공격(20-27) 인원 |
상대 전술별 승률이 외부 사이트와 일치(4-1-2-3-0 → 51.7%, 31승3무26패) |
매치 상세는 경기별 실제 스쿼드를 준다(현재 스쿼드를 돌려주는 게 아니다). 볼타 66경기에서 서로 다른 스쿼드 조합 10개가 나오는 것으로 확인.
- 선수별 통계 —
stats.aggregate_players - 상대 전술별 승률 —
stats.formation_stats - 공격력·수비력·기대득점률·선방력 — API 미제공이라 fc-info.com 프론트엔드 JS 번들
(
pages/analysis/coach/[id]-*.js)에서 계산식을 그대로 역산해 옮겼다(PlayerStat). 실제 100경기로 재현해 값이 거의 일치함을 확인(GK 선방력 151.5 vs 참고 151.6 등). 선방력은 defending 합계가 아니라 경기당 평균×100이다 — 100경기 표본에서 우연히 합계처럼 보였을 뿐, 경기 수가 다르면 값이 완전히 달라진다 - 상대 구단주별 상성 분석 —
models.opponent_stats, "상대 전적" 탭 - 기간별 승률 추이 차트 —
models.win_rate_trend(월별), "승률 추이" 탭 - 조회 결과 로컬 DB(SQLite) 누적 —
store.py - 선수 이미지 (넥슨 CDN) —
images.py. 오픈API가 아니라 비공식 CDN 직링크 (fco.dn.nexoncdn.co.kr)라 넥슨이 바꾸면 깨질 수 있다 - exe 빌드 (PyInstaller)
- 선수 카드 상세(능력치·특성·시세·클럽경력) —
playerinfo.py, 상대 스쿼드·선수 지표 표·포지션별 최다 상대 표에서 선수 클릭(더블클릭) - 선수 카드 능력치 시뮬레이터 — 강화단계(1~13강, 기본 1강)·적응도(+1/+5, 기본
+1 — 홈페이지 첫 화면과 동일 기준)·팀컬러(소속/강화/관계 3종)를
바꿔가며 실제 능력치 확인. 로컬 근사
계산이 아니라 PC 데이터센터(
fconline.nexon.com)가 실제로 쓰는/datacenter/PlayerAbility엔드포인트를 그대로 호출해서 넥슨 서버가 계산한 값을 받는다(playerinfo.fetch_player_ability) — 팀컬러 종류(600여개)별 보너스 조합표를 넥슨이 공개하지 않아 근사가 불가능하므로, 정확성을 위해 매 조작마다 재조회하는 방식을 택함. 팀컬러 드롭다운은 응답 HTML에 같이 오는 그 선수 전용 선택지로 채운다(전체 목록 페이지 불필요). 레벨 UI는 없음 — 소속 팀컬러는 항상 그 팀컬러의 최대 레벨(대부분 4)로 자동 조회하고, 강화 팀컬러는 항목 자체에 레벨이 있으며("Lv.2 금빛 물결"), 관계 팀컬러는 레벨이 없다. 능력치 값 색상도 데이터센터와 같은 10점 구간 기준(playerinfo.stat_color). "포지션별 오버롤" 탭 — 데이터센터 축구장 그림의 16개 포지션 값(1강·적응도 +1 기준) - 구단주 비교 탭에 각 계정의 최근 경기 스쿼드 표시 —
stats.own_squad - 등급(디비전) 추이 그래프 — 승률 그래프 탭 아래, 매 경기 당시 division을 계단 그래프로(저장된 데이터만 사용, 추가 조회 없음)
- 기간별 추이 탭 — 누적 전체를 1일/2일/1주/1개월 단위로 묶은 전적 표
(
models.period_stats) + 현재/최장 연승·연패(models.longest_streaks) - 팀컬러 랭킹 표에 팀컬러별 평균/최저/최고 팀가치 — 팀컬러를 긁을 때 같은 페이지에 있는 구단가치를 같이 저장(추가 요청 없음). 캐시 TTL은 팀컬러·팀가치 모두 7일. 넥슨 웹 점검 중 응답은 "랭킹 밖"으로 오인해 캐시하지 않도록 감지
- 승부처 분석 탭 — 이미 저장된
shootDetail만으로 계산(추가 조회 없음). 선제골 승률(stats.clutch_summary: 내가 선제골/선제 실점 시 승률, 역전승·역전패), 정규시간 15분 6구간+연장의 득실 분포(stats.goal_minute_buckets), 시각대별 승률(stats.time_of_day_rates: 심야/오전/오후/저녁밤 4구간) - 선수별 결정력 랭킹 —
shootDetail을 슈터(spId)·도움(assistSpId)별로 모아 선수별 슛·유효슛·골·전환율·xG·어시스트(stats.finishing_ranking). "선수별 결정력" 탭, 골−xG에 색(+초록/−빨강), 선수 더블클릭 시 선수 카드. 표시 구간 기준 - 기대득점(xG) 근사 —
stats.shot_xg. 넥슨이 정답값을 안 줘서 순수 근사(비공식). 슛 좌표의 골까지 거리·골대 각도 + 박스 여부·슛 유형(페널티킥 0.76, 헤더 감산)으로 득점 확률을 로지스틱 추정. 계수는 실제 캐시에서 xG 합≈실제 골이 되도록 맞춤 (자기 계정 캘리브레이션 — 200경기서 xG 105.4 vs 실측 113). 슛 맵 요약·결정력 표에 표시 - 슛 맵 —
shootDetail의 x/y 좌표를 공격 진영 하프 피치 위에 점으로 (stats.shot_map,widgets.ShotMapWidget). 골=초록·유효슛=노랑·빗나감=회색, 골대맞음은 흰 테두리. "슛 맵" 탭에서 내 슛/상대 슛(실점 위치) 토글, 골·유효슛· 빗나감 종류별 표시 on/off 체크박스, 표시 구간 기준. 요약(슛·골·유효슛률·전환율)은 필터와 무관하게 전체 기준으로 표시. 상대 슛은 좌표가 상대 공격 기준이라 y(좌우)를 뒤집어 내 시점으로 통일 - 성적 진단 탭 — 저장된 데이터만으로(추가 조회 없음) 상대 등급(
division)별 승률· 평균 득실(stats.division_stats)과 내 점유율 구간별(열세<40·균형40~60·우세>60) 승률(stats.possession_stats). 누적 전체 기준 - 선수 조합(케미) 탭 — 함께 선발(SUB·GK 제외)로 나온 선수쌍별 승률
(
stats.pair_synergy). 최소 경기수 필터, 승률 50% 기준 색칠(초록/빨강), 선수 더블클릭 시 카드. 누적 전체 기준
넥슨 오픈API·데이터센터를 이용하는 비공식 개인 프로젝트다. EA SPORTS FC 온라인과 관련 데이터의 권리는 NEXON / EA 에 있으며, 이 프로젝트는 넥슨·EA 와 무관하다.