Skip to content

feat(conversation): 구매자 문의 채팅 기반 — 진입 컨텍스트·메시지 전송·FAQ 자동응답·인사말 - #268

Merged
chanwoo7 merged 5 commits into
developfrom
feat/conversation-buyer-messaging
Sep 1, 2026
Merged

feat(conversation): 구매자 문의 채팅 기반 — 진입 컨텍스트·메시지 전송·FAQ 자동응답·인사말#268
chanwoo7 merged 5 commits into
developfrom
feat/conversation-buyer-messaging

Conversation

@chanwoo7

@chanwoo7 chanwoo7 commented Sep 1, 2026

Copy link
Copy Markdown
Member

배경

figma 알림센터(문의 채팅) 화면 대응 2/4. conversation feature에는 DB 모델과 판매자 측 API만 있었고, 구매자 측(대화 생성·전송)은 전무했다.

변경점

  • Prisma: store.greeting_message VARCHAR(500) 추가(마이그레이션) — 인사말 템플릿({nickname}/{storeName} 치환, null이면 서버 기본 문구).
  • storeInquiryContext(storeId): 채팅 진입 화면 데이터 — 매장 프로필, 요일별 상담시간("HH:mm"), 치환 완료 인사말, 질문 칩(활성 StoreFaqTopic 정렬순), 기존 대화 ID.
  • sendConversationMessage: 텍스트 전송. 첫 전송 시 대화 생성 + 인사말 STORE 메시지 선저장(대화당 1회, 이력 보존 — 판매자 화면에도 보임).
  • sendConversationFaqMessage: 칩 탭 → 유저 질문(TEXT) + 자동응답(FAQ answer_html, HTML) 트랜잭션 저장. 답변은 저장 시점 스냅샷.
  • 동시성/soft-delete: 대화 조회를 유니크 제약과 같은 범위(삭제 포함)로 맞춰 P2002 예방, 잔여 레이스는 P2002 복구로 방어.
  • 판매자: sellerUpdateStoreBasicInfogreetingMessage 확장(빈 문자열 → null = 기본 문구 복귀), SellerStore에 노출.
  • 시드: FAQ 칩 5종 + 대화 2건(칩 문답, 판매자 답장 3건 안읽음 재현), resetSeedScope 정리 범위 갱신(재시드 멱등 확인 완료).

테스트

  • service 12케이스 / 매퍼 helper 6케이스 / resolver 통합 2케이스 / input 6케이스 / seller greeting 케이스
  • yarn validate 전체 통과 (215 suites / 1825 tests), 시드 2회 연속 실행 멱등 확인

figma 알림센터(문의 채팅) 화면 대응 2/4. 대화 모델·판매자 API만 있던
conversation feature에 구매자 측 진입·전송 경로를 신설한다.

변경점
- Prisma: store.greeting_message VARCHAR(500) 추가(마이그레이션 동반) —
  문의 채팅 인사말 템플릿({nickname}/{storeName} 치환, null이면 기본 문구)
- Query storeInquiryContext(storeId): 매장 프로필·요일별 상담시간(영업시간
  rows, "HH:mm")·치환 완료 인사말·질문 칩(활성 StoreFaqTopic)·기존 대화 ID
- Mutation sendConversationMessage: 텍스트 전송. 첫 전송 시 대화 생성 +
  치환 완료 인사말을 STORE 메시지로 선저장(대화당 1회, 이력 보존)
- Mutation sendConversationFaqMessage: 칩 탭 → 유저 질문(TEXT) + 매장
  자동응답(FAQ answer_html, HTML) 트랜잭션 저장. 답변은 저장 시점 스냅샷
- 대화 upsert는 (account_id, store_id) 유니크와 동일 범위(삭제 포함)로
  조회해 P2002를 예방하고, 동시 첫 전송 레이스는 P2002 복구로 방어
- 판매자: sellerUpdateStoreBasicInfo에 greetingMessage 확장(빈 문자열 →
  null 저장 = 기본 문구 복귀) + SellerStore에 노출. 별도 mutation 대신
  기존 basic info 확장(자체 판단 — API 표면 최소화)
- 활성 USER 판정은 user feature의 evaluateActiveUserAccount 정책 공유
- 시드: FAQ 칩 5종 + 대화 2건(칩 문답·판매자 답장 3건 안읽음 재현),
  resetSeedScope에 대화·FAQ 정리 범위 추가

회귀 테스트
- service spec 12케이스(기본/커스텀 인사말 치환, FAQ 정렬·비활성 제외,
  첫 전송 인사말 선저장, 중복 대화 방지, soft-delete 대화 재사용,
  본문 검증, 비활성 매장·FAQ 거절, 권한 3종)
- 매퍼 helper 순수 단위 6케이스, resolver 통합 2케이스, input spec 6케이스,
  seller greeting 설정/초기화 케이스
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🧹 knip — dead-code 리포트

Duplicate exports (1)
전체 리포트
Duplicate exports (1)
KEYWORD_RANK_SNAPSHOT_SIZE|MAX_POPULAR_KEYWORDS_LIMIT  src/features/search/constants/search.constants.ts

청소 후보(오탐 가능) · 기준 docs/guide/architecture-conventions.md

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🩺 NestJS Doctor — 90/100 (Excellent)

진단 294건 (error 0).

Category error warning info
architecture 0 0 9
correctness 0 146 0
performance 0 24 16
schema 0 0 86
security 0 13 0
architecture / security 상위 항목
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'IAuditLogRepository'.
  • warning security/security/no-exposed-env-vars: Direct 'process.env.NODE_ENV' access in 'AuthController'. Use ConfigService instead.
  • warning security/security/require-guards-on-endpoints: Endpoint 'start' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'callback' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'refresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'logout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogin' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerRefresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'devIssueToken' has no @UseGuards() at class or method level.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/conversation/repositories/conversation.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'ConversationRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/order/repositories/order.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'OrderRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/product/repositories/product.repository'.

오탐 포함 가능 · 기준 docs/guide/architecture-conventions.md

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.24806% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...nversation/repositories/conversation.repository.ts 78.12% 5 Missing and 2 partials ⚠️
...versation/services/conversation-inquiry.service.ts 93.18% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.82% 5644/5770
🟢 Branches 93.75% 2235/2384
🟢 Functions 96.68% 1106/1144
🟢 Lines 98.39% 5132/5216

Test suite run success

1826 tests passing in 215 suites.

Report generated by 🧪jest coverage report action from 0224c0d

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46a2543920

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

where: {
account_id: args.accountId,
store_id: args.storeId,
deleted_at: undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reactivate a reused soft-deleted conversation

When this lookup finds a soft-deleted conversation, subsequent writes append messages without clearing deleted_at. The repository's normal buyer and seller reads are filtered by the soft-delete extension, so storeInquiryContext continues returning a null conversation ID and the seller cannot discover or read the newly sent messages even though the mutation reports success. Restore the conversation when reusing it, or permit creation of a new active row.

Useful? React with 👍 / 👎.

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.

반영: 897a8a8 — 메시지 저장 트랜잭션의 대화 갱신에 deleted_at: null 명시로 재사용 시 복구(평상시 no-op). 복구 검증 spec 추가.

Comment on lines +173 to +179
conversation = await tx.storeConversation.findFirstOrThrow({
where: {
account_id: args.accountId,
store_id: args.storeId,
deleted_at: undefined,
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry the concurrent-create lookup outside the old snapshot

With this repository's MySQL provider and its default repeatable-read isolation, two simultaneous first messages can both establish a snapshot in the preceding findFirst; after the losing insert waits and raises P2002, this findFirstOrThrow still runs in that same snapshot and may not see the winner's newly committed row. The purported race recovery therefore still rejects one sender; retry the operation in a new transaction or use a locking/upsert strategy that does not reread through the stale snapshot.

Useful? React with 👍 / 👎.

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.

반영: 897a8a8 — 대화 확보를 메시지 트랜잭션 밖 getOrCreateConversation으로 분리. P2002 복구 재조회가 새 문장(새 스냅샷)에서 실행돼 승자 row를 본다. 생성 후 메시지 tx 실패 시 남는 빈 대화는 목록에서 제외됨(last_message_at null).

Comment on lines +125 to +126
"""문의 채팅 인사말 템플릿. 빈 문자열이면 기본 문구로 되돌린다(null 저장)."""
greetingMessage: String

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expose greetingMessage on SellerStore

This adds greetingMessage only to the update input, while the SellerStore GraphQL type still lacks the corresponding field. Consequently sellers can update the template but cannot request its current value from either the mutation result or sellerMyStore, despite the service mapper returning it; add the field to SellerStore as well.

Useful? React with 👍 / 👎.

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.

반영: 897a8a8 — SellerStore 타입에 greetingMessage 추가. 편집 스크립트 오류로 type 블록 반영이 유실됐던 것(입력·매퍼·출력 타입은 반영돼 있었음).

Codex 지적 3건 반영.

- P1: soft-delete된 대화 재사용 시 deleted_at을 복구한다 — 삭제 상태로
  메시지만 쌓이면 구매자·판매자 어느 조회에도 잡히지 않아 유실돼 보인다.
  메시지 저장 트랜잭션의 대화 갱신에서 deleted_at: null 명시(평상시 no-op)
- P2: 대화 확보(getOrCreateConversation)를 메시지 트랜잭션 밖으로 분리 —
  REPEATABLE READ 스냅샷 안의 P2002 복구 재조회는 경쟁 트랜잭션의 커밋
  row를 못 볼 수 있다. 새 문장(새 스냅샷)에서 재조회해 승자 row를 얻는다.
  대화 생성 후 메시지 트랜잭션이 실패해도 빈 대화는 목록에 노출되지 않음
- P2: SellerStore GraphQL 타입에 greetingMessage 필드 누락 보완(설정값
  조회 불가 문제). 입력·매퍼·출력 타입은 기존 커밋에 이미 반영돼 있었음

- 재사용 spec에 deleted_at 복구 검증 추가

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 897a8a8a40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// 않을 수 있다(리뷰 반영). 메시지 삽입 전에 대화 row가 확정되면 충분하고,
// 이후 메시지 트랜잭션이 실패해도 빈 대화 row는 목록에 노출되지 않는다
// (last_message_at null).
const { conversation, created } = await this.getOrCreateConversation(args);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize first-message initialization

When two first-message requests for the same buyer and store overlap, the conversation creation here commits before the message transaction begins. The second request can therefore observe the new row as created: false and insert its user message before the first request inserts the greeting, violating the API contract that the greeting is stored first; if the first message transaction fails, a retry also treats the empty conversation as initialized and never stores a greeting. Keep initialization atomic or explicitly track/lock whether the greeting has been persisted.

Useful? React with 👍 / 👎.

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.

반영: 메시지 트랜잭션 안에서 대화 row SELECT ... FOR UPDATE 잠금 후 메시지 수 0건 기준으로 인사말 삽입. 동시 첫 전송은 잠금으로 직렬화되고, 생성 후 실패 재시도도 count 기준이라 인사말 계약(항상 첫 메시지)이 유지됨. spec 갱신.

Codex 지적 반영: "대화를 새로 생성했는가" 플래그 기준 인사말 저장은
동시 첫 전송(후발 요청이 인사말보다 먼저 유저 메시지 삽입)과 생성 후
메시지 트랜잭션 실패 재시도(빈 대화를 초기화 완료로 오인 → 인사말 영구
누락)에서 "인사말이 항상 첫 메시지" 계약을 깨뜨린다.

- 메시지 트랜잭션 안에서 대화 row를 SELECT ... FOR UPDATE로 잠근 뒤
  실제 메시지 수 0건일 때만 인사말을 삽입 — 동시 전송은 잠금으로 직렬화,
  실패 재시도도 count 기준이라 인사말이 복구된다
- 빈 대화(soft-delete 재사용 포함)에는 인사말부터 저장되도록 spec 갱신
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: d2c95461-3f6a-4038-9275-5a48ff3cf207

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 590ba92e97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +145 to +147
// 이후 메시지 트랜잭션이 실패해도 빈 대화 row는 목록에 노출되지 않는다
// (last_message_at null).
const { conversation } = await this.getOrCreateConversation(args);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep uninitialized conversations hidden from sellers

When a first send creates the conversation here, the row commits before the message transaction starts. Fresh evidence after the earlier initialization fix is that listConversationsByStore filters only by store/cursor and does not implement the claimed last_message_at exclusion. A seller polling during this interval can therefore obtain the empty conversation and reply first; the buyer transaction then counts that reply and permanently skips the greeting. A failed message transaction likewise leaves a phantom conversation visible indefinitely, so initialization should remain hidden/atomic until its first messages are committed.

Useful? React with 👍 / 👎.

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.

반영: 대화 생성을 메시지 트랜잭션 안으로 되돌려 원자화(lockOrCreateConversation). 스냅샷 문제는 P2002 복구를 FOR UPDATE 잠금 조회(locking read — MVCC 우회, 최신 커밋 읽음)로 수행해 해소. 실패 시 전체 롤백이라 빈 대화가 커밋되는 구간 자체가 없어짐.

Codex 지적 반영: 대화 row가 메시지 트랜잭션보다 먼저 커밋되면
sellerConversations(빈 대화 미필터)에 노출돼 판매자가 인사말보다 먼저
답장할 수 있고, 메시지 저장 실패 시 유령 대화가 영구히 남는다.

- 대화 생성/잠금을 메시지 트랜잭션 안 lockOrCreateConversation으로 통합:
  기존 대화는 id FOR UPDATE 잠금, 부재 시 본 트랜잭션에서 생성
- 동시 첫 전송의 P2002 복구는 FOR UPDATE 잠금 조회(locking read)로 수행 —
  REPEATABLE READ 스냅샷을 우회해 승자 커밋 row를 읽으므로, 앞서 지적된
  스냅샷 문제 없이 원자성을 되찾는다
- 실패 시 전체 롤백이라 유령 대화·인사말 누락 재시도 문제 모두 해소

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fbadab58fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return rows[0]?.id ?? null;
};

const existing = await this.prisma.storeConversation.findFirst({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move the preflight read outside the open transaction

When concurrent buyer sends occupy every Prisma pool connection, each interactive transaction holds one connection here while this root-client query waits to borrow a second one, so all requests can remain mutually blocked until the pool or transaction timeout expires; this also fails deterministically with connection_limit=1. Perform this preflight before opening $transaction, or execute the existence check through tx while preserving the locking-read recovery strategy.

Useful? React with 👍 / 👎.

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.

반영: 사전 조회를 tx 경유로 변경(커넥션 1개 유지). 스냅샷이 경쟁 커밋을 못 보는 경우는 create → P2002 → FOR UPDATE 잠금 조회가 복구.

Codex 지적 반영: $transaction 콜백 안에서 루트 클라이언트 조회는 풀
커넥션을 추가로 점유해, 동시 전송이 풀을 소진하면 상호 대기(타임아웃)가
난다(connection_limit=1이면 즉시 재현). 사전 조회를 tx 경유로 변경 —
tx 스냅샷이 경쟁 커밋을 못 봐도 create → P2002 → FOR UPDATE 잠금 조회
경로가 복구하므로 의미는 동일하다.
@chanwoo7
chanwoo7 merged commit fc2cc2d into develop Sep 1, 2026
11 checks passed
@chanwoo7
chanwoo7 deleted the feat/conversation-buyer-messaging branch September 1, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant