개발팀 운영 노트
대부분 한 번 데였던 것입니다. 같은 자리에 다시 빠지지 않으려고 적어 둡니다.
이 문서는 공개되어 있습니다
키·접속 정보·개인정보·서버 주소는 여기에 적지 않습니다. 그런 것은 사내 비공개 채널에 둡니다.
시간대 — DB는 UTC, PHP는 KST
운영 서버의 데이터베이스는 UTC로, PHP는 Asia/Seoul로 돕니다. 한 쿼리 안에서 둘을 섞으면 9시간이 어긋납니다.
// ✗ MySQL NOW() / CURDATE() 는 UTC
"WHERE expires_at > NOW()"
"SET created_at = NOW()"
// ✓ PHP 상수를 쓴다
"WHERE expires_at > '".G5_TIME_YMDHIS."'"
"SET created_at = '".G5_TIME_YMDHIS."'"실제로 있었던 일:
- 문자 인증번호가 5분이 아니라 9시간 5분 동안 유효했습니다. 만료 시각은 PHP(KST)로 넣고
NOW()(UTC)로 비교해, 만료 판정이 늘 9시간 밀렸습니다 - 예약 데이터의 생성·수정 시각이 9시간 과거로 기록됐습니다
날짜 비교도 같습니다. DATE(issued_at) = CURDATE() 처럼 저장은 KST, 비교는 UTC면 오전 9시 이전 실행에서 하루가 통째로 어긋납니다.
발송 실패 목록은 배열이 아니라 객체다
문자·알림톡 SDK는 실패한 건들을 객체로 돌려줍니다. 배열처럼 읽으면 PHP 8에서 치명적 오류가 나고 스크립트 전체가 그 자리에서 멈춥니다.
// ✗ isset() 이라도 예외 없이 죽는다
if (isset($failed['to'])) ...
// ✓ 공용 헬퍼로 정규화해서 쓴다
$failedPhones = uri_stamp_failed_phone_map($e->getFailedMessageList());실제로 있었던 일: 알림톡 대상 40명 중 첫 10건만 접수 시도하고 중단됐습니다. 발송 이력조차 남지 않아 다음 날 자동 재시도도 되지 않았습니다.
부분 실패는 예외로 오지 않는다
send() 는 전량 실패일 때만 예외를 던집니다. 일부만 실패하면 정상 반환되므로, 응답의 실패 목록도 함께 확인해야 합니다. 그러지 않으면 실패한 건이 「성공」으로 기록됩니다.
스탬프는 저장하지 않는다
스탬프 개수를 컬럼에 쌓지 않고, 예약 데이터에서 매번 다시 계산합니다.
- (연락처 × 날짜)로 이용 분을 합산 → 시간 단위로 나눠 내림
- 발급 장수 =
전체 스탬프 ÷ 필요 개수−이미 발급한 장수
이 구조 덕분에 과거 데이터를 다시 수집하거나 고쳐도 결과가 스스로 맞춰지고, 같은 작업을 몇 번 돌려도 중복 발급이 생기지 않습니다. 차감·초기화 로직을 두지 않는 이유입니다.
대신 발급 근거는 발급 시점에 따로 남깁니다. 나중에 되짚을 방법이 없기 때문입니다.
상태 컬럼을 판정 근거로 쓰지 않는다
이용권의 상태(사용 가능 / 만료)는 하루 한 번 도는 크론이 뒤늦게 맞추는 값입니다. 그 사이에는 기간이 지난 이용권이 「사용 가능」으로 남아 있습니다.
실제로 있었던 일: 유효기간은 그날 23:59:59 까지인데 만료 크론이 23:59:00 에 돌아 만료 다음 날 하루 종일 「사용 가능」인 채로 남았고, 그대로 사용 처리까지 됐습니다. 게다가 유효기간 검사는 「되돌리기」 경로에만 있었고 정작 「사용」 경로에는 없었습니다.
- 판정은 항상 만료일을 직접 본다 — 사용 처리·화면 표시 모두
- 크론은 표시를 맞추는 보조 수단이지 차단 수단이 아니다
- 같은 판정이 두 군데 이상 필요하면 공용 함수로 두고, 화면과 서버가 같은 함수를 쓴다
버튼을 숨기는 것은 안내일 뿐입니다. 막는 것은 서버이고, 사용 처리 경로가 둘 이상이면(매장 앱·관리자 화면) 양쪽 모두에 있어야 합니다.
금액·비율은 스냅샷으로 남긴다
- 쿠폰 금액은 발급 시점 값을 복사
- 본사 지원 비율은 사용 처리 시점 값을 복사
이벤트 설정을 나중에 바꿔도 과거 정산액이 소급 변경되지 않습니다. 정산은 사용 완료 기준이므로 비율은 사용 시점이 맞습니다.
자동화 작업을 새로 붙일 때
정확히 그날만 보는 조건은 위험합니다.
-- ✗ 하루라도 걸러지면 그 배치는 영영 누락된다
WHERE DATE(expires_at) = 오늘 + 30일
-- ✓ 범위로 잡고, 이미 보낸 건만 제외한다
WHERE DATE(expires_at) <= 오늘 + 30일
AND DATE(expires_at) >= 오늘
AND NOT EXISTS (성공 이력)같은 이유로, 발송 대상을 「오늘 발급분」으로만 좁히면 실패한 건이 다음 날 대상에서 빠져 영영 재시도되지 않습니다. 재시도 창을 며칠 두고, 실패 상태도 대상에 포함시킵니다.
원칙: 「보낸 것을 제외」하는 방식으로 짜면 스스로 복구되고, 「오늘 것만 선택」하는 방식으로 짜면 한 번 놓친 것이 영구히 남습니다.
자동 발송 경로는 반드시 잠근다
로그인 없이 도는 경로는 외부에서 호출하면 임의 발송이 가능합니다. 내부 요청만 허용하고, 테스트 이벤트는 삼중으로 차단합니다 (이벤트 진입 · 대상 조회 · 단건 지정 경로 각각).
스키마 변경은 멱등하게
컬럼·테이블 추가는 한 곳에 모아 두고 매 실행마다 안전하게 다시 돌 수 있게 만듭니다 (존재 확인 후 추가, CREATE TABLE IF NOT EXISTS). 배포 순서에 의존하는 수동 마이그레이션을 두지 않습니다.
로그를 늘렸으면 회전도 늘린다
작업을 추가하면서 로그 파일만 늘고 회전 설정에 빠져 있던 적이 있습니다. 새 로그를 만들면 같은 커밋에서 회전 대상에 넣습니다. 날짜별로 파일이 쌓이는 상세 로그는 별도로 오래된 것을 지웁니다.