TIL - 20260817

juni·2026년 8월 17일

TIL

목록 보기
432/468

0817 데이터베이스 실무 심화 (5/N): 상담/주문 상태 변경 이력과 Audit Log 설계


✅ 1. 상태 변경 이력이란 무엇인가?

  • 상태 변경 이력은 데이터의 상태가 언제, 누구에 의해, 어떤 값에서 어떤 값으로 바뀌었는지 기록하는 테이블입니다.
  • 상담, 주문, 개통, 배송, 알림 발송, 엑셀 ExportJob처럼 상태가 변하는 데이터에는 이력이 중요합니다.
  • 현재 상태만 저장하면 운영 중 문제가 생겼을 때 과거 흐름을 추적하기 어렵습니다.
현재 상태만 저장:
consult.status = CALLED

상태 이력 저장:
NEW → CALLING → CALLED
각 변경 시간과 관리자 기록

➕ 1-1. 상태 변경 이력이 필요한 이유

누가 상태를 바꿨는지 확인
언제 상태가 바뀌었는지 추적
잘못된 상태 변경 원인 확인
상담 처리 속도 분석
관리자 업무 이력 확인
고객 문의/분쟁 대응
장애 원인 추적
  • 상태 이력은 단순 로그가 아닙니다.
  • 운영자가 매일 처리하는 업무의 증거이자, 데이터 정합성을 지키는 장치입니다.

✅ 2. 현재 상태와 상태 이력의 차이

  • 현재 상태는 현재 row의 최종 상태입니다.
  • 상태 이력은 그 상태가 되기까지의 변경 과정입니다.
consults.status:
현재 상태

consult_status_histories:
상태 변경 과정

➕ 2-1. 예시

consults
  - id: 10
  - status: CONVERTED

consult_status_histories
  - NEW → CALLING
  - CALLING → CALLED
  - CALLED → CONVERTED

➕ 2-2. 둘 다 필요한 이유

현재 상태:
목록 조회/필터/정렬에 필요

상태 이력:
추적/감사/분석/분쟁 대응에 필요
  • 관리자 목록에서 매번 이력 테이블을 조회해 현재 상태를 계산하면 비효율적입니다.
  • 그래서 현재 상태는 원본 테이블에 두고, 변경 과정은 이력 테이블에 남기는 방식이 실무적으로 좋습니다.

✅ 3. 상태 이력을 남기지 않으면 생기는 문제

상담이 언제 처리됐는지 모름
누가 상태를 바꿨는지 모름
잘못 변경된 상태를 추적할 수 없음
관리자별 처리량을 알 수 없음
상담 지연 원인을 찾기 어려움
고객 문의 시 설명 근거 부족
배포 후 상태 변경 버그 추적 어려움

➕ 3-1. 실제 상황 예시

상황:
고객이 분명히 연락받지 못했다고 문의

DB:
consults.status = CALLED

문제:
누가 언제 CALLED로 바꿨는지 기록 없음

결과:
운영자 확인 불가
고객 응대 근거 부족

➕ 3-2. 이력이 있다면

확인 가능:
2026-08-17 14:22
관리자 A
CALLING → CALLED
변경 사유: 부재중 2회 후 문자 안내
  • 이력은 운영 신뢰도를 높입니다.
  • 특히 1인 개발자가 만든 관리자 시스템에서는 이런 기록이 있느냐 없느냐가 완성도를 크게 가릅니다.

✅ 4. 상담 상태 이력 테이블 기본 구조

consult_status_histories
  - id
  - consultId
  - fromStatus
  - toStatus
  - changedByAdminId
  - reason
  - memo
  - createdAt

➕ 4-1. 컬럼 의미

컬럼의미
id이력 고유 ID
consultId상담 ID
fromStatus변경 전 상태
toStatus변경 후 상태
changedByAdminId변경한 관리자
reason변경 사유 코드
memo변경 메모
createdAt변경 시각

➕ 4-2. 예시 데이터

consultIdfromStatustoStatuschangedByAdminIdreasoncreatedAt
10NEWCALLING1FIRST_CALL2026-08-17 10:20
10CALLINGCALLED1CALL_DONE2026-08-17 10:35
10CALLEDCONVERTED2OPENED2026-08-17 15:10
  • fromStatus와 toStatus가 있어야 상태 변경 흐름을 명확히 볼 수 있습니다.
  • changedByAdminId가 있어야 관리자 작업 추적이 가능합니다.

✅ 5. Prisma Schema 예시

model Consult {
  id        Int            @id @default(autoincrement())
  name      String
  phone     String
  status    ConsultStatus  @default(NEW)
  createdAt DateTime       @default(now())
  updatedAt DateTime       @updatedAt

  statusHistories ConsultStatusHistory[]
}

model ConsultStatusHistory {
  id               Int           @id @default(autoincrement())
  consultId         Int
  fromStatus        ConsultStatus
  toStatus          ConsultStatus
  changedByAdminId  Int?
  reason            String?
  memo              String?
  createdAt         DateTime      @default(now())

  consult           Consult       @relation(fields: [consultId], references: [id])
  changedByAdmin    AdminUser?    @relation(fields: [changedByAdminId], references: [id])

  @@index([consultId, createdAt])
  @@index([changedByAdminId, createdAt])
}

enum ConsultStatus {
  NEW
  CALLING
  CALLED
  PENDING
  CONVERTED
  CANCELED
  DUPLICATED
}

➕ 5-1. 설계 포인트

상태 enum 사용
consultId + createdAt 인덱스
관리자 기준 조회를 위한 changedByAdminId 인덱스
시스템 자동 변경을 고려해 adminId nullable 가능
  • 모든 상태 변경이 관리자에 의해 발생하는 것은 아닐 수 있습니다.
  • 중복 자동 처리, 시스템 만료 처리, 외부 webhook 반영 같은 경우 changedByAdminId가 없을 수 있습니다.

✅ 6. 상태 변경은 Transaction으로 묶기

  • 상태 변경에서는 현재 상태 업데이트와 이력 생성이 반드시 함께 일어나야 합니다.
  • 둘 중 하나만 성공하면 데이터 정합성이 깨집니다.
1. 현재 상담 조회
2. 상태 전이 가능 여부 확인
3. consults.status 업데이트
4. consult_status_histories 생성
5. audit_logs 생성
6. commit

➕ 6-1. Prisma 예시

await prisma.$transaction(async (tx) => {
  const consult = await tx.consult.findUniqueOrThrow({
    where: { id: consultId },
  });

  validateStatusTransition(consult.status, nextStatus);

  await tx.consult.update({
    where: { id: consultId },
    data: {
      status: nextStatus,
    },
  });

  await tx.consultStatusHistory.create({
    data: {
      consultId,
      fromStatus: consult.status,
      toStatus: nextStatus,
      changedByAdminId: adminId,
      reason,
      memo,
    },
  });
});

➕ 6-2. 주의할 점

상태 업데이트와 이력 생성을 따로 실행하지 않기
이력 생성 실패 시 상태 변경도 rollback
상태 변경 전 현재 상태 확인
허용되지 않는 상태 전이 차단
  • 상태 변경은 단순 update가 아닙니다.
  • 운영 데이터에서는 “상태가 바뀐 이유와 경로”가 중요합니다.

✅ 7. 상태 전이 규칙

  • 상태는 아무 방향으로나 바뀌면 안 됩니다.
  • 운영 정책에 맞게 가능한 전이와 불가능한 전이를 정해야 합니다.
NEW
  ↓
CALLING
  ↓
CALLED
  ↓
CONVERTED

NEW → CANCELED
CALLING → PENDING
PENDING → CALLING
CALLED → CANCELED

➕ 7-1. 상태 전이 규칙 예시

const allowedTransitions: Record<ConsultStatus, ConsultStatus[]> = {
  NEW: ['CALLING', 'CANCELED', 'DUPLICATED'],
  CALLING: ['CALLED', 'PENDING', 'CANCELED'],
  CALLED: ['CONVERTED', 'PENDING', 'CANCELED'],
  PENDING: ['CALLING', 'CANCELED'],
  CONVERTED: [],
  CANCELED: [],
  DUPLICATED: [],
};

➕ 7-2. 검증 함수 예시

function validateStatusTransition(
  currentStatus: ConsultStatus,
  nextStatus: ConsultStatus,
) {
  const allowed = allowedTransitions[currentStatus];

  if (!allowed.includes(nextStatus)) {
    throw new BadRequestException(
      `${currentStatus} 상태에서 ${nextStatus} 상태로 변경할 수 없습니다.`,
    );
  }
}

➕ 7-3. 실무 기준

최종 상태는 되돌릴 수 있는지 정책 필요
취소/중복 상태에서 다시 진행 가능한지 결정
관리자 권한별 허용 상태 변경 구분 가능
자동 상태 변경과 수동 상태 변경 구분
  • 상태 전이 규칙은 개발자가 임의로 정하는 게 아니라 운영 방식과 맞아야 합니다.
  • 운영자가 실제로 되돌리기 작업을 자주 한다면 예외 상태도 설계해야 합니다.

✅ 8. 상태 변경 사유 코드

  • 상태 변경 이력에는 단순 메모보다 사유 코드를 함께 두면 분석이 쉬워집니다.

➕ 8-1. 상담 상태 변경 사유 예시

FIRST_CALL:
첫 연락 시도

CALL_DONE:
통화 완료

NO_ANSWER:
부재중

CUSTOMER_DELAY:
고객 보류 요청

PRICE_MISMATCH:
가격/혜택 불일치

DUPLICATED_REQUEST:
중복 신청

CUSTOMER_CANCEL:
고객 취소

OPENED:
개통 완료

➕ 8-2. 사유 코드가 필요한 이유

부재중 비율 분석
고객 취소 사유 분석
상담 지연 원인 분석
관리자 업무 기준 통일
메모 검색 의존도 감소

➕ 8-3. 설계 방식

간단하게 시작:
reason String

규칙이 많아지면:
enum 또는 status_change_reasons 테이블

운영자가 관리해야 하면:
관리자에서 사유 코드 관리
  • 처음부터 너무 복잡하게 만들 필요는 없습니다.
  • 다만 사유를 완전 자유 텍스트로만 두면 나중에 통계가 어렵습니다.

✅ 9. Audit Log란 무엇인가?

  • Audit Log는 관리자 또는 시스템이 중요한 데이터를 변경한 기록입니다.
  • 상태 변경 이력은 특정 도메인의 변경 이력이고, Audit Log는 전체 시스템의 주요 작업 기록입니다.
상태 변경 이력:
상담 상태가 어떻게 바뀌었는지

Audit Log:
관리자가 시스템에서 어떤 중요한 작업을 했는지

➕ 9-1. Audit Log가 필요한 작업

상담 상태 변경
상담 메모 수정
상품 등록/수정/삭제
지원금/가격 변경
배너 노출 변경
관리자 계정 생성/비활성화
권한 변경
엑셀 다운로드
알림톡 재발송
운영 환경 설정 변경
  • Audit Log는 “운영자가 한 중요한 작업”을 추적하는 데 목적이 있습니다.
  • 실수나 문제 발생 시 책임 추궁보다 원인 확인과 복구를 위한 기록입니다.

✅ 10. 상태 이력과 Audit Log의 차이

구분상태 이력Audit Log
목적상태 흐름 추적관리자 작업 추적
대상상담, 주문, 작업 상태시스템 주요 변경
예시NEW → CALLED상품 가격 수정
조회 위치상담 상세 이력관리자 활동 로그
구조도메인별 테이블공통 로그 테이블

➕ 10-1. 둘 다 남기는 경우

관리자가 상담 상태 변경

상태 이력:
consult_status_histories에 NEW → CALLED 기록

Audit Log:
audit_logs에 CONSULT_STATUS_UPDATE 기록

➕ 10-2. 왜 중복처럼 보여도 둘 다 필요할까?

상태 이력:
상담 상세에서 상태 흐름을 보기 좋음

Audit Log:
관리자별 전체 작업을 보기 좋음
  • 목적이 다르면 같은 사건도 다른 형태로 기록할 수 있습니다.
  • 다만 너무 많은 중복을 만들지 않도록 기준을 정해야 합니다.

✅ 11. Audit Log 기본 테이블 구조

audit_logs
  - id
  - actorType
  - actorId
  - action
  - targetType
  - targetId
  - beforeValue
  - afterValue
  - ipAddress
  - userAgent
  - requestId
  - createdAt

➕ 11-1. 컬럼 의미

컬럼의미
actorType작업 주체 유형
actorId작업 주체 ID
action수행한 작업
targetType대상 리소스 유형
targetId대상 리소스 ID
beforeValue변경 전 값
afterValue변경 후 값
ipAddress요청 IP
userAgent브라우저/클라이언트 정보
requestId요청 추적 ID
createdAt발생 시각

➕ 11-2. actorType 예시

ADMIN:
관리자

SYSTEM:
시스템 자동 처리

USER:
고객 또는 일반 사용자

WORKER:
백그라운드 작업자

➕ 11-3. targetType 예시

CONSULT
PRODUCT
PRODUCT_OPTION
ADMIN_USER
BANNER
EXPORT_JOB
ALIMTALK

✅ 12. Prisma Audit Log 예시

model AuditLog {
  id          Int        @id @default(autoincrement())
  actorType   ActorType
  actorId     Int?
  action      String
  targetType  String
  targetId    String?
  beforeValue Json?
  afterValue  Json?
  ipAddress   String?
  userAgent   String?
  requestId   String?
  createdAt   DateTime   @default(now())

  @@index([actorType, actorId, createdAt])
  @@index([targetType, targetId, createdAt])
  @@index([action, createdAt])
  @@index([requestId])
}

enum ActorType {
  ADMIN
  SYSTEM
  USER
  WORKER
}

➕ 12-1. 설계 포인트

여러 도메인을 기록하므로 targetType은 유연하게
beforeValue/afterValue는 Json으로 저장 가능
actorId는 시스템 작업 때문에 nullable 가능
검색을 위해 action, target, actor 기준 인덱스 필요
  • Audit Log는 공통 테이블로 설계하는 경우가 많습니다.
  • 단, 개인정보가 들어가지 않게 before/after 값을 선별해야 합니다.

✅ 13. Audit Log의 action 설계

  • action은 나중에 검색과 필터에 쓰이므로 명확해야 합니다.
  • 단순히 UPDATE, DELETE만 넣으면 어떤 업무인지 알기 어렵습니다.

➕ 13-1. 좋은 action 예시

CONSULT_STATUS_UPDATE
CONSULT_MEMO_UPDATE
PRODUCT_CREATE
PRODUCT_UPDATE
PRODUCT_DELETE
PRODUCT_OPTION_PRICE_UPDATE
BANNER_VISIBILITY_UPDATE
ADMIN_USER_CREATE
ADMIN_USER_DISABLE
ADMIN_PERMISSION_UPDATE
EXPORT_DOWNLOAD_REQUEST
ALIMTALK_RESEND

➕ 13-2. 나쁜 action 예시

UPDATE
CHANGE
DONE
CLICK
OK
수정
처리
기타

➕ 13-3. 기준

도메인 + 작업 형태로 작성
검색하기 쉽게 대문자 snake case 사용
비즈니스 의미가 드러나야 함
너무 세분화하지 않되 모호하지 않게
  • action 이름은 미래의 검색 키워드입니다.
  • 나중에 “상품 가격 변경 누가 했어?”를 찾을 수 있어야 합니다.

✅ 14. beforeValue / afterValue 설계

  • Audit Log에는 변경 전과 변경 후 값을 남길 수 있습니다.
  • 다만 전체 row를 무조건 저장하면 개인정보와 불필요한 데이터가 많이 쌓입니다.

➕ 14-1. 좋은 예시

{
  "beforeValue": {
    "status": "CALLING"
  },
  "afterValue": {
    "status": "CALLED"
  }
}

상품 가격 변경:

{
  "beforeValue": {
    "price": 1200000,
    "subsidy": 450000
  },
  "afterValue": {
    "price": 1180000,
    "subsidy": 500000
  }
}

➕ 14-2. 피해야 할 예시

{
  "beforeValue": {
    "customerName": "김고객",
    "phone": "01012345678",
    "memo": "고객 개인정보가 포함된 긴 상담 메모",
    "accessToken": "..."
  }
}

➕ 14-3. 기준

변경된 필드만 저장
개인정보는 마스킹 또는 제외
토큰/Secret은 절대 저장 금지
긴 memo 전체 저장 주의
필요한 경우 memo 변경 여부만 기록
  • Audit Log는 추적용이지 개인정보 백업 테이블이 아닙니다.
  • 무엇을 남길지 선별하는 것이 중요합니다.

✅ 15. 개인정보와 Audit Log

  • Audit Log는 강력하지만 개인정보를 잘못 저장하면 보안 위험이 커집니다.
  • 상담 서비스에서는 이름, 전화번호, 상담 메모, IP, userAgent 등을 조심해야 합니다.

➕ 15-1. 남기면 위험한 값

전화번호 원본
고객 이름 원본
상담 메모 전체
신분증/주소/계좌번호
토큰/쿠키
Authorization header
DATABASE_URL
외부 API Secret

➕ 15-2. 마스킹 기준

전화번호:
010****1234

이름:
김*객 또는 저장하지 않음

IP:
필요 시 일부 마스킹 또는 접근권한 제한

memo:
전체 저장보다 변경 여부/요약만 저장

➕ 15-3. 실무 기준

운영 추적에 필요한 최소값만 저장
개인정보 원본은 원본 테이블에서 권한 통제
Audit Log에는 변경 사실 중심으로 기록
로그 접근 권한 제한
  • Audit Log는 오래 보관될 수 있습니다.
  • 그래서 원본 데이터보다 더 보수적으로 개인정보를 다뤄야 합니다.

✅ 16. 상태 이력과 Audit Log를 함께 저장하는 Transaction

  • 중요한 변경 작업에서는 원본 변경, 상태 이력, Audit Log를 하나의 transaction으로 묶을 수 있습니다.
Transaction 시작
  ↓
consults.status 업데이트
  ↓
consult_status_histories 생성
  ↓
audit_logs 생성
  ↓
Commit

➕ 16-1. Prisma 예시

await prisma.$transaction(async (tx) => {
  const consult = await tx.consult.findUniqueOrThrow({
    where: { id: consultId },
  });

  validateStatusTransition(consult.status, nextStatus);

  const updatedConsult = await tx.consult.update({
    where: { id: consultId },
    data: {
      status: nextStatus,
    },
  });

  await tx.consultStatusHistory.create({
    data: {
      consultId,
      fromStatus: consult.status,
      toStatus: nextStatus,
      changedByAdminId: adminId,
      reason,
      memo,
    },
  });

  await tx.auditLog.create({
    data: {
      actorType: 'ADMIN',
      actorId: adminId,
      action: 'CONSULT_STATUS_UPDATE',
      targetType: 'CONSULT',
      targetId: String(consultId),
      beforeValue: {
        status: consult.status,
      },
      afterValue: {
        status: updatedConsult.status,
      },
      ipAddress,
      userAgent,
      requestId,
    },
  });
});

➕ 16-2. 주의

Audit Log 실패 시 원본 변경도 실패시킬지 정책 필요
중요 변경은 함께 rollback 권장
분석용 로그는 transaction 밖으로 분리 가능
before/after에 개인정보 넣지 않기
  • 중요한 관리자 변경의 근거가 되는 로그라면 transaction 안에 넣는 것이 좋습니다.
  • 단순 분석 이벤트라면 비동기 로그로 분리할 수 있습니다.

✅ 17. 주문 상태 이력 설계

  • 상담보다 주문은 상태 정합성이 더 중요할 수 있습니다.
  • 주문 상태는 고객 응대, 개통 처리, 배송, 정산과 연결될 수 있기 때문입니다.

➕ 17-1. 주문 상태 예시

RECEIVED:
주문 접수

VERIFYING:
정보 확인 중

APPROVED:
승인 완료

OPENING:
개통 진행 중

OPENED:
개통 완료

SHIPPING:
배송 중

DONE:
완료

CANCELED:
취소

FAILED:
실패

➕ 17-2. 주문 상태 이력 테이블

order_status_histories
  - id
  - orderId
  - fromStatus
  - toStatus
  - changedByAdminId
  - reason
  - memo
  - createdAt

➕ 17-3. 상담보다 더 조심할 점

개통/배송/정산과 연결 가능
외부 시스템 상태와 불일치 가능
상태 되돌리기 정책 중요
고객 안내 메시지 발송과 연결 가능
  • 주문 상태는 단순 관리자 표시값이 아닐 수 있습니다.
  • 외부 시스템이나 고객 안내와 연결된다면 상태 변경을 더 엄격히 관리해야 합니다.

✅ 18. ExportJob 상태 이력

  • 엑셀 다운로드나 대량 작업을 Queue/Worker로 처리한다면 Job 상태 이력도 유용합니다.
  • 특히 실패 원인을 추적하기 좋습니다.

➕ 18-1. ExportJob 상태 예시

PENDING:
작업 대기

PROCESSING:
작업 중

DONE:
완료

FAILED:
실패

EXPIRED:
다운로드 만료

➕ 18-2. 테이블 구조

export_jobs
  - id
  - status
  - requestedByAdminId
  - fileUrl
  - errorMessage
  - createdAt
  - updatedAt

export_job_status_histories
  - id
  - exportJobId
  - fromStatus
  - toStatus
  - message
  - createdAt

➕ 18-3. 필요한 이유

작업 실패 원인 추적
Worker 재시도 이력 확인
대량 다운로드 요청 기록
관리자 문의 대응
  • 처음에는 export_jobs.status와 errorMessage만으로 시작해도 됩니다.
  • 작업 실패가 잦거나 운영자가 확인해야 한다면 이력 테이블을 추가하면 좋습니다.

✅ 19. 알림톡/SMS 발송 이력

  • 알림톡이나 SMS는 외부 API와 연결되기 때문에 발송 이력을 남기는 것이 좋습니다.
  • 발송 성공/실패, 재시도, 수신 대상, 템플릿 등을 기록해야 합니다.

➕ 19-1. 발송 이력 테이블

notification_logs
  - id
  - targetType
  - targetId
  - channel
  - templateCode
  - recipientMasked
  - status
  - providerMessageId
  - errorCode
  - errorMessage
  - requestedAt
  - sentAt

➕ 19-2. status 예시

PENDING
SENT
FAILED
RETRYING
CANCELED

➕ 19-3. 주의

수신 전화번호 원본 저장 주의
외부 API 응답에 개인정보 포함 여부 확인
실패 사유는 남기되 Secret은 제외
재시도 횟수 관리
  • 알림 발송은 성공보다 실패 추적이 중요합니다.
  • 상담 신청 저장과 알림 발송은 분리하되, 발송 상태는 추적 가능해야 합니다.

✅ 20. 관리자 화면에서 이력을 어떻게 보여줄까?

  • 이력은 DB에만 있어서는 부족합니다.
  • 운영자가 확인할 수 있는 화면으로 제공해야 의미가 있습니다.

➕ 20-1. 상담 상세 이력 UI

상담 상세 모달
  ├─ 기본 정보
  ├─ 상담 메모
  ├─ 상태 변경
  └─ 이력 탭
       - 10:20 신규 → 연락 중 / 관리자 A
       - 10:35 연락 중 → 연락 완료 / 관리자 A
       - 15:10 연락 완료 → 개통 완료 / 관리자 B

➕ 20-2. 관리자 활동 로그 UI

관리자 활동 로그
  - 관리자
  - 작업 유형
  - 대상
  - 변경 전/후
  - 시간
  - IP

➕ 20-3. 우선순위

1순위:
상담 상세에서 상태 이력 보기

2순위:
관리자별 작업 로그 보기

3순위:
상품/지원금 변경 이력 보기

4순위:
엑셀 다운로드/알림 재발송 기록 보기
  • 처음부터 복잡한 로그 관리자 화면이 필요하지는 않습니다.
  • 상담 상세 안에서 상태 변경 이력만 보여줘도 운영 품질이 크게 좋아집니다.

✅ 21. 이력 조회 성능과 인덱스

  • 이력 테이블은 시간이 지날수록 계속 쌓입니다.
  • 조회 패턴에 맞게 인덱스를 준비해야 합니다.

➕ 21-1. 상태 이력 인덱스 후보

@@index([consultId, createdAt])
@@index([changedByAdminId, createdAt])
@@index([toStatus, createdAt])

➕ 21-2. Audit Log 인덱스 후보

@@index([actorType, actorId, createdAt])
@@index([targetType, targetId, createdAt])
@@index([action, createdAt])
@@index([requestId])

➕ 21-3. 조회 패턴 기준

상담 상세에서 해당 상담 이력 조회:
consultId + createdAt

관리자별 작업 조회:
actorId + createdAt

특정 대상의 변경 로그 조회:
targetType + targetId + createdAt

장애 분석:
requestId
  • 인덱스는 “자주 조회하는 조건” 기준으로 잡아야 합니다.
  • 이력 테이블은 insert가 많기 때문에 인덱스를 너무 많이 만들면 쓰기 비용이 증가합니다.

✅ 22. 이력 데이터 보존 정책

  • 이력 데이터는 계속 쌓이기 때문에 보존 정책이 필요합니다.
  • 다만 상담/주문/관리자 변경 이력은 운영상 중요한 데이터이므로 쉽게 삭제하면 안 됩니다.

➕ 22-1. 보존 정책 후보

상담 상태 이력:
장기 보관

주문 상태 이력:
장기 보관

Audit Log:
중요 작업은 장기 보관

Access Log:
일정 기간 후 삭제 가능

Debug Log:
짧게 보관

알림 발송 로그:
운영/분쟁 기준에 따라 보관 기간 설정

➕ 22-2. 삭제 전 확인할 것

고객 응대에 필요한가?
정산/계약/분쟁 대응에 필요한가?
법적/회사 정책상 보관 기간이 있는가?
개인정보 포함 여부는 어떤가?
삭제 후 복구 가능한가?
  • 모든 로그를 영원히 보관하는 것도 답은 아닙니다.
  • 하지만 상태 이력과 Audit Log는 운영 근거가 되므로 보수적으로 다뤄야 합니다.

✅ 23. 상태 이력과 정합성 점검 쿼리

  • 현재 상태와 마지막 상태 이력이 일치하는지 점검할 수 있어야 합니다.
  • 이런 쿼리는 장애 대응이나 데이터 검증에 도움이 됩니다.

➕ 23-1. 개념

consults.status
  =
해당 consult의 마지막 consult_status_histories.toStatus

➕ 23-2. SQL 예시

SELECT c.id, c.status, h.to_status AS last_history_status
FROM consults c
LEFT JOIN LATERAL (
  SELECT to_status
  FROM consult_status_histories h
  WHERE h.consult_id = c.id
  ORDER BY h.created_at DESC
  LIMIT 1
) h ON true
WHERE h.to_status IS NOT NULL
  AND c.status <> h.to_status;

➕ 23-3. 점검 대상

현재 상태와 마지막 이력 불일치
이력 없는 상태 변경 row
존재하지 않는 관리자 ID
존재하지 않는 상담 ID
비정상 상태 전이
  • 정합성 점검 쿼리는 운영 안정성의 좋은 방어선입니다.
  • 주기적으로 확인하거나 장애 후 확인하면 좋습니다.

✅ 24. 실무 체크리스트

➕ 24-1. 상태 이력 체크리스트

  1. 현재 상태 컬럼이 원본 테이블에 있는가?
  2. 상태 변경 이력 테이블이 있는가?
  3. fromStatus, toStatus를 모두 저장하는가?
  4. 변경한 관리자 ID를 저장하는가?
  5. 시스템 자동 변경도 표현 가능한가?
  6. 상태 변경 사유와 메모가 필요한가?
  7. 상태 변경과 이력 저장이 transaction으로 묶여 있는가?
  8. 상태 전이 규칙이 있는가?

➕ 24-2. Audit Log 체크리스트

  1. 관리자 주요 작업이 Audit Log 대상인지 정리했는가?
  2. action 이름이 명확한가?
  3. targetType/targetId가 저장되는가?
  4. actorType/actorId가 저장되는가?
  5. beforeValue/afterValue에 변경된 필드만 저장하는가?
  6. 개인정보와 Secret이 저장되지 않는가?
  7. requestId가 저장되는가?
  8. 조회 패턴에 맞는 인덱스가 있는가?

➕ 24-3. 관리자 화면 체크리스트

  1. 상담 상세에서 상태 변경 이력을 볼 수 있는가?
  2. 상태 변경 시간과 관리자를 볼 수 있는가?
  3. 변경 사유와 메모를 볼 수 있는가?
  4. 관리자별 작업 로그가 필요한가?
  5. 상품/지원금 변경 이력을 볼 수 있는가?
  6. 엑셀 다운로드 이력이 필요한가?
  7. 알림톡 재발송 이력이 필요한가?
  8. 개인정보 노출 범위가 제한되어 있는가?

➕ 24-4. 정합성 체크리스트

  1. 현재 상태와 마지막 이력이 일치하는가?
  2. 상태 변경 실패 시 전체 rollback되는가?
  3. 이력 없는 상태 변경이 발생하지 않는가?
  4. 잘못된 상태 전이가 차단되는가?
  5. 동시에 상태 변경 시 충돌 처리가 되는가?
  6. Audit Log 실패 시 정책이 정해져 있는가?
  7. 정합성 점검 쿼리가 있는가?
  8. 장애 후 이력 데이터를 확인할 수 있는가?

✅ 25. AI에게 상태 이력/Audit Log 설계를 점검시킬 때 좋은 질문법

NestJS + Prisma + PostgreSQL 기반 온라인 휴대폰 판매몰에서 상담/주문 상태 변경 이력과 Audit Log를 설계하려고 해.

서비스 상황:
1. 고객은 상품 상세에서 상담 신청을 함
2. 관리자는 상담 목록에서 상태를 변경함
3. 상담 상태는 NEW, CALLING, CALLED, PENDING, CONVERTED, CANCELED, DUPLICATED가 있음
4. 상태 변경 시 누가, 언제, 어떤 상태에서 어떤 상태로 바꿨는지 남기고 싶음
5. 상담 상세 모달에서 상태 변경 이력을 보여주고 싶음
6. 상품 가격/지원금 변경, 엑셀 다운로드, 관리자 권한 변경 같은 주요 작업도 Audit Log로 남기고 싶음
7. 개인정보와 Secret은 로그에 남기면 안 됨
8. 상태 변경과 이력 저장은 transaction으로 묶고 싶음
9. 관리자 여러 명이 동시에 상태를 바꿀 수 있음

요청:
- 상담 상태 이력 테이블 설계
- 주문 상태 이력 테이블 설계
- Audit Log 테이블 설계
- Prisma Schema 예시
- 상태 전이 규칙 예시
- Transaction 처리 예시
- beforeValue/afterValue 저장 기준
- 개인정보 마스킹 기준
- 관리자 화면에서 이력을 보여주는 UI 구조
- 조회 성능을 위한 index 후보
- 정합성 점검 SQL 후보
를 실무 기준으로 정리해줘.

➕ 25-1. AI 답변 검증 기준

현재 상태와 상태 이력을 구분하는가?
상태 변경과 이력 저장을 transaction으로 묶는가?
fromStatus/toStatus를 모두 저장하는가?
changedByAdminId와 시스템 변경을 고려하는가?
Audit Log와 상태 이력의 목적 차이를 설명하는가?
beforeValue/afterValue에 개인정보를 무분별하게 넣지 않는가?
관리자 화면에서 실제로 볼 수 있는 구조를 제안하는가?
정합성 점검과 인덱스까지 고려하는가?

📌 요약

  • 상태 변경 이력은 데이터가 언제, 누구에 의해, 어떤 상태에서 어떤 상태로 바뀌었는지 기록하는 구조입니다.
  • 현재 상태는 consults.status처럼 원본 테이블에 두고, 변경 과정은 consult_status_histories 같은 이력 테이블에 따로 저장하는 방식이 실무적으로 좋습니다.
  • 상태 변경 이력이 없으면 상담 처리 시간, 관리자 작업자, 잘못된 상태 변경, 고객 문의 대응 근거를 확인하기 어렵습니다.
  • 상태 변경 시에는 consults.status 업데이트와 consult_status_histories 생성, 필요하면 audit_logs 생성을 하나의 transaction으로 묶어야 합니다.
  • 상태 전이 규칙을 두면 CANCELED → CONVERTED처럼 운영상 말이 안 되는 변경을 막을 수 있습니다.
  • 상태 변경 사유 코드를 함께 저장하면 부재중, 고객 취소, 중복 신청, 개통 완료 같은 운영 원인을 나중에 분석하기 쉽습니다.
  • Audit Log는 상태 이력보다 더 넓은 개념으로, 관리자나 시스템이 수행한 중요한 작업을 기록하는 공통 로그입니다.
  • Audit Log에는 actor, action, target, beforeValue, afterValue, requestId, createdAt 등을 저장할 수 있지만, 개인정보와 Secret은 반드시 제외하거나 마스킹해야 합니다.
  • 상담 상세에서는 상태 변경 이력을 먼저 보여주고, 필요하면 관리자별 작업 로그나 상품/지원금 변경 이력을 별도 화면으로 확장하는 것이 좋습니다.
  • 이력 테이블은 계속 쌓이므로 consultId + createdAt, actorId + createdAt, targetType + targetId + createdAt 같은 조회 패턴 기반 인덱스를 준비해야 합니다.
  • 현재 상태와 마지막 이력이 일치하는지 확인하는 정합성 점검 쿼리를 만들어두면 운영 장애나 데이터 오류를 추적하는 데 도움이 됩니다.

0개의 댓글