Skip to content

feat: 관리자 확정 견적·견적 요청 수동 취소 API 추가 - #114

Merged
yooseohyeon merged 6 commits into
devfrom
feature/admin-estimates-cancel
Aug 11, 2026
Merged

yooseohyeon merged 6 commits into
devfrom
feature/admin-estimates-cancel

Conversation

@yooseohyeon

@yooseohyeon yooseohyeon commented Aug 8, 2026 •

Copy link
Copy Markdown
Member

📌 작업 내용

관리자가 계정 정지 시 확정(CONFIRMED) 견적·견적 요청은 자동으로 취소되지 않습니다. 관리자가 확정 거래(CONFIRMED)를 수동으로 취소할 수 있는 API를 추가했습니다.
계정 정지는 향후 서비스 이용을 제한하는 조치이고, 확정 거래 취소는 이미 성립한 고객·기사 간 약속을 해제하는 조치이므로 분리했습니다.

✅ 변경 사항

관리자 확정 견적 수동 취소 API

  • PATCH /api/admin/estimates/:estimateId/cancel 추가
  • 견적과 견적 요청이 모두 CONFIRMED이고, 요청의 confirmedEstimateId가 대상 견적과 일치할 때만 취소 가능
  • SENT 견적은 본 API의 대상이 아니며, 고객 요청 취소 또는 계정 정지 시 견적 요청 단위로 함께 취소됨
  • 존재하지 않는 견적은 404 ESTIMATE_NOT_FOUND, 확정 거래가 아니거나 이미 종료된 거래는 409 ADMIN_ESTIMATE_CANCEL_NOT_ALLOWED 반환
  • path parameter 및 요청 body 검증 추가
  • OpenAPI 문서 추가

동시성 및 상태 정합성 처리

  • 견적 요청 행을 FOR UPDATE로 잠근 뒤 최신 상태를 재확인
  • 상태 변경은 조건부 updateMany로 처리해 동시 취소·완료 등으로 상태가 바뀐 거래를 다시 취소하지 않도록 처리

취소 시 처리

  • 견적 요청: CONFIRMED → CANCELED, isActive: false, canceledAt 기록
  • 확정 견적: CONFIRMED → CANCELED, canceledAt 기록
  • 연결된 PENDING 견적 수정 요청 취소
  • EstimateRequestHistory, ActivityLog 생성
  • 취소 대상 estimateId와 연결된 채팅방에만 SYSTEM 메시지 생성 및 lastMessageAt 갱신
  • 고객·기사에게 취소 알림 생성
  • sourceId와 복합 유니크 제약으로 재시도 시 알림 중복 생성 방지

알림

  • 취소 알림의 content를 완성 문장 대신 강조할 대상어를 전달하도록 수정
알림 타입 백엔드 content 프론트 문장
ESTIMATE_CANCELED_BY_ADMIN "확정 견적 거래" 관리자 확인으로 {content}가 취소되었습니다.
ESTIMATE_REQUEST_CANCELED_BY_ACCOUNT_SUSPENSION "견적 요청" 고객의 이용 제한으로 {content}이 취소되었습니다.
ESTIMATE_CANCELED_BY_ACCOUNT_SUSPENSION "견적" 기사의 이용 제한으로 {content}이 취소되었습니다.

프론트 NotificationType 및 notificationMessages에 아래 타입을 추가해야 합니다.

  • ESTIMATE_REQUEST_CANCELED_BY_ACCOUNT_SUSPENSION
  • ESTIMATE_CANCELED_BY_ACCOUNT_SUSPENSION
  • ESTIMATE_CANCELED_BY_ADMIN
ESTIMATE_REQUEST_CANCELED_BY_ACCOUNT_SUSPENSION:
prefix: "고객의 이용 제한으로 "
suffix: "이 취소되었습니다."

ESTIMATE_CANCELED_BY_ACCOUNT_SUSPENSION:
prefix: "기사의 이용 제한으로 "
suffix: "이 취소되었습니다."

ESTIMATE_CANCELED_BY_ADMIN:
prefix: "관리자 확인으로 "
suffix: "가 취소되었습니다."

채팅 SYSTEM 메시지 처리

  • SYSTEM 메시지는 특정 사용자의 발화가 아니므로 senderId: null, sender: null으로 반환
  • 기존 고객 계정 정지 시 생성하는 SYSTEM 메시지도 동일한 발신자 규칙 적용
  • SYSTEM 메시지 생성 시 대상 채팅방의 lastMessageAt을 갱신해 채팅 목록 최신순 정렬에 반영

Prisma schema 및 migration 변경

  • NotificationType에 ESTIMATE_CANCELED_BY_ADMIN 추가
  • SYSTEM 메시지 발신자 처리를 위해 ChatMessage.senderId를 nullable로 변경

두 변경에 대한 migration 파일이 포함되어 있습니다. npx prisma generate가 필요합니다.

🧪 테스트

  • 서버 실행 확인
  • API 동작 확인
  • DB 연동 확인
  • 로그 확인
  • 기타:npx tsc --noEmit, npm run lint

테스트 방법

  1. Prisma Client를 생성합니다.
npx prisma generate
  1. 고객(혹은 기사)가 확정한 견적에 대해 채팅방을 생성합니다.
POST /api/chats/rooms
Authorization: Bearer {customerAccessToken}
Content-Type: application/json

{
  "estimateId": {estimateId}
}
  1. 관리자 계정으로 확정 거래 취소를 요청합니다.
PATCH /api/admin/estimates/{estimateId}/cancel
Authorization: Bearer {adminAccessToken}
Content-Type: application/json

{
  "reason": "거래 상황 검토 후 관리자가 취소했습니다.",
  "internalNote": "관리자 통합 테스트"
}
  1. 아래 항목을 확인합니다.
  • 견적 요청과 확정 견적이 모두 CANCELED 처리
  • EstimateRequestHistory, ActivityLog 생성
  • PENDING 견적 수정 요청 취소
  • 채팅방 SYSTEM 메시지 및 lastMessageAt 갱신
  • 고객·기사 각각 ESTIMATE_CANCELED_BY_ADMIN 알림 생성
  1. 예외 케이스를 확인합니다.
  • 409 ADMIN_ESTIMATE_CANCEL_NOT_ALLOWED

    • 이미 취소한 확정 견적을 다시 취소한 경우
    • SENT, CANCELED, EXPIRED 상태 견적을 취소하려는 경우
    • 견적은 CONFIRMED지만, 연결된 견적 요청이 CONFIRMED가 아니거나 confirmedEstimateId가 대상 견적과 일치하지 않는 경우
  • 404 ESTIMATE_NOT_FOUND

    • 존재하지 않는 estimateId로 요청한 경우
  • 422 VALIDATION_ERROR

    • estimateId가 양의 정수가 아닌 경우
    • reason이 누락됐거나 비어 있는 경우, 또는 500자를 초과한 경우
    • internalNote가 1000자를 초과한 경우
  • 401 UNAUTHORIZED

    • Access Token이 없거나 형식·값이 유효하지 않은 경우
  • 403 FORBIDDEN

    • CUSTOMER 또는 MOVER 토큰으로 요청한 경우

📷 스크린샷 (선택)

관리자에 의해 확정 견적 취소 알림

스크린샷 2026-08-08 오후 6 07 37

🔥 체크리스트

  • 코드 컨벤션을 준수했습니다.
  • 불필요한 console.log를 추가하지 않았습니다.
  • ESLint 오류가 없습니다.
  • 변경 사항을 직접 테스트했습니다.
  • 관련 문서를 업데이트했습니다. (필요 시)

🙏 To Reviewer

  • NotificationType enum이 변경되었습니다. (ESTIMATE_CANCELED_BY_ADMIN 추가)
  • 여러 관리자가 같은 거래를 동시에 취소하거나, 취소 처리 중 거래 상태가 변경되는 경우에도 요청·견적 상태와 이력·알림이 일관되게 처리되는지 확인해주시면 감사하겠습니다.
  • SYSTEM 메시지의 senderId·sender nullable 응답 변경이 채팅 API 및 프론트 처리에 적절한지도 함께 확인 부탁드립니다.
  • NotificationType 추가 및 ChatMessage.senderId nullable 변경 migration이 포함되어 있어 적용 범위를 확인 부탁드립니다.

Summary by CodeRabbit

새 기능

  • 관리자가 확정 견적을 취소할 수 있습니다.
  • 취소 사유와 선택적 내부 메모를 입력할 수 있습니다.
  • 취소 결과가 거래·견적 요청 상태에 반영되며, 이력·알림·채팅 안내·감사 기록이 남습니다.
  • 취소할 수 없는 상태에서는 명확한 오류를 제공합니다.

개선

  • 견적 ID, 취소 사유 및 메모 입력을 검증합니다.
  • 시스템 채팅 메시지가 발신자 정보 없이 표시됩니다.

문서

  • 관리자 견적 취소 API 문서와 관련 명세를 추가·정비했습니다.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8f126b89-c8ca-45d0-a520-ae8bd256eb9c

📥 Commits

Reviewing files that changed from the base of the PR and between ba6c10f and 3c9cfa2.

📒 Files selected for processing (2)
  • src/app.ts
  • src/constants/error-code.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/constants/error-code.ts
  • src/app.ts

📝 Walkthrough

Walkthrough

관리자가 확정 견적을 취소하는 PATCH API를 추가했습니다. 인증과 입력 검증을 적용하고, 트랜잭션에서 상태, 이력, 알림, 채팅, 감사 로그를 갱신합니다. SYSTEM 메시지는 발신자 없이 저장하고 반환합니다.

Changes

관리자 확정 견적 취소

Layer / File(s) Summary
API 계약과 라우팅
src/modules/admin/estimates/estimates.type.ts, src/modules/admin/estimates/estimates.validator.ts, src/modules/admin/estimates/estimates.route.ts, src/modules/admin/estimates/estimates.controller.ts, src/app.ts, src/config/openapi.ts
PATCH /api/admin/estimates/:estimateId/cancel API를 추가했습니다. 관리자 인증, 권한, 활성 상태를 확인하고 견적 ID와 취소 사유를 검증합니다. OpenAPI 문서도 등록했습니다.
취소 트랜잭션과 부가 처리
src/modules/admin/estimates/estimates.service.ts, src/modules/admin/estimates/estimates.repository.ts, src/constants/error-code.ts, prisma/schema.prisma
확정 상태와 연결 관계를 재확인한 뒤 행 잠금과 조건부 갱신을 수행합니다. 취소 이력, ActivityLog, SYSTEM 채팅 메시지, 알림, 채팅방 시각을 저장합니다. 관리자 취소 제한 오류와 알림 유형을 추가했습니다.
SYSTEM 메시지 발신자 계약
prisma/schema.prisma, src/modules/admin/member-management/customers/customers.service.ts, src/modules/chat/chat.type.ts, src/modules/chat/chat.service.ts, src/modules/chat/chat.docs.ts
ChatMessage.senderId와 sender를 nullable로 변경했습니다. SYSTEM 메시지는 senderId와 sender를 null로 반환합니다. 사용자 계정 삭제 시 관련 메시지도 Cascade로 삭제합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 관리자
  participant adminEstimateRouter
  participant adminEstimatesController
  participant adminEstimatesService
  participant adminEstimatesRepository

  관리자->>adminEstimateRouter: PATCH /api/admin/estimates/:estimateId/cancel
  adminEstimateRouter->>adminEstimatesController: 검증된 견적 ID와 취소 본문 전달
  adminEstimatesController->>adminEstimatesService: 견적 ID, 관리자 ID, 취소 정보 전달
  adminEstimatesService->>adminEstimatesRepository: 견적 조회 및 행 잠금
  adminEstimatesService->>adminEstimatesRepository: 상태, 이력, 알림, 채팅, 감사 로그 저장
  adminEstimatesService-->>adminEstimatesController: 취소 결과 반환
  adminEstimatesController-->>관리자: HTTP 200 응답
Loading

Possibly related PRs

Suggested labels: 🏢코드리뷰

Suggested reviewers: juengseulki, karrum5692, wkdalswn11

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 관리자 확정 견적·견적 요청 수동 취소 API 추가라는 PR의 핵심 변경을 정확하고 간결하게 설명합니다.
Description check ✅ Passed 설명은 템플릿의 주요 섹션을 포함하고 변경 사항, 테스트 방법, 체크리스트와 리뷰 요청을 구체적으로 작성했습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/admin-estimates-cancel

Comment @coderabbitai help to get the list of available commands.

@juengseulki juengseulki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 PR 리뷰

👍 좋았던 점

  • 계정 정지와 확정 거래 취소를 별도 정책으로 분리한 방향이 명확합니다.
  • 취소 가능 조건을 Estimate / EstimateRequest / confirmedEstimateId까지 함께 검증하고 있습니다.
  • EstimateRequest를 FOR UPDATE로 잠근 뒤 최신 상태를 다시 조회합니다.
  • 실제 상태 변경도 조건부 updateMany로 처리해 동시 취소/완료에 대한 방어를 유지했습니다.
  • EstimateRequest와 Estimate 중 하나라도 상태 전이에 실패하면 transaction 전체가 rollback됩니다.
  • 원 거래 취소 시 PENDING 수정 요청도 함께 종료합니다.
  • EstimateRequestHistory와 ActivityLog를 모두 남겨 운영 추적이 가능합니다.
  • 고객/기사 취소 알림에 sourceId를 사용해 재시도 시 중복 생성을 방지하도록 구성했습니다.
  • NotificationType enum과 migration도 함께 추가되었습니다.
  • 입력 validator와 OpenAPI 문서도 API 추가 범위에 맞게 포함되어 있습니다.

🚨 확인이 필요한 부분

채팅 SYSTEM 메시지 처리에서 두 가지를 꼭 확인하고 싶습니다.

첫 번째는 SYSTEM 메시지의 senderId입니다.

현재 관리자 ID를 ChatMessage.senderId로 저장하고 있는데,
관리자가 해당 ChatRoom participant가 아니라면
기존 메시지 조회/Socket 응답이 sender를 CUSTOMER/MOVER participant로 가정하는 경우
발신자 정보가 깨질 수 있습니다.

기존 SYSTEM 메시지가 senderId=null 또는 별도 시스템 발신 규칙을 사용하는지 확인이 필요합니다.


To Reviewer 내용 기준으로 동시 취소와 상태 정합성을 중점적으로 확인했습니다.

EstimateRequest 행을 FOR UPDATE로 잠근 뒤
최신 견적/요청 상태를 다시 조회하고,

  • Estimate = CONFIRMED
  • EstimateRequest = CONFIRMED
  • isActive = true
  • confirmedEstimateId 일치

조건을 모두 만족하는지 검증한 뒤 취소하도록 되어 있습니다.

그 이후에도 EstimateRequest와 Estimate 각각 조건부 updateMany를 수행하고
둘 중 하나라도 count=0이면 transaction을 실패시키기 때문에
여러 관리자가 동시에 취소하거나
다른 상태 변경과 경합하는 경우의 정합성 방어는 잘 구성되어 있습니다.

이력, ActivityLog, 수정 요청 취소, 알림 생성도 같은 transaction 안에서 처리되어
본 상태 변경만 반영되고 side effect 일부만 빠지는 문제를 줄이고 있습니다.

다만 채팅 SYSTEM 메시지는 별도 확인이 필요해 보입니다.

현재 관리자 ID를 senderId로 저장하고 있는데
관리자가 ChatRoom participant가 아닌 구조라면
기존 메시지 sender 처리와 충돌할 수 있습니다.

또한 estimateRequest.chatRooms 전체에 SYSTEM 메시지를 생성하고 있어
한 요청에 여러 기사 채팅방이 존재할 경우
취소 대상이 아닌 다른 기사 채팅방에도 동일한 취소 안내가 들어갈 가능성이 있습니다.

이 부분이 현재 ChatRoom/ChatMessage 도메인 정책에 맞는지 확인 부탁드립니다.

Notification의 경우 동일 sourceId를 고객/기사 모두 사용하고 있으므로
복합 unique에 userId가 포함되어 있는지도 함께 확인하면 좋겠습니다.

두 번째는 SYSTEM 메시지를 생성하는 ChatRoom 범위입니다.

현재 estimate.estimateRequest.chatRooms 전체에 취소 메시지를 생성합니다.

견적 요청 하나에 여러 기사와의 채팅방이 존재할 수 있다면,
관리자가 확정한 특정 견적을 취소했는데
다른 SENT 견적 기사와의 채팅방까지 동일한 “확정 거래 취소” 메시지를 받게 될 수 있습니다.

관리자 취소 대상이 특정 estimateId이므로
확정 견적과 연결된 ChatRoom만 대상으로 해야 하는지 확인이 필요해 보입니다.

sourceId: notificationSourceId,
}));
const chatRoomIds = estimate.estimateRequest.chatRooms.map((room) => room.id);
const systemMessages: Prisma.ChatMessageCreateManyInput[] = chatRoomIds.map((roomId) => ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 확인이 필요합니다.

현재 관리자 취소 SYSTEM 메시지를 만들 때

{
  roomId,
  senderId: adminId,
  type: "SYSTEM",
  content: CANCELLATION_MESSAGE,
}

형태로 관리자 ID를 senderId에 저장하고 있습니다.

다만 해당 채팅방의 실제 participant는 CUSTOMER / MOVER이고,
관리자는 해당 ChatRoom 참여자가 아닐 가능성이 높습니다.

기존 메시지 조회나 Socket payload에서 senderId를 기준으로
참여자 정보나 이름/role을 조회하는 구조라면
SYSTEM 메시지에 adminId를 넣었을 때 발신자 해석이 깨질 수 있어 보입니다.

SYSTEM 메시지는 기존 도메인에서
senderId = null을 허용하거나 별도의 시스템 발신자 규칙을 사용하는지 확인 부탁드립니다.

관리자가 채팅 participant가 아닌 구조라면
현재 방식은 수정이 필요할 수 있습니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 감사합니다! 말씀해주신대로 관리자는 해당 채팅방의 참여자가 아니기 때문에, senderId = null을 허용하고 시스템 메시지의 senderId과 sender를 null으로 반환하도록 수정했습니다.

expiresAt: null,
sourceId: notificationSourceId,
}));
const chatRoomIds = estimate.estimateRequest.chatRooms.map((room) => room.id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 확인 및 제안

현재 estimateRequest.chatRooms 전체를 대상으로 SYSTEM 메시지를 생성하고 있습니다.

견적 요청 하나에 여러 ChatRoom이 생길 수 있는 구조라면
이 중 실제 확정된 estimate.id의 채팅방에만 취소 안내를 보내야 하는지,
아니면 요청에 연결된 모든 채팅방에 알려야 하는지 정책 확인이 필요해 보입니다.

현재 select는 ChatRoom id만 가져오고 있어
어떤 estimate의 채팅방인지 구분하지 않습니다.

관리자 취소 대상이 “확정된 특정 견적 거래”라면
확정 견적과 연결된 room만 대상으로 하는 것이 더 자연스러울 수 있습니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인 감사합니다! 관리자 확정 거래 취소 안내는 견적 요청의 모든 채팅방이 아닌 취소 대상 estimateId와 연결된 채팅방에만 생성되도록 수정했습니다.

@yooseohyeon
yooseohyeon force-pushed the feature/admin-estimates-cancel branch from d6570ce to 30556ea Compare August 10, 2026 06:35
@yooseohyeon
yooseohyeon requested review from Obebe-creator and removed request for karrum5692 August 10, 2026 06:45

@Obebe-creator Obebe-creator left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저 역시 SYSTEM 메시지를 특정 사용자의 발화가 아닌 자동 안내로 보고 senderId/sender를 null로 통일한 방향은 적절해 보입니다. 관리자는 채팅방 참여자가 아니기 때문에, 관리자 ID를 senderId로 저장하면 추후 FE에서 일반 참여자 메시지처럼 렌더링될 수 있다는 판단도 타당하다고 생각합니다

Notification content 컨벤션과 ChatMessage sender nullable 변경에 따른 삭제 정책 영향은 한 번 더 확인하면 좋을 것 같습니다.

또한 FE에서도 sender:null이 오면 깨질 수 있어서 일반 말풍선 로직이 아닌 따로 구현을 해야할 것 같은데 계획에 있으신지 궁금합니다

Comment thread src/modules/admin/estimates/estimates.service.ts Outdated
@yooseohyeon
yooseohyeon force-pushed the feature/admin-estimates-cancel branch from ba6c10f to 3c9cfa2 Compare August 11, 2026 00:46
@yooseohyeon
yooseohyeon merged commit 27a4df0 into dev Aug 11, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Aug 20, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants