직원 연말 평가 시스템 개발 기록 (1) 왜 만들었나와 전체 구조

평가 근거가 시트와 엑셀에 흩어져 있었다 MotherK에서는 부서마다 연간 OKR과 평가기준을 구글시트로 관리하고 있었다. 연말이 되면 팀장이 평가기준 엑셀을 열어 항목별 점수를 매기고, 비중을 곱하고, 팀원별 배분율을 곱해 개인 점수를 냈다. 계산식은 엑셀 셀 안에만 있었고, 1년 동안 매달 무엇을 했는지는 또 다른 시트에 흩어져 있었다. 문제는 연결이었다. 월간 성과와 연말 평가가 서로 이어지지 않으니, 연말에 "이 사람이 왜 이 점수인가"를 물으면 여러 파일을 다시 뒤져야 했다. 부서마다 시트 양식도 달라서 한 부서의 방식을 다른 부서에 그대로 적용할 수도 없었다. 그래서 사내 그룹웨어(mkware)에 OKR·평가 모듈을 직접 만들기로 했다. 목표는 단순했다. OKR 설정부터 월간 체크인, 월간 평가, 연말 평가까지를 한 흐름으로 묶고, 점수가 어떤 근거와 계산으로 나왔는지 화면에서 바로 보이게 하는 것이었다. 요구사항과 제약 기존 시트 구조를 그대로 옮길 것. Mission, 경영목표(양적·질적), 전략 계기판, Objective, Key Result, 세부계획까지 이미 쓰던 구글시트 구조가 있었다. 새 체계를 강요하면 아무도 쓰지 않는다. 부서마다 다른 양식을 받아들일 것. 열 이름도, 블록 배치도, 병합셀 쓰는 방식도 제각각이었다. 평가 엑셀과 같은 숫자가 나올 것. 시스템이 낸 점수가 손으로 계산한 엑셀과 다르면 신뢰를 잃는다. 권한이 명확할 것. 조회는 자기 부서, 점수 부여는 관리자·C레벨·팀장만. 팀장은 자기 팀의 평가 대상에서 빠져야 했다. 기존 그룹웨어가 PHP와 MySQL 기반이라 같은 스택 안에서 페이지와 API를 추가하는 방식으로 갔다. 별도 서비스로 떼어 내면 로그인·권한·사용자 관리를 다시 만들어야 했기 때문이다. 전체 구조 모듈은 pages/okr/ 아래에 모았다. 화면 단위로 파일을 나누고, 저장·계산은 공용 API와 헬퍼로 모았다. 구성요소 역할 okr_setup Mission, 경영목표, 전략 계기판, Objective·KR·세부계획 편집 okr_monthly KR × 월 체크인(계획·실적·신호등·코멘트·근거)과 연간 진척 매트릭스 okr_member_score 월간 평가 배분. 체크인 근거와 팀원별 점수를 한 화면에 okr_eval 정량/정성 평가기준과 연말 채점, 팀원별 배분율 okr_report 인쇄·PDF용 보고서와 엑셀 내보내기 okr_import 부서별 시트 불러오기(붙여넣기·CSV·구글시트 URL) okr_sales 매출 부서의 월 목표 대비 달성률 점수 _helpers.php / api.php 권한 판정, 점수 계산, 저장 API 데이터 흐름은 이렇다. 시트 불러오기나 직접 입력으로 연간 OKR과 평가기준이 들어온다. 팀장이 매달 KR별 체크인을 저장하면, 그 달 팀원별 점수가 자동으로 다시 계산돼 월간 평가 배분 표에 쌓인다. 연말에는 평가기준 항목별 점수와 팀원별 배분율로 개인 실 반영점수를 낸다. 매출 부서는 판매 실적 데이터에서 달성률을 가져와 매출 관점의 기준점수로 쓴다. 처음 커밋은 설정·체크인·보고서·연말평가 네 화면을 한 번에 만든 것이었고, 이후 약 3주 동안 실제 부서 시트를 넣어 보면서 불러오기와 점수 계산을 계속 고쳐 나갔다. 그 두 부분은 이어지는 글에서 따로 다룬다. 이 글에서는 전체를 만들며 겪은 작은 사고들을 정리한다. 구현 중 만난 작은 사고들 alert 한 줄이 페이지 전체를 죽였다 KR 비중 검사를 추가하면서 저장 전에 경고를 띄우도록 했다. 그런데 배포 후 설정 화면에서 경영목표·전략·Objective·KR의 수정과 추가 버튼이 전부 반응하지 않았다. 원인은 PHP 파일 안에 쓴 인라인 스크립트였다. alert 문자열 안의 \n 이 출력 과정에서 실제 줄바꿈으로 들어가, 따옴표 문자열이 줄 중간에서 끊겼다. 자바스크립트는 SyntaxError 하나로 스크립트 블록 전체를 버리기 때문에, 문제와 상관없는 함수까지 하나도 정의되지 않았다. 문자열을 나눠 이어붙이는 방식으로 고치고, OKR 페이지들의 인라인 스크립트를 node --check 로 전부 검사했다. 며칠 뒤 연말 평가 페이지에서 같은 실수를 또 했다. "월간 배분 가져오기" 버튼의 alert 문구에 줄바꿈을 넣었다가 저장 함수까지 모두 정의되지 않는 상태가 됐다. 이번에는 한 줄 문장으로 바꿨다. 같은 실수를 두 번 하고 나서야, PHP 안에 인라인 스크립트를 쓸 때는 문자열에 줄바꿈을 넣지 않는다는 규칙을 스스로 정했다. 한글이 글자 한가운데서 잘렸다 전략 계기판의 실행지침은 여러 줄 텍스트였고, 화면에서는 줄 단위로 나눠 목록으로 보여 줬다. 그런데 "각 프로젝트의 명확한"이 "각 프로젝트의 (깨진 글자)"와 "확한" 두 줄로 쪼개져 나왔다. 줄을 나누던 코드는 preg_split('/\R/', ...) 였다. u 수식자가 없으면 PCRE는 바이트 단위로 동작하고, \R 은 NEL(0x85) 바이트도 줄바꿈으로 본다. 한글 UTF-8은 3바이트인데 마지막 바이트가 0x85인 글자가 있다. '명'(EB AA 85), '독'(EB 8F 85), '업'(EC 97 85) 같은 글자다. 그 글자 한복판에서 줄이 잘리고 있었다. function okr_lines($text): array { $parts = preg_split('/\R/u', (string)$text) ?: []; $out = []; foreach ($parts as $p) { $p = trim($p); if ($p !== '') $out[] = $p; } return $out; } /u 를 붙이고 빈 줄 제거까지 한 헬퍼를 만들어 설정과 보고서 화면이 같이 쓰게 했다. DB에 저장된 값은 처음부터 멀쩡했고 출력만 깨졌던 것이라 데이터를 고칠 필요는 없었다. 비중 상한을 걸었더니 고치는 것도 막혔다 한 Objective 아래 KR 비중 합은 100%여야 한다. 저장 API에서 같은 Objective의 다른 KR 합계에 입력값을 더해 100%를 넘으면 거부하도록 했다. 거부 메시지에는 넣은 값, 다른 KR 합계, 넣을 수 있는 최대치를 함께 보여 줬다. 문제는 시트 불러오기로 이미 100%를 넘긴 데이터였다. 이 상태에서는 비중을 낮추려 해도 "합계 초과"로 막혔다. 그래서 이미 초과된 경우에는 "더 키우지만 않으면" 저장을 허용하도록 바꿨다. 화면에서는 Objective 머리에 잔여·초과 비중 배지를 달고, 초과면 빨간색으로 표시했다. 카드 머리에 전체 Objective 합계(3개면 300%)를 보여 주던 것도 "비중 100% 충족 N개"로 바꿨다. 합계 300%는 사람이 보기에 의미 없는 숫자였다. 기본 부서가 있으면 생기는 오해 처음에는 부서 파라미터가 없으면 로그인한 사람의 부서로 열었다. 그러다 보니 관리자로 들어가면 개발팀 OKR이 먼저 떠서, 마치 대표 팀처럼 보였다. 기본 부서를 없애고, 부서가 지정되지 않으면 열람 권한이 있는 부서 버튼만 나열하는 선택 화면을 띄웠다. 해당 연도 OKR이 있는 부서는 파란색, 없는 부서는 "미생성"으로 구분했다. 같은 처리를 설정·체크인·보고서·연말평가·시트 불러오기 다섯 화면에 똑같이 적용했다. 결과 OKR 설정, 월간 체크인, 월간 평가 배분, 연말 평가, 보고서가 하나의 모듈 안에서 이어진다. 팀장이 체크인을 저장하면 팀원 점수가 따라 바뀌고, 연말 평가는 평가 엑셀과 같은 계산식으로 나온다. 실제 부서 시트(년간 OKR, 평가기준, 부문별 추진사항)를 불러와 운영에 들어갔고, 이후 여러 부서를 하나의 OKR 사이클로 묶어 운영하는 부서도 사원 소속을 건드리지 않고 지원하게 됐다. 회고 이 프로젝트는 계산 로직보다 "기존 업무 방식에 맞추는 일"이 훨씬 오래 걸렸다. 시트 양식, 엑셀 계산식, 팀장이 직접 고치는 습관까지 시스템이 받아들여야 했다. 인라인 스크립트는 처음부터 별도 JS 파일로 빼고 빌드 단계에서 문법 검사를 했어야 했다. 같은 SyntaxError를 두 번 낸 건 구조 문제였다. PHP에서 한글을 다루는 정규식은 습관적으로 /u 를 붙이도록 공용 헬퍼를 먼저 만들어 두는 게 맞다. 제약 조건(비중 100%)을 걸 때는 "이미 위반된 데이터를 어떻게 고칠 것인가"를 같이 설계해야 한다. 막기만 하면 고칠 길도 막힌다. 다음 글에서는 부서마다 다른 시트를 하나의 구조로 받아들이는 시트 불러오기를, 그다음 글에서는 월간·연말 점수 계산을 어떻게 바꿔 왔는지 정리한다. 기간: 2026.08 ~ 2026.09 기술: PHP, MySQL, JavaScript, Bootstrap, Google Sheets CSV export 관련 프로젝트: 직원 연말 평가 시스템