Fullstack 119

heo4·3일 전

Fullstack

목록 보기
74/74

풀스택

FC 온라인 스쿼드 메이커를 만들며 — "게임과 숫자가 다르다"를 끝까지 따라간 하루

해피파(HAFIPA) 개발기 · 2026-10-08
해피파는 FC 온라인 유저를 위한 비공식 올인원 웹앱(PWA)입니다. 수수료·강화 계산, 쿠폰, 스쿼드, 선수 정보를 탭 몇 번으로 해결하는 게 목표예요.

오늘은 원래 "갱신시간 알리미" 실험 결과를 정리하는 날이었습니다. 그런데 하루를 돌아보면 거의 모든 작업이 한 문장으로 모입니다.

"앱에 나오는 숫자가 게임이랑 달라요."

이 한 문장을 따라가다 보니 스쿼드 메이커가 생겼고, 공식 데이터에 숨어 있던 규칙 몇 가지를 찾았고, 그동안 모르고 있던 버그도 여럿 잡았습니다. 그 과정을 정리해 봅니다.


1. 스쿼드 메이커를 앞당기다

로드맵에서 스쿼드 메이커는 "Phase 4, 보류"였습니다. 커뮤니티 기능까지 생각하면 운영 부담이 컸기 때문이에요. 그런데 실사용을 생각하면 스쿼드 짜기가 가장 자주 쓰는 기능이라는 의견이 나왔습니다.

다시 보니 보류 이유는 UGC(사용자 글) 운영 부담이었지, 스쿼드 메이커 자체가 아니었습니다. 그래서 이렇게 정리했어요.

  • 스쿼드는 기기 로컬에만 저장 (서버 저장·공유 게시판 없음)
  • 서버는 선수 데이터만 내려준다

이러면 운영 부담이 사실상 0입니다. 하단 탭도 4개에서 5개로 늘려 04 스쿼드를 따로 뺐습니다.

처음 버전: 포지션 OVR + 급여 + 팀컬러 인원

첫 버전은 단순했습니다.

  • 포메이션 11종, 칸을 눌러 선수 검색 → 배치
  • 강화 단계 1~13 선택
  • 선발 급여 합계 (한도 310 — 26TOTS 업데이트로 300 → 310)
  • 같은 팀컬러에 속한 선발 인원 수

그런데 이 과정에서 첫 번째 "숫자가 다르다"를 만났습니다.


2. 0강과 1강 — 모든 숫자가 3씩 낮았다

데이터센터에서 선수 능력치를 가져올 때 n1Strong=0(강화 0단계)로 받아 저장하고 있었습니다. "카드 기본값"이라고 생각했거든요. 그런데 비교해 보니:

카드0강 (우리 DB)1강 (데이터센터 기본 표시)
손흥민 TK119122
엘링 홀란 26 TOTS122125
토니 프루크6770
마누엘 노이어 (GK)99102
김민재9497

전부 정확히 +3. 세부 능력치도 34종 전부 +3이었습니다.

게임과 데이터센터는 1강을 기본으로 보여 주는데, 앱의 선수정보·집중훈련 화면은 0강 값을 그대로 보여 주고 있었던 거예요. 사용자 입장에선 "앱 숫자가 게임보다 3 낮다"로 보였을 겁니다.

해결은 간단했습니다. DB는 그대로 두고, 화면에 보여 줄 때만 +3(asGrade1)을 하도록 공용 함수를 만들어 선수정보·스쿼드·집중훈련·관심 선수 목록에 모두 적용했습니다. 포지션 OVR은 가중치 합이 100이라, 능력치에 +3을 하면 OVR도 정확히 +3이 됩니다. 계산이 어긋날 일이 없어요.


3. "팀컬러까지 적용되게 해 주세요" — 공식 스쿼드 메이커에서 찾은 정답지

첫 버전은 팀컬러를 "몇 명이 모였는지"만 보여 줬습니다. 하지만 실제로 필요한 건 팀컬러 효과까지 들어간 OVR이었죠.

데이터센터 HTML을 뜯다가

처음엔 데이터센터의 팀컬러 상세 페이지 HTML을 하나씩 파싱하려 했습니다. 단계별 필요 인원(예: 토트넘 3/6/8/11명)과 효과(전체 능력치 +1/+3/+3/+4, 반응 속도 +2…)가 나오거든요.

그러다 넥슨 공식 스쿼드 메이커(/squadmaker) 페이지 스크립트를 열어 봤는데, 로그인 없이 호출하는 기본 데이터 요청 하나에 팀컬러 803개의 정의가 전부 들어 있었습니다.

{
  "id": 1002, "teamcolorname": "토트넘 홋스퍼",
  "category": "affiliation",
  "min_nums": { "1": 3, "2": 6, "3": 8, "4": 11 },
  "boosts": {
    "3": [ { "type": "ovr", "value": 3 },
           { "type": "reactions", "value": 2 },
           { "type": "positioning", "value": 1 } ]
  }
}
  • category: 소속(클럽·국가·클래스) / 특성(특정 선수 조합) / 강화(N강 이상)
  • type: "ovr" = 전체 능력치, 나머지는 세부 능력치 키

강화 팀컬러도 여기서 알게 됐습니다.

강화 팀컬러조건5명8명
동빛 물결3강 이상+1—
은빛 물결5강 이상+1+3
금빛 물결8강 이상+3+4
백금빛 물결11강 이상+4+5

계산식 세우고, 데이터센터로 검증하기

이제 계산식은 이렇게 됩니다.

포지션 OVR = ⌊ Σ 가중치 × (세부 능력치 + 1강 3 + 강화 보너스 + 팀컬러 효과) / 100 ⌋

가중치는 예전에 집중훈련 계산기를 만들 때 카드 1,350장으로 도출해 둔 공식을 그대로 썼습니다.

검증은 데이터센터로 했습니다. 데이터센터 능력치 요청에는 팀컬러 ID와 단계를 넣는 파라미터가 있어서, 같은 조건으로 넣고 비교하면 됩니다.

  • 무작위 카드 30장 × 강화·팀컬러 무작위 조합 79건 → 16포지션 전부 일치
  • 소속 + 강화 + 특성 팀컬러를 동시에 걸어도 단순 합산으로 일치
  • 실제 스쿼드(케인·손흥민·김민재·이강인·황희찬·로메로·포로·매디슨, 3~13강)를 엔진에 넣고, 엔진이 고른 팀컬러를 그대로 데이터센터에 넣어 9명 전원 일치 (케인 ST 13강 158, 손흥민 RM 11강 146)

규칙은 하나 더 있었다

계산이 맞아도 "누구에게 적용되나"가 남았습니다. 공식 개발자 노트와 커뮤니티 정리를 보면:

  • 선수 1명당 소속·특성·강화 팀컬러 각 1개씩
  • 조건을 만족한 그 선수에게만 적용 (토트넘 팀컬러는 토트넘 출신 선수만 받음)
  • 소속·특성은 선수별로 고를 수 있고, 강화는 가장 좋은 효과가 자동

그래서 앱도 선수마다 "그 선수 OVR이 가장 많이 오르는 팀컬러"를 자동으로 고르고, 직접 바꾸거나 끌 수 있게 했습니다.


4. 적응도 — 최대 4가 통째로 빠져 있었다

팀컬러까지 맞췄는데도 "게임이랑 다르다"는 의심이 남았습니다. 공식 스쿼드 메이커의 OVR 계산 함수를 다시 읽다가 이 줄을 발견했어요.

return base + 강화보너스(buildUp) + (includeAdapBonus ? 적응도보너스(adap) : 0)
// 적응도보너스(a) = min(5, a) - 1   → 적응도 5면 +4
// 공식 스쿼드 메이커 기본값: adap = 5

팀 적응도. 적응도 1~5에 따라 +0~+4가 붙는데, 우리 계산엔 아예 없었습니다. 게임에서 오래 쓴 선수(적응도 5)라면 앱이 최대 4 낮게 보여 주고 있었던 거예요.

데이터센터의 n1Grow 파라미터가 바로 이 값이라는 것도 확인했습니다(값만큼 전 능력치 +1). 앱에는 선수별 적응도 1~5 선택을 넣고, 기본값은 공식과 같은 5로 했습니다.

덤으로 공식 강화표 [0,3,4,5,7,9,11,14,18,20,22,24,27,30](0강 기준)가 우리 강화 계산기 표(1강 +3 위에 1강 대비 누적)와 13단계 모두 같다는 것도 확인했습니다.


5. 포지션을 끌어서 옮기기 — 공식 구역 데이터 그대로

"포지션을 움직일 수 있게 해 달라"는 요청도 있었습니다. 공식 스쿼드 메이커 번들에는 포지션 구역 28개가 좌표로 들어 있었어요.

{ id: "lcb", left: 25, top: 80, width: 25, height: 20 }
{ id: "cb",  left: 25, top: 80, width: 50, height: 20 }   // LCB·RCB 를 포함

구역이 겹치는데, 공식 앱의 판정 규칙은 "놓은 지점이 들어간 구역 중 중심이 가장 가까운 것"이었습니다. 같은 포지션에 다른 선수가 있으면 두 선수의 자리를 맞바꾸고요. 이 규칙을 그대로 옮기고, 포메이션 11종의 모든 슬롯 좌표가 자기 포지션으로 판정되는지 테스트했습니다(불일치 0).

이제 황희찬을 LM에서 LW 구역으로 끌어 놓으면 LW로 바뀌고 OVR도 LW 기준으로 다시 계산됩니다.


6. 라이브 부스트 — "포함돼 있는데, 오래됐다"

"라이브 부스트가 적용된 스탯이 다르다"는 제보도 받았습니다. PTG(Path to Glory) 같은 카드는 실제 경기 결과에 따라 능력치가 오르거든요(국가 승리마다 +1, 최대 +5).

처음엔 "부스트가 빠져 있나?" 싶었는데, 데이터센터 선수 목록에서 답을 찾았습니다.

리오넬 메시 (PTG)   122 (118)

괄호 안이 부스트 전 기본값이고, 122가 라이브 +4가 이미 포함된 값입니다. 우리 DB도 122였어요. 문제는 다른 데 있었습니다.

  • 부스트는 경기마다 바뀌는데, 우리 데이터는 최대 25일 전에 크롤링한 값
  • 앱 어디에도 "부스트 +4 포함"이라는 표시가 없어서 확인할 방법이 없음

그래서 선수 상세나 스쿼드에서 카드를 볼 때 데이터가 하루 넘었으면 그 자리에서 다시 받도록 바꿨습니다. 라이브 값은 공식 스쿼드 메이커의 선수 정보(JSON 3.6KB)에서 받고, 상세 화면에는 "라이브 퍼포먼스 +4 포함 (부스트 전 118)"처럼 보여 줍니다.

이 김에 선수 상세 화면도 새로 만들었습니다. 원래 선수정보 탭은 "비교"만 됐거든요. 이제 선수를 누르면 능력치 34종, 강화 단계별 포지션 OVR, 급여, 특성, 팀컬러, 라이브 퍼포먼스가 한 화면에 나옵니다.


7. 아무도 몰랐던 누락 — 새 시즌 113장

"없는 시즌 카드가 있다"는 제보를 따라가 보니, 넥슨 공식 카드 목록은 88,359장인데 우리 DB는 88,246장이었습니다. 빠진 건 정확히 신규 시즌 UC(Untouchable Champions) 113장.

원인은 단순했습니다. 9월에 카드 목록을 한 번 넣은 뒤로, 자동 작업은 이미 있는 카드의 상세 정보만 다시 받고 있었어요. 새 시즌을 추가하는 단계 자체가 없었던 거죠.

이제는 매시 정각에 넥슨 카드 수만 확인하고, 달라졌을 때만 DB를 조회해 새 시즌·카드를 넣습니다. 카드 수가 같으면 DB를 아예 건드리지 않으니 무료 플랜 한도에도 부담이 없어요.

작은 함정도 하나 있었습니다. 로컬에서 테스트하니 "113장 추가"가 아니라 "2,286장 추가"라고 나왔는데, D1 batch 결과의 changes가 검색 색인(FTS) 트리거가 쓴 행까지 세고 있었습니다. 실제 행은 정확히 113장이었고요. 추가 건수는 시즌별 부족분으로 직접 계산하도록 바꿨습니다.


8. 보안 구멍 하나

신규 시즌 추가를 손으로 돌리려고 동기화 Worker 주소를 보다가 발견했습니다. /sync/all 같은 운영용 주소에 인증이 없었어요. 누구나 호출해 DB에 8.8만 행 쓰기를 일으킬 수 있었고, 무료 플랜 하루 쓰기 한도(10만)를 한 번에 다 쓸 수 있는 구조였습니다.

관리자 토큰(Bearer) 검사를 공용 모듈로 만들어 API Worker와 동기화 Worker가 같이 쓰도록 했습니다. 이제 토큰이 없으면 401, Worker에 토큰이 아예 설정되지 않았으면 503으로 잠깁니다. 정해진 시간에 도는 자동 작업(cron)은 영향이 없고요.


9. 그리고 아직 진행 중인 것 — 갱신시간 알리미

FC 온라인 이적시장은 카드마다 "갱신" 시점에 가격 범위가 바뀝니다. 이걸 알려 주는 게 원래 해피파의 핵심 기능인데, 공식 API에도 데이터센터에도 갱신 시각이나 거래량을 주는 곳이 없습니다. 그래서 기존 서비스들은 사용자 제보에 기대는 것으로 보여요.

제보 없이 자동으로 잡을 방법을 찾는 중입니다.

  • 1강 현재가 하나만 2.5시간 지켜봤더니 8장 중 1번만 바뀌었습니다. 갱신이 있어도 가격이 같으면 못 잡아요.
  • 그런데 데이터센터 "구독 선수" 기능은 요청 한 번에 한 선수의 전 카드 × 1~13강 가격을 줍니다. 메시 한 명이면 카드 56장 × 13단계 = 가격 728개요.
  • 지금 6명의 전 카드(가격 3,549개)를 5분마다 6시간째 기록하고 있습니다.

중간 결과가 흥미롭습니다. 첫 55분 동안 가격 변화는 11:42 한 번에만, 서로 다른 선수 카드 5장에서 동시에 일어났어요. 카드별로 따로 갱신되는 게 아니라 시장 전체가 주기적으로 한꺼번에 갱신되는 것일 수도 있습니다. 다음 회차에도 같은 패턴이 보이면 알리미 설계가 훨씬 단순해질 거예요. 결과는 다음 글에서 정리하겠습니다.


오늘 배운 것

  1. "숫자가 다르다"는 제보는 대부분 맞다. 0강/1강(-3), 적응도(-4), 오래된 라이브 부스트, 빠진 시즌. 각각은 작지만 다 실제로 있던 문제였습니다.
  2. 공식 도구가 가장 좋은 명세서다. 공식 스쿼드 메이커 스크립트에 팀컬러 정의, 강화표, 적응도 식, 포지션 구역이 다 있었습니다. 추측해서 만들 필요가 없었어요.
  3. 계산은 반드시 독립된 기준으로 대조한다. 데이터센터에 같은 조건을 넣어 맞춰 본 덕분에 "맞는 것 같다"가 아니라 "79건 전부 일치"라고 말할 수 있었습니다.
  4. 새 데이터가 들어오는 길도 자동이어야 한다. 기존 데이터를 갱신하는 것만으론 부족했습니다. 새로 생기는 것을 받아들이는 경로가 따로 필요했어요.

해피파는 넥슨과 무관한 비공식 팬 서비스입니다. Data based on NEXON Open API. 선수 상세 데이터는 FC 온라인 공식 데이터센터, 팀컬러·라이브 퍼포먼스 정보는 FC 온라인 공식 스쿼드 메이커의 공개 데이터를 참고했습니다.

👉 https://hafipa.pages.dev

0개의 댓글