Skip to content

[Refactor] Seller 도메인 명칭 변경 및 데이터 모델 구조 개선#8

Open
Takch02 wants to merge 10 commits intomainfrom
refactor/schema-separation
Open

[Refactor] Seller 도메인 명칭 변경 및 데이터 모델 구조 개선#8
Takch02 wants to merge 10 commits intomainfrom
refactor/schema-separation

Conversation

@Takch02
Copy link
Copy Markdown
Contributor

@Takch02 Takch02 commented Apr 30, 2026

#7

1. 개요 (Summary)

서비스 내 도메인 용어의 명확성을 확보하고, 계정 정보와 비즈니스 정보를 분리하여 데이터 확장성을 높이는 작업을 진행했습니다.

2. 주요 변경 사항 (Key Changes)

  • 명칭 리팩토링 (Company → Seller)
    • 모든 모듈, 컨트롤러, 서비스, DTO 내 Company 키워드를 Seller로 변경했습니다.
    • 기존 companies 모듈을 sellers 모듈로 폴더 및 파일명을 재배치했습니다.
  • 스키마 구조 분리 (Account vs Domain)
    • 계정 및 인증 정보를 담는 User 데이터(User-Seller, User-Buyer)와 검색 및 매칭에 사용되는 비즈니스 데이터(Seller, Buyer)를 분리했습니다.
    • 이를 통해 계정의 상태 관리(활성/비활성)와 비즈니스 프로필 관리를 독립적으로 운영할 수 있도록 개선했습니다.
  • Buyer 도메인 고도화
    • Buyer 스키마의 필드를 요구사항에 맞춰 수정했습니다. (name_kr, intro_kr, industry_kr 등 반영)
    • 변경된 필드 구조에 맞춰 BuyersService의 검색(Filter) 로직을 업데이트했습니다.

3. 변경 전/후 구조 비교 (Architecture)

  • Before: User 데이터 내에 기업 정보가 혼재되어 있거나, Company라는 모호한 명칭 사용
  • After:
    • UserSeller / UserBuyer: 로그인, 지갑 주소, 가입 상태 관리
    • Seller / Buyer: 기업 소개, 태그, 임베딩 등 비즈니스 데이터 관리
      * Vector Search를 위해 약 5000개 Buyer 정보를 Embedding 하여 넣음. (Buyer 컬럼 수정 이유)

4. 영향도 (Impact)

  • 인증(Auth), 매칭(Match), 미팅 예약(Consultation) 모듈 등 전반적인 비즈니스 로직에 영향이 있으며, 모든 참조 관계가 최신화되었습니다.
  • 기존 API 엔드포인트가 /companies에서 /sellers로 변경되었습니다.

Summary by CodeRabbit

  • New Features

    • 판매자(Sellers) 전용 엔드포인트와 판매자 모델 추가
  • Changes

    • API 필드 명칭 전면 변경: companyId → sellerId, companyName → sellerName 등
    • 검색/매칭/컨설팅 흐름을 판매자 중심으로 전환
    • 검색에서 지역화된 이름/소개(name_kr/name_en 등) 필드 사용 확대
    • 파트너 검색(q) 파라미터가 필수화됨
  • Bug Fixes

    • 임베딩 실패 시 에러 로깅 개선 및 빈 결과 반환 처리

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories.

📝 Walkthrough

Walkthrough

대규모 도메인 리팩토링: Company/CompanyModule 중심 모델을 Seller/UserSeller/UserBuyer 중심으로 전환하고 관련 스키마, DTO, 서비스, 모듈 의존성, 검색/매칭/결제/상담 경로 및 환경 설정을 일괄 업데이트했습니다.

Changes

도메인 · 스키마 · DTO 변경 (데이터 형태)

Layer / File(s) Summary
데이터 스키마 추가/수정
src/modules/sellers/schemas/seller.schema.ts, src/modules/buyers/schemas/buyer.schema.ts, src/modules/users/schemas/user-seller.schema.ts, src/modules/users/schemas/user-buyer.schema.ts, src/modules/users/schemas/user.schema.ts
Seller/Buyer/UserSeller/UserBuyer 스키마 추가 및 기존 user 스키마 필드 재구성(industries, sellerIntroduction 등).
DTO 변경
src/modules/auth/dto/register.dto.ts, src/modules/consultations/dto/*.ts, src/modules/payments/dto/create-payment.dto.ts, src/modules/sellers/dto/*, src/modules/buyers/dto/query-buyer.dto.ts
companyId/companyName 등 → sellerId/sellerName/sellerIntroduction, seller-focused 필드명 및 일부 필드(industries, tags 제거/통일) 변경.
스키마 삭제/이전
src/modules/users/schemas/buyer.schema.ts (삭제)
기존 buyer.schema 삭제(대체로 user-buyer로 이동).

핵심 서비스 로직 변경

Layer / File(s) Summary
인증/유저 서비스 로직
src/modules/auth/auth.service.ts, src/modules/auth/*.spec.ts
Company/Buyer 모델 참조 → UserSeller/UserBuyer 모델로 교체, 등록·활성화 흐름에서 필드 매핑 변경(sellerName 등).
판매자/회사 서비스
src/modules/sellers/sellers.service.ts, src/modules/sellers/sellers.service.spec.ts
CompaniesService → SellersService로 교체; 조회/생성/수정/삭제 로직을 seller 모델과 persistence 매핑으로 변경.
파트너(검색) 서비스
src/modules/partners/partners.service.ts, src/modules/partners/partners.service.spec.ts
검색 옵션 q를 필수화, intent 기반(buyer vs seller) activeModel 선택, intent별 텍스트/벡터/DB/그래프 재검색·매핑 및 debug 확장.
매칭 서비스
src/modules/matches/matches.service.ts, src/modules/matches/schemas/*
Company 관련 로직 → Seller 중심으로 변경, score 로직(새 overlap 신호: buyer 산업 ↔ seller tags)으로 재구성, submitFeedback 파라미터명 변경.
상담/결제 서비스
src/modules/consultations/consultations.service.ts, src/modules/payments/payments.service.ts
companyId → sellerId로 조회/저장 및 DTO/스키마 매핑 변경.
임베딩 예외 처리
src/modules/embeddings/embeddings.service.ts
embed 실패 시 에러 로그 후 빈 배열 반환(기존 무시에서 로깅 추가).

모듈/와이어링(등록·DI·라우팅) 변경

Layer / File(s) Summary
모듈 추가/삭제/교체
src/modules/sellers/sellers.module.ts, src/modules/companies/companies.module.ts (삭제), src/modules/buyers/buyers.module.ts, src/modules/partners/partners.module.ts, src/modules/matches/matches.module.ts, src/modules/consultations/consultations.module.ts, src/modules/users/users.module.ts, src/app.module.ts
CompaniesModule 삭제, SellersModule 추가, 관련 모듈 의존성들(Companies → Sellers/Users)로 교체 및 Mongoose 등록(판매자/구매자 모델).
컨트롤러 라우팅/스웨거
src/modules/sellers/sellers.controller.ts, src/modules/matches/matches.controller.ts, src/modules/partners/partners.controller.ts, src/modules/consultations/consultations.controller.ts
route/tag/파라미터(예: companies → sellers), Swagger 메타데이터, 파라미터 필수성(q: required) 등 업데이트.

테스트 · 사소한 파일 변경 · 환경설정

Layer / File(s) Summary
테스트 업데이트
src/modules/*/*.spec.ts (auth, buyers, partners, matches, sellers, partners.controller 등)
대부분 company → seller, 모델 토큰/모킹/기대치/fixture 이름 교체 및 일부 테스트 내용(intent 분기 등) 수정.
환경/설정 변경
src/config/env.config.ts, .env.test, .coderabbit.yaml
MONGODB_URI 필수화(기본 localhost 제거), mongodb.dbName 추가, ATLAS_VECTOR_INDEX fallback 제거, OpenAI modelId 제거, .env.test에서 TAVILY_API_KEY 삭제, .coderabbit.yaml의 리뷰 지침·문구 보강.
모듈 메타데이터(내보내기)
src/modules/buyers/buyers.module.ts
BuyersModule이 Buyer 모델을 직접 등록하고 MongooseModule을 export하도록 변경.

(참고: 변경 파일 수가 많아 위 표는 리뷰 순서 관점의 계층화이며, 실제 diff에 있는 모든 파일 경로를 요약해 포함했습니다.)

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PartnersService
  participant EmbeddingsService
  participant MongoDB
  participant Neo4j
  participant WebSearch

  Client->>PartnersService: search(q, options)
  PartnersService->>PartnersService: intent 분석 (buyer/seller)
  PartnersService->>EmbeddingsService: generateHyDEAndKeywords(q)
  EmbeddingsService-->>PartnersService: hydeDocument + keywords
  alt intent = buyer
    PartnersService->>MongoDB: buyerModel.find / aggregate (text/vector)
  else intent = seller
    PartnersService->>MongoDB: sellerModel.find / aggregate (text/vector)
  end
  PartnersService->>WebSearch: fallback web search (optional)
  WebSearch-->>PartnersService: web results
  PartnersService->>Neo4j: re-rank graph (sellerMongoIds)
  Neo4j-->>PartnersService: graph scores
  PartnersService-->Client: merged results + debug (hydeDocument, duration, intent)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60분

Possibly related issues

Possibly related PRs

Poem

🏢 → 🛍️ 이름을 갈아입은 도메인,
Seller로 불러 새 길을 열고,
UserSeller·UserBuyer로 경계를 다듬어,
검색·매칭은 의도 따라 춤추고,
코드베이스는 새 걸음으로 나아가네 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main refactoring effort: renaming the Company domain to Seller and improving data model structure through schema separation.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/schema-separation

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/modules/sellers/sellers.service.ts (1)

48-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

tagpartnership를 함께 전달하면 필터가 덮어써집니다.

Line 48-51에서 filter.tags를 두 번 할당해 마지막 값만 남습니다. 두 조건을 함께 준 요청의 결과가 잘못됩니다.

수정 예시
-    if (tag) filter.tags = tag;
+    if (tag) filter.tags = tag;
     if (country) filter["location.country"] = country;
     if (size) filter.sizeBucket = size;
-    if (partnership) filter.tags = partnership;
+    if (tag && partnership) {
+      filter.tags = { $all: [tag, partnership] };
+    } else if (partnership) {
+      filter.tags = partnership;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/sellers/sellers.service.ts` around lines 48 - 51, The filter
assignment overwrites filter.tags when both tag and partnership are present;
update the logic in the sellers service where filter is built (the code that
checks tag and partnership) to merge values instead of reassigning: ensure
filter.tags becomes an array (or preserves existing array) and push/concat the
tag and partnership values as needed, handling the case where one or both are
already arrays or strings so both criteria are applied.
src/modules/matches/matches.controller.ts (1)

25-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Swagger/에러 설명 용어를 Seller 기준으로 통일해 주세요.

동작은 sellerId로 잘 바뀌었지만, 문서 텍스트에 "기업"이 남아 있어 /companies → /sellers 전환 컨텍스트에서 혼동을 줄 수 있습니다.

권장 수정 예시
- `@ApiOperation`({ summary: "바이어 기반 기업 매칭" })
+ `@ApiOperation`({ summary: "바이어 기반 셀러 매칭" })

- description: "기업 MongoDB ID (24자 hex)",
+ description: "셀러 MongoDB ID (24자 hex)",

- `@ApiResponse`({ status: 404, description: "기업 없음" })
+ `@ApiResponse`({ status: 404, description: "셀러 없음" })

Also applies to: 56-57, 71-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/matches/matches.controller.ts` at line 25, The Swagger summaries
and any user-facing error/description text in matches.controller.ts still use
"Company"/"기업" terminology and should be changed to "Seller"/"바이어" to match the
route/parameter switch to sellerId; update all `@ApiOperation`(...) decorators,
DTO/property descriptions, and error messages in the controller (look for the
`@ApiOperation` decorators and any messages near the methods handling sellerId and
the related match endpoints) so they consistently refer to Seller (and sellerId)
instead of Company to avoid confusion.
src/modules/auth/auth.service.ts (1)

40-66: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

이메일 중복 체크를 exists()에만 의존하면 동시 가입에서 500이 납니다.

지금 구조는 두 요청이 동시에 exists() === false를 통과한 뒤 save()에서 같은 email unique 인덱스를 치는 순간 duplicate key가 발생할 수 있습니다. 이 예외를 ConflictException으로 변환하지 않아서 seller/buyer 가입이 간헐적으로 500으로 실패합니다. save()E11000을 잡아서 409로 매핑해 주세요.

수정 예시
-    const saved = await newSeller.save();
+    let saved;
+    try {
+      saved = await newSeller.save();
+    } catch (err: any) {
+      if (err?.code === 11000) {
+        throw new ConflictException("Already registered Email");
+      }
+      throw err;
+    }

Also applies to: 77-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/auth/auth.service.ts` around lines 40 - 66, The current signup
flow relies on this.userModel.exists(...) and then calls newSeller.save(), which
can still raise a duplicate-key error under concurrent requests; catch errors
from newSeller.save() (and the analogous buyer save path) and if the error is a
Mongo duplicate-key (error.code === 11000 or message contains "E11000") throw a
ConflictException("Already registered Email") instead of letting it bubble as a
500; update the save call sites for userSellerModel (newSeller.save()) and the
buyer save block to wrap in try/catch and map E11000 to ConflictException so
duplicate email inserts return 409.
src/modules/partners/partners.service.ts (1)

562-569: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

국가 재랭킹이 웹 본문을 거의 보지 못하고 있습니다.

여기서는 item.sellerIntroduction을 읽고 있는데, 위에서 만든 웹 결과 객체는 본문을 profileText에 넣습니다. 지금 상태면 국가 키워드 가중치가 사실상 제목(name)에만 걸려서 웹 fallback 정렬 정확도가 떨어집니다.

수정 예시
-          const text = (
-            (item.sellerIntroduction || "") +
-            " " +
-            (item.name || "")
-          ).toLowerCase();
+          const text = (
+            (item.profileText || "") +
+            " " +
+            (item.name || "")
+          ).toLowerCase();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/partners/partners.service.ts` around lines 562 - 569, The country
re-ranking currently uses (item.sellerIntroduction || "") + " " + (item.name ||
"") to build text, but the web result body was stored in profileText, so country
weighting ignores most of the page; update the mappedWebResults mapping (the
mappedWebResults = mappedWebResults.map(...) callback in partners.service.ts
where intentData?.country is checked) to include item.profileText (e.g., use
(item.profileText || item.sellerIntroduction || "") + " " + (item.name || ""))
so country keyword matching considers the full web body rather than only the
name.
🧹 Nitpick comments (2)
src/modules/auth/auth.controller.spec.ts (1)

23-29: RegisterBuyerDto의 필드 명명이 의미론적으로 부정확합니다.

RegisterBuyerDtosellerName, sellerIntroduction 필드를 사용하고 있어 구매자(buyer) 등록 맥락에서 의미가 불분명합니다. 이 필드들은 buyerName, buyerIntroduction으로 명명되어야 이해하기 쉬울 것 같습니다. RegisterSellerDto와 차이를 명확히 하는 것이 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/auth/auth.controller.spec.ts` around lines 23 - 29, The test uses
RegisterBuyerDto fields named sellerName and sellerIntroduction which are
semantically incorrect for buyer registration; update the DTO usage and test
fixtures to rename sellerName -> buyerName and sellerIntroduction ->
buyerIntroduction in RegisterBuyerDto (and any places constructing Buyer objects
in auth.controller.spec.ts), update any assertions and mock requests that
reference those properties, and ensure any mapping logic or validators (e.g.,
the RegisterBuyerDto class, validationPipe expectations, or DTO-to-entity
mappers) are updated to accept and validate buyerName/buyerIntroduction instead
of the old seller* names so the test and implementation remain consistent.
src/modules/matches/schemas/match-log.schema.ts (1)

8-9: ⚡ Quick win

MatchResult.sellerId를 필수로 고정해 데이터 무결성을 보장해주세요.

현재는 sellerId가 optional로 저장될 수 있어, 매칭 로그에 식별자 없는 결과가 누적될 수 있습니다. 최소한의 스키마 제약으로 방지하는 게 안전합니다.

🔧 제안 수정안
-  `@Prop`({ type: Types.ObjectId, ref: "Seller", index: true })
+  `@Prop`({ type: Types.ObjectId, ref: "Seller", index: true, required: true })
   sellerId: Types.ObjectId;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/matches/schemas/match-log.schema.ts` around lines 8 - 9, Make
sellerId required in the MatchResult schema to enforce data integrity: update
the `@Prop` on sellerId (in match-log.schema.ts / the MatchResult-related schema)
to include required: true (or required: true, type: Types.ObjectId, ref:
"Seller", index: true) so the schema rejects documents without a sellerId;
ensure the TypeScript property signature (sellerId: Types.ObjectId) remains
consistent with the decorator change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.coderabbit.yaml:
- Around line 3-9: The YAML config's tone_instructions text may exceed
tone_instructions.maxLength(250) and references old domain names (Companies,
user-company) that conflict with current renaming to Seller/user-seller; update
.coderabbit.yaml to shorten and normalize the tone_instructions value to under
250 characters and replace obsolete terms with the new domain names (Seller,
user-seller), and ensure the PR comment limit rule ("max 20 comments") remains
explicit and consistent with the schema so validation won't reject the file.

In `@src/config/env.config.ts`:
- Around line 6-7: 현재 설정 객체의 uri 필드가 process.env.MONGODB_URI를 그대로 사용하여 누락 시 연결
시점까지 지연 실패하므로, env 로딩 시점에 MONGODB_URI가 존재하는지 검증하고 누락이면 즉시 예외를 던져 프로세스를 중단하도록
변경하세요; 구체적으로 src/config/env.config.ts의 uri 프로퍼티를 설정할 때 process.env.MONGODB_URI가
falsy면 명확한 메시지로 Error를 throw(예: "MONGODB_URI env var is required") 하거나 프로세스 종료를
호출하도록 하고 dbName 기본값("K-statra")은 그대로 둡니다.

In `@src/modules/buyers/buyers.service.ts`:
- Around line 25-39: The current filter construction in buyers.service.ts
incorrectly merges the q and industry criteria into a single $or (so matching
either q OR industry) and also uses raw user input in $regex; update the logic
to build two separate condition groups (e.g., nameIntroOr for q and industryOr
for industry) and combine them with $and when both are present so both groups
must match, and escape user input before constructing RegExp; modify the code
that builds filter.$or (and where q/industry are referenced) to instead compose
filter.$and = [ { $or: nameIntroOr }, { $or: industryOr } ] when both exist, or
keep a single $or group when only one is provided, and ensure regex escaping is
applied to q and industry inputs before using them in the $regex options.

In `@src/modules/consultations/schemas/consultation.schema.ts`:
- Around line 24-25: Create an idempotent migration that renames
companyId→sellerId and companyName→sellerName across collections that still use
the old fields (at least consultations, payments, matches, auth/users), skipping
documents that already have sellerId; for each collection run an updateMany that
sets sellerName = companyName and sellerId = ObjectId(companyId) (use safe
conversion and only if companyId exists and sellerId is null), then unset
companyId and companyName; after migration run validations: sample a few
documents and compare counts (documents with companyId before vs documents with
sellerId after) and run a script to assert no documents remain with
companyId/companyName; include logging for successes/failures and make the
script re-runnable and reversible (backup or dry-run option).

In `@src/modules/partners/partners.service.ts`:
- Line 1001: The returned query string currently forces the token "seller" into
buyer-targeted Tavily queries, which increases reverse/noise; remove the
"seller" token from the constructed string so buyer-intent queries only include
buyer-relevant tokens (keep regionEn, productEn, "importer distributor buyer
B2B" and "contact" and the exclude variable as-is). Locate the string
construction (the return `${regionEn ? regionEn + " " : ""}${productEn ?
productEn + " " : ""}importer distributor buyer B2B seller "contact"
${exclude}`) and delete the literal "seller" (and any surrounding extra spaces)
or make its inclusion conditional on a non-buyer intent flag if this function is
used for multiple intents. Ensure spacing remains correct after removal.
- Around line 124-125: detectedIntent is hardcoded to "seller" so requests
without q (filter-only or browse) always hit sellerModel; change the flow so
detectedIntent defaults to null/undefined (not "seller"), accept an explicit
intent parameter if provided, and update the model-selection logic (the branch
that currently reads detectedIntent to pick sellerModel vs buyerModel) to: if an
explicit intent === "buyer" use buyerModel, if explicit intent === "seller" use
sellerModel, otherwise (no q and no explicit intent) either query both models
and merge/union results or return a clear error/requirement; make these changes
around the detectedIntent variable and wherever sellerModel/buyerModel are
chosen so buyer browse requests no longer silently resolve to seller results.

In `@src/modules/payments/schemas/payment.schema.ts`:
- Around line 44-45: Removing companyId and replacing it with sellerId in the
Payment schema will break reads/aggregations for existing documents; add a
migration and rollout plan: implement a one-time backfill script that reads
existing payments with companyId, resolves the corresponding Seller object/ID,
and sets sellerId (and create an index on sellerId), deploy schema change only
after backfill completes, and during deployment enable dual-write/dual-read
logic in the payments service to accept both companyId and sellerId (and
fallback mapping) until all consumers and aggregations are switched; include
idempotency in the migration, tests for queries using sellerId and companyId
mappings, and monitor for missing mappings to handle manually.

In `@src/modules/sellers/sellers.controller.ts`:
- Around line 19-20: Changing the controller route to `@Controller`("sellers")
will immediately break clients calling the previous /companies path; update the
controller to expose an alias route (keep /companies alongside /sellers) or add
versioned routes so both work during a transition: e.g., ensure the
SellersController (the class with `@Controller`("sellers")) also registers
/companies (either via an additional `@Controller`("/companies") decorator, a
secondary route decorator on methods, or by mounting a router alias) and/or
introduce versioning (e.g., /v2/sellers) so old clients continue to hit
/companies for at least one release before removing it.

In `@src/modules/sellers/sellers.service.ts`:
- Around line 94-109: The create(dto: CreateSellerDto) and update(id: string,
dto: UpdateSellerDto) methods currently pass DTOs directly into
sellerModel.create and findByIdAndUpdate which can hide schema/DTO mismatches;
instead implement an explicit mapping layer that builds a plain object
containing only the model fields (e.g., mapCreateSellerFields(dto) /
mapUpdateSellerFields(dto) or inline mapping in create/update), validate/strip
unknown keys, and then call this.sellerModel.create(mapped) and
this.sellerModel.findByIdAndUpdate(id, { ...mappedUpdate, updatedAt: new Date()
}, {...}); ensure mapping references the exact model field names used by the
Seller schema and reuse the mapping in both create and update to avoid silent
omissions.

In `@src/modules/users/schemas/user.schema.ts`:
- Around line 59-61: You changed the text index on UserSchema from
companyIntroduction to sellerIntroduction but MongoDB allows only one text index
per collection, so update the change into a safe migration: do not deploy a raw
UserSchema.index(...) swap—create an explicit migration that (1) builds the new
text index on sellerIntroduction (e.g., via an index-creation migration using
the users collection), (2) verifies index build completion and search
correctness, and only then drops the old companyIntroduction text index, or use
Mongoose syncIndexes/explicit dropIndex calls to control timing; add
monitoring/logging around the index build and document this in deployment notes
so ProductSearch (text search) is not disrupted. Ensure references to
UserSchema.index, companyIntroduction, sellerIntroduction, and consider using
syncIndexes or explicit Model.collection.dropIndex in the migration.

---

Outside diff comments:
In `@src/modules/auth/auth.service.ts`:
- Around line 40-66: The current signup flow relies on
this.userModel.exists(...) and then calls newSeller.save(), which can still
raise a duplicate-key error under concurrent requests; catch errors from
newSeller.save() (and the analogous buyer save path) and if the error is a Mongo
duplicate-key (error.code === 11000 or message contains "E11000") throw a
ConflictException("Already registered Email") instead of letting it bubble as a
500; update the save call sites for userSellerModel (newSeller.save()) and the
buyer save block to wrap in try/catch and map E11000 to ConflictException so
duplicate email inserts return 409.

In `@src/modules/matches/matches.controller.ts`:
- Line 25: The Swagger summaries and any user-facing error/description text in
matches.controller.ts still use "Company"/"기업" terminology and should be changed
to "Seller"/"바이어" to match the route/parameter switch to sellerId; update all
`@ApiOperation`(...) decorators, DTO/property descriptions, and error messages in
the controller (look for the `@ApiOperation` decorators and any messages near the
methods handling sellerId and the related match endpoints) so they consistently
refer to Seller (and sellerId) instead of Company to avoid confusion.

In `@src/modules/partners/partners.service.ts`:
- Around line 562-569: The country re-ranking currently uses
(item.sellerIntroduction || "") + " " + (item.name || "") to build text, but the
web result body was stored in profileText, so country weighting ignores most of
the page; update the mappedWebResults mapping (the mappedWebResults =
mappedWebResults.map(...) callback in partners.service.ts where
intentData?.country is checked) to include item.profileText (e.g., use
(item.profileText || item.sellerIntroduction || "") + " " + (item.name || ""))
so country keyword matching considers the full web body rather than only the
name.

In `@src/modules/sellers/sellers.service.ts`:
- Around line 48-51: The filter assignment overwrites filter.tags when both tag
and partnership are present; update the logic in the sellers service where
filter is built (the code that checks tag and partnership) to merge values
instead of reassigning: ensure filter.tags becomes an array (or preserves
existing array) and push/concat the tag and partnership values as needed,
handling the case where one or both are already arrays or strings so both
criteria are applied.

---

Nitpick comments:
In `@src/modules/auth/auth.controller.spec.ts`:
- Around line 23-29: The test uses RegisterBuyerDto fields named sellerName and
sellerIntroduction which are semantically incorrect for buyer registration;
update the DTO usage and test fixtures to rename sellerName -> buyerName and
sellerIntroduction -> buyerIntroduction in RegisterBuyerDto (and any places
constructing Buyer objects in auth.controller.spec.ts), update any assertions
and mock requests that reference those properties, and ensure any mapping logic
or validators (e.g., the RegisterBuyerDto class, validationPipe expectations, or
DTO-to-entity mappers) are updated to accept and validate
buyerName/buyerIntroduction instead of the old seller* names so the test and
implementation remain consistent.

In `@src/modules/matches/schemas/match-log.schema.ts`:
- Around line 8-9: Make sellerId required in the MatchResult schema to enforce
data integrity: update the `@Prop` on sellerId (in match-log.schema.ts / the
MatchResult-related schema) to include required: true (or required: true, type:
Types.ObjectId, ref: "Seller", index: true) so the schema rejects documents
without a sellerId; ensure the TypeScript property signature (sellerId:
Types.ObjectId) remains consistent with the decorator change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 80684830-579d-4205-b59b-72f5cea0ca9d

📥 Commits

Reviewing files that changed from the base of the PR and between 42941d3 and 8acfc4b.

📒 Files selected for processing (50)
  • .coderabbit.yaml
  • .env.test
  • src/app.module.ts
  • src/config/env.config.ts
  • src/modules/auth/auth.controller.spec.ts
  • src/modules/auth/auth.service.spec.ts
  • src/modules/auth/auth.service.ts
  • src/modules/auth/dto/register.dto.ts
  • src/modules/buyers/buyers.module.ts
  • src/modules/buyers/buyers.service.spec.ts
  • src/modules/buyers/buyers.service.ts
  • src/modules/buyers/dto/query-buyer.dto.ts
  • src/modules/buyers/schemas/buyer.schema.ts
  • src/modules/companies/companies.module.ts
  • src/modules/companies/dto/update-company.dto.ts
  • src/modules/consultations/consultations.controller.ts
  • src/modules/consultations/consultations.module.ts
  • src/modules/consultations/consultations.service.ts
  • src/modules/consultations/dto/create-consultation.dto.ts
  • src/modules/consultations/dto/query-consultation.dto.ts
  • src/modules/consultations/schemas/consultation.schema.ts
  • src/modules/embeddings/embeddings.service.ts
  • src/modules/matches/matches.controller.spec.ts
  • src/modules/matches/matches.controller.ts
  • src/modules/matches/matches.module.ts
  • src/modules/matches/matches.service.spec.ts
  • src/modules/matches/matches.service.ts
  • src/modules/matches/schemas/match-feedback.schema.ts
  • src/modules/matches/schemas/match-log.schema.ts
  • src/modules/partners/partners.controller.spec.ts
  • src/modules/partners/partners.module.ts
  • src/modules/partners/partners.service.spec.ts
  • src/modules/partners/partners.service.ts
  • src/modules/payments/dto/create-payment.dto.ts
  • src/modules/payments/payments.service.ts
  • src/modules/payments/schemas/payment.schema.ts
  • src/modules/sellers/dto/create-seller.dto.ts
  • src/modules/sellers/dto/query-seller.dto.ts
  • src/modules/sellers/dto/update-seller.dto.ts
  • src/modules/sellers/schemas/seller.schema.ts
  • src/modules/sellers/sellers.controller.spec.ts
  • src/modules/sellers/sellers.controller.ts
  • src/modules/sellers/sellers.module.ts
  • src/modules/sellers/sellers.service.spec.ts
  • src/modules/sellers/sellers.service.ts
  • src/modules/users/schemas/buyer.schema.ts
  • src/modules/users/schemas/user-buyer.schema.ts
  • src/modules/users/schemas/user-seller.schema.ts
  • src/modules/users/schemas/user.schema.ts
  • src/modules/users/users.module.ts
💤 Files with no reviewable changes (4)
  • .env.test
  • src/modules/companies/dto/update-company.dto.ts
  • src/modules/users/schemas/buyer.schema.ts
  • src/modules/companies/companies.module.ts

Comment thread .coderabbit.yaml Outdated
Comment thread src/config/env.config.ts Outdated
Comment thread src/modules/buyers/buyers.service.ts Outdated
Comment thread src/modules/consultations/schemas/consultation.schema.ts
Comment thread src/modules/partners/partners.service.ts
Comment thread src/modules/partners/partners.service.ts Outdated
Comment thread src/modules/payments/schemas/payment.schema.ts
Comment thread src/modules/sellers/sellers.controller.ts
Comment thread src/modules/sellers/sellers.service.ts
Comment thread src/modules/users/schemas/user.schema.ts
Takch02 and others added 7 commits May 2, 2026 13:08
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/modules/partners/partners.service.ts (1)

597-608: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

웹 결과의 국가 보정이 옛 필드명을 보고 있습니다.

위에서 웹 결과는 profileText로 내려주는데, 여기서는 sellerIntroduction을 읽고 있어서 소개문구에 있는 국가 정보가 보정에 반영되지 않습니다. item.profileText로 바꿔야 웹 폴백의 랭킹이 의도대로 동작합니다.

수정 예시
-            (item.sellerIntroduction || "") +
+            (item.profileText || "") +
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/partners/partners.service.ts` around lines 597 - 608, The
country-based score adjustment in the mappedWebResults mapping is using the old
field sellerIntroduction instead of the actual web result field profileText, so
country mentions in the real fallback text are ignored; update the mapping
inside the block that checks intentData?.country to build the text from
item.profileText (and item.name as before) and keep the same logic that reduces
score (item.score * 0.7) when the lowercased text does not include countryLower,
ensuring you reference mappedWebResults, intentData?.country, sellerIntroduction
-> profileText, and the score multiplication logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/env.config.ts`:
- Around line 6-10: The uri assignment using the nullish coalescing operator
allows empty or whitespace-only MONGODB_URI to pass; update the logic in
src/config/env.config.ts (the uri field) to read process.env.MONGODB_URI, trim
it, and treat an empty/whitespace string as missing—i.e., if
process.env.MONGODB_URI is undefined/null or process.env.MONGODB_URI.trim() is
falsy, throw the Error("MONGODB_URI env var is required") so blank values fail
fast during configuration.

In `@src/modules/partners/partners.service.ts`:
- Around line 197-355: The hybrid AI + search branch currently runs even when
the query q is empty, causing missing score/NaN issues; update the conditional
that gates HyDE and searches so that generateHyDEAndKeywords, the textSearchTask
and vectorSearchTask (including embeddingsService.embed and Promise.all([...]))
only run when q is non-empty (e.g. add a guard if (q && !forceWebSearch) or
early return/skip inside the existing if (!forceWebSearch) block), and route
empty-q flows directly to the browse/show-all path so hydeDocument, textResults
and vectorResults are not produced for blank queries.
- Around line 213-223: The code passes raw user input `industry` directly into
RegExp construction (used in the `filter` clauses such as `filter.$and` with
`industry_kr`/`industry_en`), which can break searches when `industry` contains
regex metacharacters; import and reuse the existing `escapeRegex` helper from
`src/modules/buyers/buyers.service.ts` to sanitize `industry` before building
the `{ $regex: ... , $options: "i" }` queries, and apply this change
consistently for all three occurrences where `industry` is used in regex filters
(the `isBuyerSearch` branch and the other two similar blocks).

---

Outside diff comments:
In `@src/modules/partners/partners.service.ts`:
- Around line 597-608: The country-based score adjustment in the
mappedWebResults mapping is using the old field sellerIntroduction instead of
the actual web result field profileText, so country mentions in the real
fallback text are ignored; update the mapping inside the block that checks
intentData?.country to build the text from item.profileText (and item.name as
before) and keep the same logic that reduces score (item.score * 0.7) when the
lowercased text does not include countryLower, ensuring you reference
mappedWebResults, intentData?.country, sellerIntroduction -> profileText, and
the score multiplication logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5f0154e5-19bd-425c-9fc1-cc4329e29b9b

📥 Commits

Reviewing files that changed from the base of the PR and between 8acfc4b and a69cc13.

📒 Files selected for processing (7)
  • .coderabbit.yaml
  • src/config/env.config.ts
  • src/modules/buyers/buyers.service.ts
  • src/modules/partners/partners.controller.ts
  • src/modules/partners/partners.service.spec.ts
  • src/modules/partners/partners.service.ts
  • src/modules/sellers/sellers.service.ts
✅ Files skipped from review due to trivial changes (1)
  • .coderabbit.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/modules/sellers/sellers.service.ts

Comment thread src/config/env.config.ts
Comment on lines +197 to +355
if (!forceWebSearch) {
const aiStartTime = performance.now();

if (!forceWebSearch && vector.length > 0) {
const pipeline: any[] = [
{
$vectorSearch: {
index: process.env.ATLAS_VECTOR_INDEX || "vector_index",
path: "embedding",
queryVector: vector,
numCandidates: 100,
limit: Number(limit) * 2,
// 1.0 AI Query Understanding (HyDE + Keywords)
const aiAnalysis = await this.generateHyDEAndKeywords(q, detectedIntent);
hydeDocument = aiAnalysis.profile;
aiKeywords = aiAnalysis.keywords;

this.logger.log(
`[Step 2.AI Analysis] took ${Math.round(performance.now() - aiStartTime)}ms. Keywords: "${aiKeywords}"`,
);

// Define search tasks using AI output
const textSearchTask = (async () => {
const textStartTime = performance.now();
const filter: Record<string, any> = { $text: { $search: aiKeywords } };
if (industry) {
if (isBuyerSearch) {
filter.$and = [
{ $text: { $search: aiKeywords } },
{
$or: [
{ industry_kr: { $regex: industry, $options: "i" } },
{ industry_en: { $regex: industry, $options: "i" } },
],
},
];
} else {
filter.industry = INDUSTRY_MAPPING[industry]
? { $in: INDUSTRY_MAPPING[industry] }
: industry;
}
}
if (country) {
if (isBuyerSearch) filter.country = country;
else filter["location.country"] = country;
}

const projection = isBuyerSearch
? {
name_kr: 1,
name_en: 1,
industry_kr: 1,
industry_en: 1,
country: 1,
intro_kr: 1,
intro_en: 1,
website: 1,
email: 1,
updatedAt: 1,
score: { $meta: "textScore" },
}
: { ...SEARCH_PROJECTION, score: { $meta: "textScore" } };

try {
const raw = await activeModel
.find(filter as any)
.select(projection as any)
.sort({ score: { $meta: "textScore" } } as any)
.limit(100)
.lean();
textResults = raw.map((r: any) => {
const common = isBuyerSearch ? mapBuyerToCommon(r) : r;
return { ...common, textScore: r.score };
});
this.logger.log(
`[Step 2.Text] Found ${textResults.length} items. took ${Math.round(performance.now() - textStartTime)}ms`,
);
} catch (err: any) {
this.logger.error(`[Search] Text search error: ${err.message}`);
}
})();

const vectorSearchTask = (async () => {
// 2.1 Embedding
const embedStartTime = performance.now();
try {
vector = await this.embeddingsService.embed(hydeDocument);
this.logger.log(
`[Step 2.Embed] took ${Math.round(performance.now() - embedStartTime)}ms`,
);
} catch {
return;
}

// 2.2 Vector Search
const indexName = isBuyerSearch
? process.env.ATLAS_BUYER_VECTOR_INDEX || "buyer_vector_index"
: process.env.ATLAS_VECTOR_INDEX || "sellers_vector_index";

const pipeline: any[] = [
{
$vectorSearch: {
index: indexName,
path: "embedding",
queryVector: vector,
numCandidates: 100,
limit: 100,
},
},
},
];
// Seller 만 검색함 (필터 적용)
const matchStage: Record<string, any> = { type: "seller" };
if (industry) {
matchStage.industry = INDUSTRY_MAPPING[industry]
? { $in: INDUSTRY_MAPPING[industry] }
: industry;
}
if (country) matchStage["location.country"] = country;
if (partnership) matchStage.tags = partnership;
if (size) matchStage.sizeBucket = size;

if (!matchStage.industry && predictedIndustry) {
matchStage.industry = INDUSTRY_MAPPING[predictedIndustry]
? { $in: INDUSTRY_MAPPING[predictedIndustry] }
: predictedIndustry;
}
];

if (!matchStage.industry) {
matchStage.industry = {
$not: { $regex: /Investment|Fund|Asset|Capital/i },
};
matchStage.name = {
$not: { $regex: /Investment|Fund|Asset|Capital/i },
};
}
const matchStage: Record<string, any> = {};
if (industry) {
if (isBuyerSearch) {
matchStage.$or = [
{ industry_kr: { $regex: industry, $options: "i" } },
{ industry_en: { $regex: industry, $options: "i" } },
];
} else {
matchStage.industry = INDUSTRY_MAPPING[industry]
? { $in: INDUSTRY_MAPPING[industry] }
: industry;
}
}
if (country) {
if (isBuyerSearch) matchStage.country = country;
else matchStage["location.country"] = country;
}
if (Object.keys(matchStage).length > 0)
pipeline.push({ $match: matchStage });

if (Object.keys(matchStage).length > 0) {
pipeline.push({ $match: matchStage });
}
if (isBuyerSearch) {
pipeline.push({
$project: {
name_kr: 1,
name_en: 1,
industry_kr: 1,
industry_en: 1,
country: 1,
intro_kr: 1,
intro_en: 1,
website: 1,
email: 1,
updatedAt: 1,
score: { $meta: "vectorSearchScore" },
},
});
} else {
pipeline.push({
$project: {
...SEARCH_PROJECTION,
score: { $meta: "vectorSearchScore" },
},
});
}

pipeline.push({
$project: {
...SEARCH_PROJECTION,
score: { $meta: "vectorSearchScore" },
},
});
pipeline.push({ $limit: Number(limit) });
try {
const raw = await activeModel.aggregate(pipeline);
vectorResults = isBuyerSearch ? raw.map(mapBuyerToCommon) : raw;
this.logger.log(
`[Step 2.Vector] Found ${vectorResults.length} items`,
);
} catch (err: any) {
this.logger.error(`[Search] Vector search error: ${err.message}`);
}
})();

try {
dbResults = await this.companyModel.aggregate(pipeline);
} catch (err: any) {
this.logger.error(`[Search] Vector search error: ${err.message}`);
dbResults = [];
}
await Promise.all([textSearchTask, vectorSearchTask]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

q에서도 하이브리드 검색을 타지 마세요.

q가 없어도 이 블록이 실행돼 OpenAI/벡터/텍스트 검색까지 돌고, 현재처럼 score가 없는 결과가 섞이면 병합 단계에서 NaN이 나옵니다. q가 있을 때만 HyDE/검색 블록을 실행하고, 빈 q는 아래 browse/show-all 경로로 바로 넘기세요.

수정 예시
-    if (!forceWebSearch) {
+    if (q && !forceWebSearch) {

Also applies to: 358-410

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/partners/partners.service.ts` around lines 197 - 355, The hybrid
AI + search branch currently runs even when the query q is empty, causing
missing score/NaN issues; update the conditional that gates HyDE and searches so
that generateHyDEAndKeywords, the textSearchTask and vectorSearchTask (including
embeddingsService.embed and Promise.all([...])) only run when q is non-empty
(e.g. add a guard if (q && !forceWebSearch) or early return/skip inside the
existing if (!forceWebSearch) block), and route empty-q flows directly to the
browse/show-all path so hydeDocument, textResults and vectorResults are not
produced for blank queries.

Comment on lines +213 to +223
if (industry) {
if (isBuyerSearch) {
filter.$and = [
{ $text: { $search: aiKeywords } },
{
$or: [
{ industry_kr: { $regex: industry, $options: "i" } },
{ industry_en: { $regex: industry, $options: "i" } },
],
},
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --type ts 'escapeRegExp|escapeRegex|sanitize.*regex' src

Repository: K-Statra/backend

Length of output: 347


🏁 Script executed:

# Check the exact code at the specified lines in partners.service.ts
head -n 230 src/modules/partners/partners.service.ts | tail -n 20

Repository: K-Statra/backend

Length of output: 755


🏁 Script executed:

# Check other mentioned line ranges
head -n 310 src/modules/partners/partners.service.ts | tail -n 15

Repository: K-Statra/backend

Length of output: 544


🏁 Script executed:

# Check the third range
head -n 435 src/modules/partners/partners.service.ts | tail -n 15

Repository: K-Statra/backend

Length of output: 556


🏁 Script executed:

# Check if partners.service.ts imports escapeRegex from anywhere
rg -n 'escapeRegex|import.*buyers' src/modules/partners/partners.service.ts

Repository: K-Statra/backend

Length of output: 133


🏁 Script executed:

# Check if there's any escape function defined in partners.service.ts
rg -n 'escape|sanitize' src/modules/partners/partners.service.ts | head -20

Repository: K-Statra/backend

Length of output: 42


industry 값을 정규식에 그대로 전달하면 특수 문자로 인한 필터 오류 또는 결과 오염 발생 가능

사용자 입력인 industry가 정규식 메타문자(., *, +, ?, (, ) 등)를 포함할 경우 의도하지 않은 필터링이 발생할 수 있습니다. 3곳 모두에서 동일한 패턴이 반복되므로, 이미 src/modules/buyers/buyers.service.ts에 정의된 escapeRegex 헬퍼를 재사용하세요.

-                  { industry_kr: { $regex: industry, $options: "i" } },
-                  { industry_en: { $regex: industry, $options: "i" } },
+                  { industry_kr: { $regex: escapeRegex(industry), $options: "i" } },
+                  { industry_en: { $regex: escapeRegex(industry), $options: "i" } },

Also applies to: 299-310, 420-430

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/partners/partners.service.ts` around lines 213 - 223, The code
passes raw user input `industry` directly into RegExp construction (used in the
`filter` clauses such as `filter.$and` with `industry_kr`/`industry_en`), which
can break searches when `industry` contains regex metacharacters; import and
reuse the existing `escapeRegex` helper from
`src/modules/buyers/buyers.service.ts` to sanitize `industry` before building
the `{ $regex: ... , $options: "i" }` queries, and apply this change
consistently for all three occurrences where `industry` is used in regex filters
(the `isBuyerSearch` branch and the other two similar blocks).

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