From 5f3e0f1e21a24023db138d333d5fcd4e2c4a3723 Mon Sep 17 00:00:00 2001 From: yoonsojung Date: Thu, 30 Jul 2026 15:24:57 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix:=201=20=EC=9D=B4=EC=83=81=20=EC=A0=95?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/estimate/estimate.validator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/estimate/estimate.validator.ts b/src/modules/estimate/estimate.validator.ts index e3bb49e1..c9c0ffae 100644 --- a/src/modules/estimate/estimate.validator.ts +++ b/src/modules/estimate/estimate.validator.ts @@ -68,7 +68,10 @@ export const moverEstimateRequestListQuerySchema = z.object({ //기사 견적 반려 내역 조회 export const moverEstimateRejectionListQuerySchema = z.object({ - cursor: z.string().regex(/^\d+$/, "커서는 양의 정수 형식이어야 합니다.").optional(), + cursor: z + .string() + .regex(/^[1-9]\d*$/, "커서는 1 이상의 정수여야 합니다.") + .optional(), limit: z.coerce .number("조회 개수는 숫자여야 합니다.") From 393282090c7d38ecef17d8ac98910ff410f9bed7 Mon Sep 17 00:00:00 2001 From: yoonsojung Date: Thu, 30 Jul 2026 17:55:30 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/estimate/estimate.repository.ts | 1 + src/modules/estimate/estimate.service.ts | 30 ++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/modules/estimate/estimate.repository.ts b/src/modules/estimate/estimate.repository.ts index 3153c956..052dff8f 100644 --- a/src/modules/estimate/estimate.repository.ts +++ b/src/modules/estimate/estimate.repository.ts @@ -232,6 +232,7 @@ export const moverEstimateRequestRepository = { }, }, select: { + nickname: true, serviceTypes: { select: { moveType: true, diff --git a/src/modules/estimate/estimate.service.ts b/src/modules/estimate/estimate.service.ts index ffa0dc25..f15f9c11 100644 --- a/src/modules/estimate/estimate.service.ts +++ b/src/modules/estimate/estimate.service.ts @@ -36,6 +36,11 @@ import type { */ const KST_OFFSET_MS = 9 * 60 * 60 * 1000; +const MOVE_TYPE_LABEL: Record = { + SMALL: "소형이사", + HOME: "가정이사", + OFFICE: "사무실이사", +}; // ============================================================================= // 기사: 고객의 견적 요청 목록 조회 @@ -197,7 +202,7 @@ export const moverEstimateRequestService = { //견적 제안 async sendEstimate({ estimateRequestId, moverId, input }: SendEstimateParams) { - return runTransaction(async (tx) => { + const result = await runTransaction(async (tx) => { const profile = await moverEstimateRequestRepository.findMoverProfile(moverId, tx); //기사 프로필 존재 확인 @@ -262,8 +267,8 @@ export const moverEstimateRequestService = { const isDesignated = estimateRequest.designatedMovers.length > 0; - //견적 셍성 - return moverEstimateRequestRepository.createEstimate( + //견적 생성 + const estimate = await moverEstimateRequestRepository.createEstimate( { estimateRequestId, moverId, @@ -273,7 +278,26 @@ export const moverEstimateRequestService = { }, tx, ); + + return { + estimate, + customerId: estimateRequest.customerId, + moverNickname: profile.nickname, + moveType: estimateRequest.moveType, + expiresAt: estimateRequest.expiresAt, + }; }); + + await notificationService.createNotification({ + userId: result.customerId, + type: "ESTIMATE_RECEIVED", + title: "견적 도착", + content: `${result.moverNickname} 기사님의 ${MOVE_TYPE_LABEL[result.moveType]} 견적`, + linkUrl: "/estimates/pending", + expiresAt: result.expiresAt, + }); + + return result.estimate; }, // 견적 요청 반려 From a706475e1a9d0dbe044c257652608f8a46ef53dd Mon Sep 17 00:00:00 2001 From: yoonsojung Date: Thu, 30 Jul 2026 18:44:17 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EA=B2=AC=EC=A0=81=20=EB=B0=98?= =?UTF-8?q?=EB=A0=A4=20=EC=95=8C=EB=A6=BC=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/estimate/estimate.service.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/modules/estimate/estimate.service.ts b/src/modules/estimate/estimate.service.ts index 59a2d869..42129958 100644 --- a/src/modules/estimate/estimate.service.ts +++ b/src/modules/estimate/estimate.service.ts @@ -302,7 +302,7 @@ export const moverEstimateRequestService = { // 견적 요청 반려 async rejectEstimate({ estimateRequestId, moverId, input }: RejectEstimateParams) { - return runTransaction(async (tx) => { + const result = await runTransaction(async (tx) => { //기사 프로필 const profile = await moverEstimateRequestRepository.findMoverProfile(moverId, tx); @@ -366,7 +366,7 @@ export const moverEstimateRequestService = { } //데이터 생성 - return moverEstimateRequestRepository.createEstimateRejection( + const rejection = await moverEstimateRequestRepository.createEstimateRejection( { estimateRequestId, moverId, @@ -374,7 +374,25 @@ export const moverEstimateRequestService = { }, tx, ); + + return { + rejection, + customerId: estimateRequest.customerId, + moverNickname: profile.nickname, + expiresAt: estimateRequest.expiresAt, + }; }); + + await notificationService.createNotification({ + userId: result.customerId, + type: "ESTIMATE_REQUEST_REJECTED", + title: "견적 요청 반려", + content: result.moverNickname, + linkUrl: `/estimates/requests/${String(estimateRequestId)}`, + expiresAt: result.expiresAt, + }); + + return result.rejection; }, }; From a696105cbfbee1ccbd6818b52e6e055a51e0a937 Mon Sep 17 00:00:00 2001 From: yoonsojung Date: Thu, 30 Jul 2026 18:54:40 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A1=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/estimate/estimate.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/estimate/estimate.service.ts b/src/modules/estimate/estimate.service.ts index 42129958..616f1ae0 100644 --- a/src/modules/estimate/estimate.service.ts +++ b/src/modules/estimate/estimate.service.ts @@ -293,7 +293,7 @@ export const moverEstimateRequestService = { type: "ESTIMATE_RECEIVED", title: "견적 도착", content: `${result.moverNickname} 기사님의 ${MOVE_TYPE_LABEL[result.moveType]} 견적`, - linkUrl: "/estimates/pending", + linkUrl: null, expiresAt: result.expiresAt, }); @@ -388,7 +388,7 @@ export const moverEstimateRequestService = { type: "ESTIMATE_REQUEST_REJECTED", title: "견적 요청 반려", content: result.moverNickname, - linkUrl: `/estimates/requests/${String(estimateRequestId)}`, + linkUrl: null, expiresAt: result.expiresAt, }); From fec5a6843e07c73bd8cf23866c18916d0e0e6f4f Mon Sep 17 00:00:00 2001 From: yoonsojung Date: Fri, 31 Jul 2026 11:24:48 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=EA=B2=AC=EC=A0=81=20=EB=B0=98?= =?UTF-8?q?=EB=A0=A4=20=EC=95=8C=EB=A6=BC=20=EC=A0=95=EC=B1=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../estimate/estimate.notification-policy.test.ts | 15 +++++++++++++++ .../estimate/estimate.notification-policy.ts | 6 ++++++ 2 files changed, 21 insertions(+) create mode 100644 src/modules/estimate/estimate.notification-policy.test.ts create mode 100644 src/modules/estimate/estimate.notification-policy.ts diff --git a/src/modules/estimate/estimate.notification-policy.test.ts b/src/modules/estimate/estimate.notification-policy.test.ts new file mode 100644 index 00000000..de6e7b77 --- /dev/null +++ b/src/modules/estimate/estimate.notification-policy.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { getRejectionNotificationExpiresAt } from "./estimate.notification-policy"; + +describe("반려 알림 만료 정책", () => { + it("알림 생성 시점부터 7일 후에 만료합니다.", () => { + const createdAt = new Date("2026-07-31T06:30:00.000Z"); + + assert.equal( + getRejectionNotificationExpiresAt(createdAt).toISOString(), + "2026-08-07T06:30:00.000Z", + ); + }); +}); diff --git a/src/modules/estimate/estimate.notification-policy.ts b/src/modules/estimate/estimate.notification-policy.ts new file mode 100644 index 00000000..8d2532b8 --- /dev/null +++ b/src/modules/estimate/estimate.notification-policy.ts @@ -0,0 +1,6 @@ +const REJECTION_NOTIFICATION_VISIBILITY_DAYS = 7; +const DAY_MS = 24 * 60 * 60 * 1000; + +export function getRejectionNotificationExpiresAt(createdAt: Date): Date { + return new Date(createdAt.getTime() + REJECTION_NOTIFICATION_VISIBILITY_DAYS * DAY_MS); +} From d20451b64c0fca2c9a290798d149938c58e8b309 Mon Sep 17 00:00:00 2001 From: yoonsojung Date: Fri, 31 Jul 2026 11:27:22 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=EB=B0=98=EB=A0=A4=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=A0=95=EC=B1=85=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/estimate/estimate.service.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/modules/estimate/estimate.service.ts b/src/modules/estimate/estimate.service.ts index 616f1ae0..f2cb5daf 100644 --- a/src/modules/estimate/estimate.service.ts +++ b/src/modules/estimate/estimate.service.ts @@ -4,7 +4,12 @@ import { AppError } from "../../lib/app-error"; import { buildPagination } from "../../utils/pagination.util"; import { runTransaction } from "../../utils/transaction"; import { notificationService } from "../notification/notification.service"; -import { moverEstimateRequestRepository, receivedEstimateRepository } from "./estimate.repository"; +import { getRejectionNotificationExpiresAt } from "./estimate.notification-policy"; +import { + moverEstimateRequestRepository, + moverSentEstimateRepository, + receivedEstimateRepository, +} from "./estimate.repository"; import type { ConfirmReceivedEstimateParams, GetReceivedEstimateDetailParams, @@ -15,6 +20,7 @@ import type { MoverEstimateRejectionListQuery, MoverEstimateRejectionListItem, MoverEstimateRejectionListResult, + MoverSentEstimateListQuery, PendingEstimateQuery, RejectEstimateParams, SendEstimateParams, @@ -379,17 +385,17 @@ export const moverEstimateRequestService = { rejection, customerId: estimateRequest.customerId, moverNickname: profile.nickname, - expiresAt: estimateRequest.expiresAt, }; }); + const notificationCreatedAt = new Date(); await notificationService.createNotification({ userId: result.customerId, type: "ESTIMATE_REQUEST_REJECTED", title: "견적 요청 반려", content: result.moverNickname, linkUrl: null, - expiresAt: result.expiresAt, + expiresAt: getRejectionNotificationExpiresAt(notificationCreatedAt), }); return result.rejection;