diff --git a/src/features/conversation/conversation-inquiry.graphql b/src/features/conversation/conversation-inquiry.graphql index b3cf37f..04bb5e5 100644 --- a/src/features/conversation/conversation-inquiry.graphql +++ b/src/features/conversation/conversation-inquiry.graphql @@ -64,11 +64,11 @@ type ConversationMessage { conversationId: ID! """발신 주체. 말풍선 좌우 배치 기준이다.""" senderType: ConversationSenderType! - """본문 형식. 이 값에 따라 bodyText·bodyHtml 중 하나가 채워진다.""" + """본문 형식. 읽어야 할 본문 필드를 가리킨다.""" bodyFormat: ConversationBodyFormat! - """평문 본문. bodyFormat이 TEXT일 때 채워진다.""" + """평문 본문. bodyFormat이 TEXT면 이 값을 쓴다.""" bodyText: String - """서식 본문. bodyFormat이 HTML일 때 채워진다.""" + """서식 본문. bodyFormat이 HTML이면 이 값을 쓴다.""" bodyHtml: String """발송 시각. 메시지 스트림 정렬은 id 기준이다.""" createdAt: DateTime! diff --git a/src/features/conversation/repositories/conversation.repository.spec.ts b/src/features/conversation/repositories/conversation.repository.spec.ts index 31d78bb..65b5eb4 100644 --- a/src/features/conversation/repositories/conversation.repository.spec.ts +++ b/src/features/conversation/repositories/conversation.repository.spec.ts @@ -73,6 +73,33 @@ describe('ConversationRepository (real DB)', () => { expect(rows.map((r) => r.id)).toEqual([older.id]); }); + it('updated_at이 같으면 id 내림차순으로 이어서 끊는다', async () => { + // 커서의 보조 키 분기(updated_at 동률 → id < cursor.id)를 타는 케이스. + // 서로 다른 updated_at만 쓰면 이 분기가 한 번도 실행되지 않는다. + const store = await createStore(prisma); + const sameTime = new Date('2026-09-05T00:00:00Z'); + const make = async () => { + const customer = await createAccount(prisma, { account_type: 'USER' }); + return prisma.storeConversation.create({ + data: { + account_id: customer.id, + store_id: store.id, + updated_at: sameTime, + }, + }); + }; + const first = await make(); + const second = await make(); + expect(second.id > first.id).toBe(true); + + const rows = await repo.listConversationsByStore({ + storeId: store.id, + limit: 10, + cursor: { updatedAt: sameTime, id: second.id }, + }); + expect(rows.map((r) => r.id)).toEqual([first.id]); + }); + it('id가 더 큰 오래된 대화도 커서 페이지에서 빠지지 않는다', async () => { // 정렬은 updated_at desc인데 커서가 id 단독이면 `id < cursor`가 정렬과 // 무관한 행을 잘라내, id가 큰 오래된 대화가 목록에서 영영 빠졌다. diff --git a/src/features/seller/seller-content.graphql b/src/features/seller/seller-content.graphql index 8d9bc23..025a5e3 100644 --- a/src/features/seller/seller-content.graphql +++ b/src/features/seller/seller-content.graphql @@ -100,7 +100,7 @@ type SellerBanner { title: String """배너 이미지 URL.""" imageUrl: String! - """이동 대상 타입. 이 값에 따라 아래 링크 필드 중 하나만 채워진다.""" + """이동 대상 타입. 읽어야 할 링크 필드를 가리킨다(판매자 조회는 다른 필드 값도 그대로 내려준다).""" linkType: BannerLinkType! """이동할 URL. linkType이 URL일 때 쓰는 값.""" linkUrl: String diff --git a/src/features/seller/seller-conversation.graphql b/src/features/seller/seller-conversation.graphql index b43c51f..c887bc1 100644 --- a/src/features/seller/seller-conversation.graphql +++ b/src/features/seller/seller-conversation.graphql @@ -48,19 +48,26 @@ type SellerConversationConnection { nextCursor: String } -"""대화 메시지 1건. bodyFormat에 따라 bodyText 또는 bodyHtml 하나만 채워진다.""" +""" +대화 메시지 1건. bodyFormat이 읽어야 할 본문 필드를 가리킨다. +발송 시 다른 형식의 본문을 함께 넘기면 그대로 저장되므로, 두 필드가 모두 채워져 +있을 수 있다 — 표시할 본문은 bodyFormat으로 고른다. +""" type SellerConversationMessage { id: ID! conversationId: ID! """발신 주체. 말풍선 좌우 배치 기준이다.""" senderType: ConversationSenderType! - """보낸 계정 ID. senderType이 SYSTEM이면 null이다.""" + """ + 보낸 계정 ID. 사람이 보낸 메시지에만 채워진다. + 시스템 메시지는 물론, 인사말·FAQ 자동응답처럼 서버가 만든 STORE 메시지도 null이다. + """ senderAccountId: ID - """본문 형식. 이 값에 따라 bodyText·bodyHtml 중 하나가 채워진다.""" + """본문 형식. 읽어야 할 본문 필드를 가리킨다(다른 쪽에도 값이 남아 있을 수 있다).""" bodyFormat: ConversationBodyFormat! - """평문 본문. bodyFormat이 TEXT일 때 채워진다.""" + """평문 본문. bodyFormat이 TEXT면 이 값을 쓴다.""" bodyText: String - """서식 본문. bodyFormat이 HTML일 때 채워진다.""" + """서식 본문. bodyFormat이 HTML이면 이 값을 쓴다.""" bodyHtml: String """발송 시각.""" createdAt: DateTime! @@ -84,7 +91,7 @@ type SellerConversationMessageConnection { """ input SellerSendConversationMessageInput { conversationId: ID! - """본문 형식. 이 값에 따라 아래 둘 중 어느 필드가 필수인지 정해진다.""" + """본문 형식. 이 값에 해당하는 본문 필드가 필수다(다른 쪽을 함께 넘기면 그대로 저장된다).""" bodyFormat: ConversationBodyFormat! """평문 본문(최대 2000자). bodyFormat이 TEXT면 필수.""" bodyText: String diff --git a/src/features/user/user-order.graphql b/src/features/user/user-order.graphql index 0abb77b..438b678 100644 --- a/src/features/user/user-order.graphql +++ b/src/features/user/user-order.graphql @@ -111,7 +111,10 @@ type MyOrderItemDetail { productId: ID! """상품명(주문 당시).""" productName: String! - """대표 이미지 URL(주문 당시). 이미지가 없으면 null.""" + """ + 대표 상품 이미지 URL. 주문 당시 스냅샷이 아니라 현재 상품의 첫 이미지를 읽는다 — + 판매자가 이미지를 바꾸거나 지우면 값이 달라지거나 null이 된다. + """ representativeImageUrl: String """주문 수량.""" quantity: Int!