0630 백엔드 실무 심화 (7/N): 상태값 설계와 변경 이력 관리
✅ 1. 상태값이란 무엇인가?
- 상태값(Status)은 데이터가 현재 어떤 단계에 있는지 표현하는 값입니다.
- 백엔드 실무에서는 주문, 상담 신청, 사전예약, 결제, 배송, 알림 발송, 엑셀 생성 작업처럼 여러 단계로 진행되는 데이터에 상태값을 사용합니다.
상담 신청:
PENDING → CALLING → DONE → CANCELLED
주문:
PENDING → PAID → PROCESSING → COMPLETED → CANCELLED
엑셀 Export:
PENDING → PROCESSING → COMPLETED → FAILED
- 상태값은 단순한 문자열이 아니라 업무 흐름을 표현하는 핵심 데이터입니다.
- 상태값이 엉성하면 관리자 페이지, 통계, 알림, 엑셀 다운로드, 정산, 고객 응대가 모두 꼬일 수 있습니다.
✅ 2. 상태값 설계가 중요한 이유
➕ 2-1. 운영 흐름을 명확하게 만든다
- 상담 신청이 “접수됨”인지, “상담 중”인지, “완료”인지, “취소”인지 알아야 운영자가 다음 행동을 결정할 수 있습니다.
PENDING:
새로 접수된 상담
CALLING:
상담원이 연락 중
DONE:
상담 완료
CANCELLED:
취소 또는 무효 처리
➕ 2-2. 관리자 페이지 필터 기준이 된다
- 상태값은 관리자 목록에서 필터로 자주 사용됩니다.
상담 신청 목록:
- 전체
- 신규 접수
- 상담 중
- 완료
- 취소
주문 목록:
- 결제 대기
- 결제 완료
- 처리 중
- 개통 완료
- 취소
- 상태값이 잘 설계되어 있으면 운영자가 데이터를 빠르게 찾을 수 있습니다.
➕ 2-3. 통계와 리포트 기준이 된다
오늘 신규 신청 수
상담 완료 수
취소 수
전환율
유입경로별 완료율
상품별 상담 완료율
- 상태값이 불명확하면 통계도 믿기 어려워집니다.
➕ 2-4. 자동화 조건이 된다
- 특정 상태가 되었을 때 알림을 보내거나, 일정 시간이 지나면 자동 변경하는 기능을 만들 수 있습니다.
PENDING 상태로 24시간 이상 유지
↓
관리자 알림
사전예약 상태가 OPEN
↓
신청 버튼 노출
ExportJob 상태가 COMPLETED
↓
다운로드 버튼 활성화
✅ 3. 좋은 상태값의 기준
➕ 3-1. 의미가 명확해야 한다
나쁜 예시:
WAIT
OK
NO
CHECK
TEMP
END
- 이런 상태값은 나중에 봤을 때 정확한 의미를 알기 어렵습니다.
좋은 예시:
PENDING
IN_PROGRESS
COMPLETED
CANCELLED
FAILED
EXPIRED
- 상태명만 봐도 현재 단계를 이해할 수 있어야 합니다.
➕ 3-2. 상태값 개수가 너무 많지 않아야 한다
- 상태가 너무 많으면 운영자가 헷갈립니다.
- 백엔드 로직도 복잡해지고, 통계도 어려워집니다.
좋지 않은 예시:
NEW
NEW_CHECKED
CALL_WAIT
CALL_FIRST
CALL_SECOND
CALL_THIRD
CALL_FAIL_TEMP
CALL_FAIL_FINAL
DONE_OK
DONE_BAD
CANCEL_USER
CANCEL_ADMIN
- 처음에는 큰 흐름 중심으로 단순하게 만들고, 정말 필요한 경우에만 세분화하는 것이 좋습니다.
➕ 3-3. 상태 변경 방향이 정해져 있어야 한다
- 아무 상태에서나 아무 상태로 바뀌면 데이터가 꼬입니다.
- 상태 변경에는 가능한 흐름이 있어야 합니다.
상담 신청:
PENDING → CALLING → DONE
PENDING → CANCELLED
CALLING → CANCELLED
불가능한 흐름:
DONE → PENDING
CANCELLED → CALLING
- 예외적으로 되돌리기가 필요하다면 권한과 이력을 함께 남겨야 합니다.
✅ 4. 상태값을 문자열로 관리할 때의 문제
- 상태값을 아무 문자열로 저장하면 오타와 불일치가 생길 수 있습니다.
DB에 저장된 상태:
PENDING
pending
Pendding
WAIT
WAITING
DONE
COMPLETE
COMPLETED
- 같은 의미인데 여러 값이 섞이면 필터, 통계, 엑셀 다운로드에서 문제가 됩니다.
➕ 4-1. 문제 예시
SELECT COUNT(*) FROM consults
WHERE status = 'COMPLETED';
- 만약 어떤 데이터는
DONE, 어떤 데이터는 COMPLETE로 저장되어 있다면 완료 건수가 정확히 나오지 않습니다.
✅ 5. Enum으로 상태값 관리하기
- 상태값은 가능하면 Enum으로 관리하는 것이 좋습니다.
- Prisma, TypeScript, DB에서 상태값 범위를 제한하면 오타를 줄일 수 있습니다.
➕ 5-1. Prisma Enum 예시
enum ConsultStatus {
PENDING
CALLING
DONE
CANCELLED
}
model Consult {
id Int @id @default(autoincrement())
name String
phone String
status ConsultStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
ConsultStatus에 정의된 값만 저장할 수 있습니다.
- 상태값 오타를 줄일 수 있습니다.
➕ 5-2. TypeScript Enum 예시
export enum ConsultStatus {
PENDING = 'PENDING',
CALLING = 'CALLING',
DONE = 'DONE',
CANCELLED = 'CANCELLED',
}
- 프론트엔드와 백엔드에서 같은 상태값을 공유하면 더 안전합니다.
- 가능하면 상수 파일이나 타입 패키지로 관리하는 것도 좋습니다.
✅ 6. 상태값 이름과 화면 표시명 분리
- DB에는 영어 코드값을 저장하고, 화면에는 한글 표시명을 보여주는 방식이 좋습니다.
➕ 6-1. 코드값과 표시명 예시
| DB 상태값 | 화면 표시명 |
|---|
PENDING | 신규 접수 |
CALLING | 상담 중 |
DONE | 상담 완료 |
CANCELLED | 취소 |
export const CONSULT_STATUS_LABEL: Record<ConsultStatus, string> = {
[ConsultStatus.PENDING]: '신규 접수',
[ConsultStatus.CALLING]: '상담 중',
[ConsultStatus.DONE]: '상담 완료',
[ConsultStatus.CANCELLED]: '취소',
};
- DB에 한글을 직접 저장하면 나중에 화면 문구 변경, 다국어 처리, API 연동에서 불편해질 수 있습니다.
- DB에는 안정적인 코드값을 저장하고, UI에서 표시명을 매핑하는 것이 좋습니다.
✅ 7. 상태 변경 API 설계
- 상태 변경은 단순 수정 API와 분리하는 것이 좋습니다.
- 상태 변경은 업무 흐름과 이력이 중요하기 때문입니다.
➕ 7-1. API 예시
PATCH /api/admin/consults/:id/status
PATCH /api/admin/orders/:id/status
PATCH /api/admin/reservations/:id/status
➕ 7-2. 요청 Body 예시
{
"status": "DONE",
"memo": "고객 상담 완료"
}
- 상태 변경 사유나 메모를 함께 받을 수 있습니다.
- 나중에 변경 이력에 남기기 좋습니다.
✅ 8. 상태 변경 검증
- 상태 변경 API에서는 다음을 검증해야 합니다.
➕ 8-1. 검증 항목
- 로그인한 관리자인가?
- 해당 데이터를 수정할 권한이 있는가?
- 요청한 상태값이 허용된 상태인가?
- 현재 상태에서 목표 상태로 변경 가능한가?
- 상태 변경 사유가 필요한 경우 입력했는가?
- 변경 이력을 저장할 수 있는가?
예시:
DONE 상태의 상담을 다시 PENDING으로 변경
↓
일반 관리자에게는 금지
SUPER_ADMIN만 되돌리기 가능
↓
사유 필수
↓
변경 이력 저장
✅ 9. 상태 전이 규칙
- 상태 전이(State Transition)는 어떤 상태에서 어떤 상태로 변경할 수 있는지 정한 규칙입니다.
➕ 9-1. 상담 신청 상태 전이 예시
const CONSULT_STATUS_TRANSITIONS: Record<ConsultStatus, ConsultStatus[]> = {
[ConsultStatus.PENDING]: [
ConsultStatus.CALLING,
ConsultStatus.DONE,
ConsultStatus.CANCELLED,
],
[ConsultStatus.CALLING]: [
ConsultStatus.DONE,
ConsultStatus.CANCELLED,
],
[ConsultStatus.DONE]: [],
[ConsultStatus.CANCELLED]: [],
};
DONE과 CANCELLED는 종료 상태로 보고 일반 변경을 막았습니다.
- 실제 운영에서 되돌리기가 필요하다면 별도 권한과 사유를 요구할 수 있습니다.
➕ 9-2. 상태 전이 검증 함수
function canChangeStatus(
currentStatus: ConsultStatus,
nextStatus: ConsultStatus,
) {
return CONSULT_STATUS_TRANSITIONS[currentStatus].includes(nextStatus);
}
- 상태 변경 API에서 이 함수를 사용하면 잘못된 흐름을 막을 수 있습니다.
✅ 10. 상태 변경 이력이란 무엇인가?
- 상태 변경 이력(Status History)은 데이터의 상태가 언제, 누가, 어떤 값에서 어떤 값으로 바뀌었는지 기록하는 테이블입니다.
- 상태값만 저장하면 현재 상태는 알 수 있지만, 과거 흐름은 알 수 없습니다.
현재 상태:
DONE
알 수 없는 것:
누가 변경했는지
언제 변경했는지
이전 상태가 무엇이었는지
왜 변경했는지
- 실무에서는 현재 상태와 변경 이력을 함께 관리해야 합니다.
✅ 11. 변경 이력이 필요한 이유
➕ 11-1. 운영 추적
- 누가 언제 상담 상태를 바꿨는지 알 수 있습니다.
- 고객 민원이나 내부 확인이 필요할 때 근거가 됩니다.
➕ 11-2. 관리자 책임 분리
- 여러 관리자가 같은 데이터를 처리할 때 이력이 없으면 책임 추적이 어렵습니다.
➕ 11-3. 장애와 실수 복구
- 잘못된 상태 변경이 발생했을 때 이전 상태를 확인할 수 있습니다.
➕ 11-4. 통계 분석
- 상태 변경 시간을 기준으로 처리 시간을 계산할 수 있습니다.
신규 접수 시간:
2026-06-30 10:00
상담 완료 시간:
2026-06-30 14:30
처리 소요 시간:
4시간 30분
✅ 12. 상태 변경 이력 테이블 설계
➕ 12-1. ConsultStatusHistory 모델 예시
model ConsultStatusHistory {
id Int @id @default(autoincrement())
consultId Int
adminId Int?
fromStatus ConsultStatus?
toStatus ConsultStatus
memo String?
reason String?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
consult Consult @relation(fields: [consultId], references: [id])
}
➕ 12-2. 주요 필드 설명
| 필드 | 설명 |
|---|
consultId | 어떤 상담 신청의 이력인지 |
adminId | 변경한 관리자 |
fromStatus | 이전 상태 |
toStatus | 변경 후 상태 |
memo | 관리자 메모 |
reason | 변경 사유 |
ipAddress | 요청 IP |
userAgent | 요청 브라우저 정보 |
createdAt | 변경 일시 |
- 생성 시점에는
fromStatus가 없을 수 있습니다.
- 자동 변경이라면
adminId가 없고 reason에 시스템 변경 사유를 남길 수 있습니다.
✅ 13. 상태 변경 Service 예시
async updateConsultStatus(
admin: AdminPayload,
consultId: number,
dto: UpdateConsultStatusDto,
requestMeta: {
ipAddress?: string;
userAgent?: string;
},
) {
return this.prisma.$transaction(async (tx) => {
const consult = await tx.consult.findUnique({
where: {
id: consultId,
},
});
if (!consult) {
throw new NotFoundException('상담 신청을 찾을 수 없습니다.');
}
if (!canChangeStatus(consult.status, dto.status)) {
throw new BadRequestException('변경할 수 없는 상태입니다.');
}
const updated = await tx.consult.update({
where: {
id: consultId,
},
data: {
status: dto.status,
},
});
await tx.consultStatusHistory.create({
data: {
consultId,
adminId: admin.id,
fromStatus: consult.status,
toStatus: dto.status,
memo: dto.memo,
ipAddress: requestMeta.ipAddress,
userAgent: requestMeta.userAgent,
},
});
return updated;
});
}
- 상태 변경과 이력 저장을 하나의 트랜잭션으로 묶었습니다.
- 상태만 바뀌고 이력이 빠지는 상황을 막을 수 있습니다.
✅ 14. 상태 변경과 권한
- 상태 변경은 권한과 함께 설계해야 합니다.
- 모든 관리자가 모든 상태로 변경할 수 있으면 운영 사고가 발생할 수 있습니다.
➕ 14-1. 권한별 상태 변경 예시
| 역할 | 가능한 변경 |
|---|
SUPER_ADMIN | 모든 상태 변경, 되돌리기 가능 |
ADMIN | 일반 상태 변경 가능 |
MANAGER | 본인 담당 상담의 상담 중/완료 변경 가능 |
VIEWER | 변경 불가 |
MARKETER | 변경 불가 |
➕ 14-2. Service 권한 검증 예시
if (admin.role === AdminRole.VIEWER) {
throw new ForbiddenException('상태 변경 권한이 없습니다.');
}
if (admin.role === AdminRole.MANAGER && consult.managerId !== admin.id) {
throw new ForbiddenException('본인 담당 상담만 변경할 수 있습니다.');
}
- Controller의 Guard에서 1차로 막고, Service에서 데이터 범위까지 확인하는 것이 좋습니다.
✅ 15. 상태값과 Soft Delete
- 삭제가 필요한 데이터도 실제 삭제보다 상태값이나
deletedAt으로 처리하는 경우가 많습니다.
➕ 15-1. 실제 삭제
DELETE FROM consults WHERE id = 123;
- 데이터가 완전히 사라집니다.
- 복구와 추적이 어렵습니다.
➕ 15-2. Soft Delete
deletedAt = 2026-06-30 15:20
status = CANCELLED
- 목록에서는 숨기지만 DB에는 남겨둡니다.
- 이력 추적과 복구가 가능합니다.
➕ 15-3. 실무 기준
상담 신청:
Soft Delete 또는 CANCELLED 처리 권장
상품:
판매 종료, 숨김 처리 권장
배너:
isActive=false 또는 deletedAt 사용
관리자 계정:
삭제보다 isActive=false 권장
- 운영 데이터는 실제 삭제보다 상태 변경이나 비활성화로 처리하는 것이 안전합니다.
✅ 16. 상태값과 날짜 필드
- 상태값만으로는 “언제 완료됐는지”, “언제 취소됐는지”를 빠르게 알기 어렵습니다.
- 자주 쓰는 상태 변경 시간은 별도 컬럼으로 저장할 수 있습니다.
➕ 16-1. 예시
model Consult {
id Int @id @default(autoincrement())
status ConsultStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
calledAt DateTime?
completedAt DateTime?
cancelledAt DateTime?
}
➕ 16-2. 장점
- 완료 일시 기준 검색이 빠릅니다.
- 처리 소요 시간 계산이 쉽습니다.
- 통계 쿼리가 단순해집니다.
- 관리자 화면에서 표시하기 좋습니다.
처리 소요 시간:
completedAt - createdAt
취소율 계산:
cancelledAt이 있는 데이터 기준
- 단, 날짜 필드와 이력 테이블의 값이 서로 맞도록 트랜잭션으로 관리해야 합니다.
✅ 17. 상태값과 알림
- 상태가 바뀌면 알림이 필요한 경우가 있습니다.
- 다만 상태 변경 트랜잭션 안에서 외부 알림을 직접 보내는 것은 조심해야 합니다.
➕ 17-1. 상태 변경 후 알림 예시
상담 상태 DONE 변경
↓
고객에게 완료 알림톡 발송
주문 상태 COMPLETED 변경
↓
고객에게 개통 완료 알림 발송
사전예약 상태 OPEN 변경
↓
신청 대기 고객에게 오픈 알림 발송
➕ 17-2. 권장 흐름
1. 상태 변경
2. 상태 변경 이력 저장
3. 트랜잭션 완료
4. Queue에 알림 Job 등록
5. Worker가 알림 발송
6. 알림 성공/실패 이력 저장
- 상태 변경 자체는 DB 트랜잭션으로 안전하게 처리합니다.
- 알림은 Queue로 분리해 실패와 재시도를 관리하는 것이 좋습니다.
✅ 18. 상태값과 관리자 UI
- 상태값은 백엔드만의 문제가 아닙니다.
- 관리자 UI에서도 상태값이 명확하게 보이고, 가능한 변경만 선택할 수 있어야 합니다.
➕ 18-1. UI에서 필요한 요소
- 상태 배지
- 상태 필터
- 상태 변경 드롭다운
- 변경 가능 상태만 표시
- 상태 변경 사유 입력
- 상태 변경 이력 타임라인
- 처리 시간 표시
- 완료/취소 상태에서 수정 제한
상담 상태:
[신규 접수] [상담 중] [상담 완료] [취소]
변경 이력:
2026-06-30 10:00 신규 접수
2026-06-30 11:20 상담 중 - 김관리자
2026-06-30 14:30 상담 완료 - 김관리자
- UI에서 아무 상태나 선택 가능하게 하면 실수가 늘어납니다.
- 백엔드 상태 전이 규칙과 UI 선택지를 맞추는 것이 좋습니다.
✅ 19. 상태값 설계에서 자주 하는 실수
➕ 19-1. 상태값을 화면 문구로 저장
DB 상태값:
신규 접수
상담 중
상담 완료
취소
- 화면 문구가 바뀌면 DB 값까지 바꿔야 합니다.
- API 연동이나 통계에서도 불편합니다.
DB 상태값:
PENDING
CALLING
DONE
CANCELLED
UI 표시:
신규 접수
상담 중
상담 완료
취소
➕ 19-2. 상태 변경 이력을 남기지 않음
- 현재 상태만 있으면 누가 언제 바꿨는지 알 수 없습니다.
- 운영 사고가 났을 때 원인 파악이 어렵습니다.
➕ 19-3. 상태 변경 규칙이 없음
PENDING → DONE
DONE → PENDING
CANCELLED → CALLING
DONE → CANCELLED
- 아무 상태로 변경 가능하면 데이터가 신뢰성을 잃습니다.
- 상태 전이 규칙을 코드로 관리해야 합니다.
➕ 19-4. 상태와 날짜 필드가 따로 논다
status = DONE
completedAt = null
status = PENDING
completedAt = 2026-06-30
- 상태와 날짜 필드가 맞지 않으면 통계가 틀어집니다.
- 상태 변경 시 관련 날짜도 함께 업데이트해야 합니다.
✅ 20. 실무 체크리스트
➕ 20-1. 상태값 설계 체크리스트
- 상태값 이름이 명확한가?
- DB에는 코드값을 저장하고 UI 표시명은 분리했는가?
- 상태값 개수가 과하게 많지 않은가?
- 종료 상태와 진행 상태를 구분했는가?
- 상태 전이 규칙이 정해져 있는가?
- 되돌리기 가능 여부와 권한이 정해져 있는가?
- 상태별 날짜 필드가 필요한지 검토했는가?
- 상태값이 통계와 필터 기준으로 사용 가능한가?
➕ 20-2. 상태 변경 API 체크리스트
- 상태 변경 API가 별도로 분리되어 있는가?
- 요청 상태값이 Enum으로 검증되는가?
- 현재 상태에서 변경 가능한 상태인지 확인하는가?
- 상태 변경 권한을 확인하는가?
- 본인 담당 데이터인지 확인하는가?
- 상태 변경과 이력 저장을 트랜잭션으로 묶었는가?
- 변경 사유나 메모를 저장하는가?
- 상태 변경 후 필요한 알림은 Queue로 분리했는가?
➕ 20-3. 변경 이력 체크리스트
- 이전 상태와 변경 후 상태를 모두 저장하는가?
- 변경한 관리자 ID를 저장하는가?
- 변경 사유와 메모를 저장하는가?
- IP와 User-Agent를 저장하는가?
- 자동 변경과 수동 변경을 구분할 수 있는가?
- 관리자 화면에서 이력을 확인할 수 있는가?
- 로그에 개인정보가 과도하게 저장되지 않는가?
✅ 21. AI를 활용해 상태값/이력 구조를 설계할 때 질문법
- 상태값은 업무 흐름과 직접 연결되어 있기 때문에 AI에게 요청할 때 실제 상태 흐름, 관리자 권한, 이력 필요 여부를 함께 알려줘야 합니다.
➕ 21-1. 좋은 질문 예시
NestJS + Prisma로 상담 신청 상태 관리 기능을 설계하고 싶어.
상황:
1. 상담 신청은 신규 접수, 상담 중, 상담 완료, 취소 상태가 있음
2. 관리자는 상태를 변경할 수 있음
3. MANAGER는 본인 담당 상담만 변경 가능
4. VIEWER는 조회만 가능하고 변경 불가
5. 상태 변경 시 누가 언제 어떤 상태에서 어떤 상태로 바꿨는지 이력을 남기고 싶음
6. 완료 상태가 되면 completedAt을 저장하고 싶음
7. 취소 상태가 되면 cancelledAt을 저장하고 싶음
8. 상태 변경 후 고객에게 알림톡을 보낼 수도 있음
9. 알림톡은 Queue로 분리하고 싶음
요청:
- Prisma Enum과 모델 설계
- 상태 전이 규칙
- NestJS 상태 변경 Service 예시
- 변경 이력 테이블 설계
- 권한 검증 기준
- 상태별 날짜 필드 처리
- 알림 Queue 분리 방식
을 실무 기준으로 설명해줘.
➕ 21-2. AI 답변 검증 기준
- 상태값을 문자열 아무 값으로 받지 않고 Enum으로 제한하는가?
- DB 코드값과 UI 표시명을 분리하는가?
- 상태 전이 규칙을 고려하는가?
- 상태 변경과 이력 저장을 트랜잭션으로 묶는가?
- 권한과 데이터 범위 검사를 함께 설명하는가?
- 상태별 날짜 필드를 함께 업데이트하는가?
- 외부 알림은 트랜잭션 밖 Queue로 분리하는가?
- 변경 이력에 관리자, 이전 상태, 변경 후 상태, 사유를 남기도록 하는가?
📌 요약
- 상태값은 데이터가 현재 어떤 단계에 있는지 표현하는 값이며, 주문, 상담 신청, 사전예약, 알림, 엑셀 Export 같은 기능에서 핵심 역할을 합니다.
- 상태값은 운영 흐름, 관리자 필터, 통계, 자동화 조건, 알림 발송 기준이 됩니다.
- DB에는
PENDING, DONE, CANCELLED 같은 안정적인 코드값을 저장하고, UI에서는 한글 표시명으로 매핑하는 것이 좋습니다.
- 상태값은 문자열 자유 입력보다 Enum으로 제한해 오타와 불일치를 줄여야 합니다.
- 상태 변경은 가능한 흐름, 즉 상태 전이 규칙을 정해두고 검증해야 합니다.
- 상태 변경과 변경 이력 저장은 트랜잭션으로 묶어야 데이터 정합성을 지킬 수 있습니다.
- 변경 이력에는 누가, 언제, 어떤 상태에서 어떤 상태로, 왜 변경했는지를 남기는 것이 좋습니다.
- 상태 변경 후 알림톡, SMS, 이메일 같은 외부 작업은 Queue로 분리해 실패와 재시도를 관리하는 것이 안정적입니다.