직원 연말 평가 시스템 개발 기록 (3) 배분율 100%에서 팀 평균 80점으로, 점수 계산 바로잡기

점수 계산이 가장 많이 바뀌었다 평가 시스템에서 가장 여러 번 갈아엎은 부분은 월간 점수 계산이었다. 처음 설계는 직관적이었다. 한 달치 매출·고객·프로세스 점수를 팀원에게 "나눠 준다". 관점별로 팀 합계가 100%를 넘지 않게 하고, 화면과 서버 양쪽에서 검사한다. 합계 막대가 입력 즉시 갱신되고, 넘으면 빨간색이 된다. 이 설계는 하루를 넘기지 못했다. 같은 날 체크인 근거를 붙이고, 자동 계산을 넣고, 결국 점수 체계 자체를 바꿨다. 이 글은 그 과정과, 연말 평가 엑셀과 같은 숫자를 내기 위해 계산을 맞춰 간 이야기다. 요구사항 팀장이 매달 무엇을 보고 점수를 줬는지 같은 화면에서 보여야 한다. 계산은 기존 평가 엑셀과 같아야 한다. 연말에 엑셀로 계산하던 결과와 시스템 결과가 달라서는 안 된다. 자동 계산이 있어도 팀장이 직접 조정할 수 있어야 하고, 조정한 값이 다음 자동 계산에 지워지면 안 된다. 매출 부서는 실제 매출 실적이 점수에 반영돼야 한다. 시행착오 체크인과 평가를 두 화면에 두었더니 같은 일을 두 번 했다 처음에는 월간 체크인(KR별 실적·신호등·코멘트) 화면과 월간 평가 배분 화면이 따로 있었다. 팀장은 체크인을 입력하고, 다른 화면으로 가서 그걸 보며 다시 점수를 넣어야 했다. 먼저 배분 표 아래에 그 달까지의 KR 달성률·신호등·코멘트·담당자를 함께 보여 줬다. 그리고 [체크인으로 자동 계산] 버튼을 만들었다. 관점별로 KR 비중을 담당자 수만큼 나눠 갖고, 달성률을 곱해 100점으로 환산한다. 이 방식이면 관점 합계는 그 관점의 가중 달성률이 되므로 100% 상한을 넘을 수 없다. 팀장은 자기 팀의 평가 대상에서 빠지고, 팀장이 담당하던 KR 지분은 남은 담당자에게 나뉘어 관점 합계가 그대로 유지되게 했다. 그런데 써 보니 체크인 달성률이 곧 팀장이 주는 점수였다. 두 화면을 따로 관리할 이유가 없었다. 체크인 표를 평가 화면 안에서 바로 입력할 수 있게 바꾸고, 체크인을 저장하면 어느 화면에서 저장했든 그 달 팀원 점수를 다시 계산해 넣도록 했다. 자동 적재가 팀장의 수정을 지웠다 자동 적재를 붙이자 새 문제가 생겼다. 팀장이 특정 팀원 점수를 손으로 고쳐 놓아도, 다음 체크인 저장 때 자동 계산이 덮어썼다. 점수 테이블에 is_auto 열을 추가했다. 수동 저장은 0, 자동 적재는 1이다. 자동 적재는 is_auto=1 인 행만 덮어쓴다. 화면에는 "수동 조정 N명" 배지와 [자동값으로 되돌리기] 버튼을 두었다. // 손으로 고친 행은 남긴다 $st = $pdo->prepare("SELECT sabun FROM okr_member_score WHERE ym=? AND dept=? AND is_auto=0"); $st->execute([$ym, $dept]); $manual = array_flip($st->fetchAll(PDO::FETCH_COLUMN) ?: []); foreach ($members as $m) { $sb = (string)$m['sabun']; if (isset($manual[$sb])) continue; // INSERT ... ON DUPLICATE KEY UPDATE ..., is_auto=1 } 검증은 테스트 데이터로 했다. 체크인 100%로 저장해 전원에게 자동 점수가 들어가는 것을 보고, 한 사람만 수동으로 고친 뒤 체크인을 50%로 다시 저장했다. 수동으로 고친 사람은 그대로, 나머지는 새 값으로 바뀌었고, 되돌리기를 누르면 전원이 자동값으로 돌아왔다. 확인 후 테스트 데이터는 지웠다. 배분율 100%와 팀 평균 80점은 동시에 성립할 수 없었다 여기서 근본적인 문제가 드러났다. 참고한 평가 엑셀은 "팀 평균이 기준점수 80점이 되도록 보정"하는 방식이었다. 그런데 시스템은 "관점별 팀 합계 100%"였다. 두 규칙은 동시에 성립할 수 없다. 팀원이 2명이면 평균 80점은 합계 160이다. 합계 100% 상한을 지키면 인원이 많을수록 개인 점수가 작아지고, 엑셀과 같은 숫자가 나올 수가 없었다. 월간 평가를 배분율에서 점수로 전환했다. 기여도를 먼저 계산한 뒤, "기준점수 − 팀 평균"만큼 전원에게 더해 관점별 팀 평균이 항상 80점이 되게 했다. 이때 0점 아래나 100점 위로 나가는 사람이 생기면, 그 사람을 경계값에 고정하고 남은 사람끼리 다시 나눠 평균을 맞춘다. $target = $base * $n; // 팀 합계가 이 값이면 평균이 기준점수 $fixed = []; for ($pass = 0; $pass < 3; $pass++) { $free = array_diff_key($raw, $fixed); if (!$free) break; $adj = ($target - array_sum($fixed) - array_sum($free)) / count($free); $hit = false; foreach ($free as $sb => $x) { $y = $x + $adj; if ($y < 0) { $fixed[$sb] = 0.0; $hit = true; } elseif ($y > OKR_SCORE_CAP) { $fixed[$sb] = OKR_SCORE_CAP; $hit = true; } } if (!$hit) { foreach ($free as $sb => $x) $fixed[$sb] = $x + $adj; break; } } 화면 라벨도 함께 바꿨다. (%)는 (점)으로, 개인 합계는 개인 평균으로, 팀 합계/100은 팀 평균/80으로. 서버의 저장 검사도 "팀 합계 100 이하"에서 "개인 점수 0~100"으로 바꿨다. 규칙이 바뀌었는데 라벨과 검사가 예전 규칙을 말하고 있으면, 사용자는 숫자를 믿지 못한다. 연말 평가를 엑셀과 같은 계산으로 맞추다 연말 평가는 참고 엑셀의 구조를 그대로 따랐다. 본인/팀장/최종으로 나뉘어 있던 점수 칸을 "평가점수" 한 칸으로 정리하고, 항목마다 팀원별 배분율 열을 붙였다. 계산은 다음과 같다. 구분 점수 = Σ(항목비중 × 항목 평가점수) — 엑셀의 비중 × 점수 합 개인 획득 = 구분 점수 × 개인 배분율 보정 = 80 − 팀 평균 (전원에게 같은 값을 더함) 실 반영점수 = 반올림 종합 = Σ(구분비중 × 실 반영점수) ÷ Σ구분비중 종합 점수는 채점된 구분의 비중만으로 나누게 했다. 항목이 없는 구분까지 분모에 넣으면, 그 구분이 0점처럼 작용해 점수를 깎기 때문이다. 항목비중 합이 100%가 아니면 해당 구분 카드를 빨간색으로 표시했다. 배분율 기본값은 월간 평가 배분의 연간 평균에서 가져올 수 있게 했다. 검증은 임시 점수를 한 부서에 넣고 손으로 계산한 값과 대조하는 방식이었다. 구분 점수가 매출 100, 고객 70, 프로세스 87로 손계산과 일치했고, 개인 점수와 보정값까지 맞춘 뒤 임시 데이터를 지웠다. 매출 부서는 기준점수 자체가 달라야 했다 매출 부서는 "평균 80점"이 공정하지 않았다. 목표를 크게 넘긴 달과 못 미친 달이 같은 80점이 되기 때문이다. 부서에 판매 채널을 연결하고 월 목표액을 넣으면, 판매 실적과 비교한 달성률로 그 달 매출 관점의 기준점수를 정하게 했다. 달성 점수 = 80 + (달성률 − 100) × 0.5 적중 점수 = 80 + (적중률 − 목표 적중률) × 0.5, 적중률은 팀별 1 − WMAPE 매출 점수 = 두 점수를 부서별 비중(기본 70:30)으로 가중 평균 자동 계산 함수에 관점별 기준점수를 넘기는 인자를 추가해, 매출 목표가 있는 부서는 80 대신 이 점수를 기준으로 평균을 맞춘다. 실적이 아직 없는 달은 계획만 있어 적중률이 왜곡되므로 계산에서 제외했다. 목표 적중률 기본값은 전사 평균보다 조금 높게 60%로 잡았다. 결과 팀장이 체크인을 저장하면 그 달 팀원 점수가 자동으로 쌓이고, 직접 고친 값은 유지된다. 월간 점수는 관점별 팀 평균 80점 기준으로 계산되고, 연말 평가는 평가 엑셀과 같은 계산식으로 개인 실 반영점수를 낸다. 매출 부서는 실적 달성률과 적중률이 기준점수에 반영된다. 모든 계산은 테스트 데이터를 넣어 손계산과 대조한 뒤 데이터를 지우는 방식으로 확인했다. 회고 가장 큰 실수는 참고 엑셀의 계산 규칙을 끝까지 읽기 전에 "배분율 합계 100%"라는 직관적인 규칙으로 먼저 만든 것이다. 기존 엑셀을 먼저 수식 단위로 풀어 놓고 시작했다면 하루 사이에 체계를 두 번 바꿀 일은 없었다. 자동 계산과 수동 조정이 공존하는 데이터는 처음부터 "누가 이 값을 썼는가"를 저장해야 한다. is_auto 한 열로 해결됐지만, 나중에 붙이느라 기존 행의 의미를 다시 정해야 했다. 평균을 맞추는 보정은 경계값(0, 100) 처리가 핵심이었다. 고정하고 다시 나누는 반복 횟수를 3번으로 제한했는데, 다시 한다면 수렴 조건을 명시적으로 두고 테스트 케이스를 남겨 두겠다. 점수 규칙이 바뀌면 라벨, 서버 검사, 화면 문구를 한 커밋에서 같이 바꿔야 한다. 숫자보다 라벨이 먼저 사람을 헷갈리게 한다. 기간: 2026.08 ~ 2026.09 기술: PHP, MySQL, JavaScript 관련 프로젝트: 직원 연말 평가 시스템