이력 화면을 만들어 두고 몇 달 지나 열어 봤더니 무엇이 왜 바뀌었는지 알 수 있는 줄이 하나도 없었다.
가격 변경
관리자가 수정함
12000 -> 13200
프로모션 적용
상품 정보 업데이트
Table of contents
Open Table of contents
자리마다 다르게 적고 있었다
grep -rn 으로 이력을 남기는 코드를 찾으니 여덟 군데였고 형식이 전부 달랐다.
$this->history->add($no, '가격 변경');
$this->history->add($no, '관리자가 수정함');
$this->history->add($no, "{$old} -> {$new}");
$this->history->add($no, '프로모션 적용');
값만 적은 것과 행위만 적은 것과 사유만 적은 것이 섞여 있었다. add() 가 문자열 하나만 받으니 부르는 쪽이 각자 편한 대로 채운 결과였다.
형식을 정하고 인자로 강제했다
한 문장에 들어갈 것을 먼저 정했다. 원인과 대상과 행위와 전후 값 네 가지다. 하나라도 빠지면 나중에 읽을 수 없다.
프로모션 적용으로 판매가를 변경했다 (12,000 → 13,200)
관리자 수정으로 재고를 조정했다 (15 → 20)
입점사 요청으로 판매를 중지했다 (판매중 → 판매중지)
형식만 정하고 문자열을 계속 받으면 또 제각각이 된다. entity 와 entity_no 와 field 와 old 와 new 와 reason 을 인자로 나눠 받게 바꿨다. manager_id 와 reg_date 는 함수 안에서 채운다.
public function add($entity, $entityNo, $field, $old, $new, $reason)
{
$label = self::label($field);
$msg = sprintf('%s로 %s을(를) 변경했다 (%s → %s)',
$reason, $label, self::fmt($field, $old), self::fmt($field, $new));
$this->db->insert('history', [
'entity' => $entity,
'entity_no' => $entityNo,
'field' => $field,
'old_value' => $old,
'new_value' => $new,
'reason' => $reason,
'message' => $msg,
'manager_id'=> $this->auth->id(),
'reg_date' => date('Y-m-d H:i:s'),
]);
}
sprintf 로 만든 message 만 history 에 넣지 않았다. old_value 와 new_value 도 컬럼으로 따로 담았다. 문장만 있으면 GROUP BY field 로 셀 수 없다.
reason 은 기본값 없는 필수 인자로 뒀다. 비었으면 insert 전에 오류가 나게 했다. 사유를 못 적을 상황이면 그건 이력에 남길 일이 아니거나 사유를 정해야 하는 상황이다. 자동 처리는 사람이 적을 수 없으니 const 로 고정 사유를 뒀다. REASON_SYNC 와 REASON_BATCH 와 REASON_ADMIN 과 REASON_API 넷이다.
const REASON_SYNC = '자동 동기화';
const REASON_BATCH = '정기 배치';
const REASON_ADMIN = '관리자 수정';
const REASON_API = '외부 연동';
사람이 읽는 말로 바꿨다
값과 컬럼 이름이 저장된 그대로 화면에 나왔다. use_yn 이 Y 에서 N 으로 바뀌었다는 줄은 읽을 수 없다.
private static function fmt($field, $v)
{
static $maps = [
'status' => ['sale' => '판매중', 'stop' => '판매중지', 'soldout' => '품절'],
'use_yn' => ['Y' => '사용', 'N' => '미사용'],
];
if (isset($maps[$field][$v])) return $maps[$field][$v];
if (in_array($field, ['price', 'qty'], true)) return number_format($v);
return $v;
}
금액에는 number_format 으로 쉼표를 넣고 코드는 이름으로 바꾼다. fmt() 안의 maps 가 그 대응표이고 컬럼 이름도 label() 로 같은 방식을 뒀다. 대응표에 없으면 원래 이름을 그대로 쓰게 해서 새 컬럼이 생겨도 안 죽게 했다. 다만 없는 것이 나오면 log_message 로 남겼다. 표에 추가해야 할 것이 그렇게 드러난다.
안 바뀐 것과 이미 쌓인 것
수정 버튼만 눌러도 이력이 여덟 줄씩 쌓이고 있었다. 아무것도 안 바꿔도 필드 수만큼 생긴다.
foreach ($fields as $f) {
if ((string)$before[$f] === (string)$after[$f]) continue;
$this->history->add('product', $no, $f, $before[$f], $after[$f], $reason);
}
바뀐 필드만 남기게 했다. (string) 으로 맞춘 뒤 === 로 비교하는 데는 이유가 둘이다.
하나는 타입이다. === 만 쓰면 DB 에서 온 "12000" 과 코드의 12000 이 다르게 잡힌다.
다른 하나는 == 의 대가다. 그 시절 PHP 는 0 == 'abc' 를 참으로 본다. '12000' == '12000.0' 도 참이다. DECIMAL 컬럼이 12000.00 으로 오는 자리에서는 그것이 편하다. 대신 0 과 글자를 같다고 보는 것을 함께 받는다.
(string) 으로 맞추면 12000 과 "12000" 은 같다. 0 과 'abc' 는 다르다. 이걸 넣고 하루치 이력이 3분의 1로 줄었다. 줄어든 만큼이 읽을 필요가 없던 줄이었다.
이미 쌓인 4천여 건은 reason 이 비어 있고 형식도 제각각이다. old_value 가 안 남아 있어 다시 만들 수 없었다. 지우지 않고 그대로 두되 화면에서 구분해 표시했다. 불완전해도 있는 것이 없는 것보다 낫다.
정리
- 이력 문구 형식을 안 정하면 자리마다 다르게 적힌다
- 한 문장에 원인·대상·행위·전후 값이 다 들어가야 나중에 읽힌다
- 문자열을 받지 말고
field·old·new·reason으로 나눠 형식을 강제한다 history에message와 함께old_value·new_value도 저장한다- 문장만 있으면
GROUP BY field로 셀 수 없다 reason을 필수로 받고 자동 처리는REASON_BATCH같은const를 둔다- 값이 안 바뀐 필드는 남기지 않는다. 쌓이면 읽을 줄이 묻힌다
- 비교는
(string)으로 맞춘 뒤===로 한다 —==는0 == 'abc'를 참으로 본다 - 코드 값과 컬럼 이름을 사람이 읽는 말로 바꾸고 없는 것은 로그에 남긴다
- 옛 이력은 형식이 달라도 지우지 않고 화면에서 구분해 표시한다