Conversation
|
Warning Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories. 📝 WalkthroughWalkthrough대규모 도메인 리팩토링: Company/CompanyModule 중심 모델을 Seller/UserSeller/UserBuyer 중심으로 전환하고 관련 스키마, DTO, 서비스, 모듈 의존성, 검색/매칭/결제/상담 경로 및 환경 설정을 일괄 업데이트했습니다. Changes도메인 · 스키마 · DTO 변경 (데이터 형태)
핵심 서비스 로직 변경
모듈/와이어링(등록·DI·라우팅) 변경
테스트 · 사소한 파일 변경 · 환경설정
(참고: 변경 파일 수가 많아 위 표는 리뷰 순서 관점의 계층화이며, 실제 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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분 Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
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
tag와partnership를 함께 전달하면 필터가 덮어써집니다.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 winSwagger/에러 설명 용어를
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()에서 같은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의 필드 명명이 의미론적으로 부정확합니다.
RegisterBuyerDto가sellerName,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
📒 Files selected for processing (50)
.coderabbit.yaml.env.testsrc/app.module.tssrc/config/env.config.tssrc/modules/auth/auth.controller.spec.tssrc/modules/auth/auth.service.spec.tssrc/modules/auth/auth.service.tssrc/modules/auth/dto/register.dto.tssrc/modules/buyers/buyers.module.tssrc/modules/buyers/buyers.service.spec.tssrc/modules/buyers/buyers.service.tssrc/modules/buyers/dto/query-buyer.dto.tssrc/modules/buyers/schemas/buyer.schema.tssrc/modules/companies/companies.module.tssrc/modules/companies/dto/update-company.dto.tssrc/modules/consultations/consultations.controller.tssrc/modules/consultations/consultations.module.tssrc/modules/consultations/consultations.service.tssrc/modules/consultations/dto/create-consultation.dto.tssrc/modules/consultations/dto/query-consultation.dto.tssrc/modules/consultations/schemas/consultation.schema.tssrc/modules/embeddings/embeddings.service.tssrc/modules/matches/matches.controller.spec.tssrc/modules/matches/matches.controller.tssrc/modules/matches/matches.module.tssrc/modules/matches/matches.service.spec.tssrc/modules/matches/matches.service.tssrc/modules/matches/schemas/match-feedback.schema.tssrc/modules/matches/schemas/match-log.schema.tssrc/modules/partners/partners.controller.spec.tssrc/modules/partners/partners.module.tssrc/modules/partners/partners.service.spec.tssrc/modules/partners/partners.service.tssrc/modules/payments/dto/create-payment.dto.tssrc/modules/payments/payments.service.tssrc/modules/payments/schemas/payment.schema.tssrc/modules/sellers/dto/create-seller.dto.tssrc/modules/sellers/dto/query-seller.dto.tssrc/modules/sellers/dto/update-seller.dto.tssrc/modules/sellers/schemas/seller.schema.tssrc/modules/sellers/sellers.controller.spec.tssrc/modules/sellers/sellers.controller.tssrc/modules/sellers/sellers.module.tssrc/modules/sellers/sellers.service.spec.tssrc/modules/sellers/sellers.service.tssrc/modules/users/schemas/buyer.schema.tssrc/modules/users/schemas/user-buyer.schema.tssrc/modules/users/schemas/user-seller.schema.tssrc/modules/users/schemas/user.schema.tssrc/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
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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
.coderabbit.yamlsrc/config/env.config.tssrc/modules/buyers/buyers.service.tssrc/modules/partners/partners.controller.tssrc/modules/partners/partners.service.spec.tssrc/modules/partners/partners.service.tssrc/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
| 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]); |
There was a problem hiding this comment.
빈 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.
| if (industry) { | ||
| if (isBuyerSearch) { | ||
| filter.$and = [ | ||
| { $text: { $search: aiKeywords } }, | ||
| { | ||
| $or: [ | ||
| { industry_kr: { $regex: industry, $options: "i" } }, | ||
| { industry_en: { $regex: industry, $options: "i" } }, | ||
| ], | ||
| }, | ||
| ]; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n --type ts 'escapeRegExp|escapeRegex|sanitize.*regex' srcRepository: 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 20Repository: 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 15Repository: K-Statra/backend
Length of output: 544
🏁 Script executed:
# Check the third range
head -n 435 src/modules/partners/partners.service.ts | tail -n 15Repository: 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.tsRepository: 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 -20Repository: 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).
#7
1. 개요 (Summary)
서비스 내 도메인 용어의 명확성을 확보하고, 계정 정보와 비즈니스 정보를 분리하여 데이터 확장성을 높이는 작업을 진행했습니다.
2. 주요 변경 사항 (Key Changes)
3. 변경 전/후 구조 비교 (Architecture)
* Vector Search를 위해 약 5000개 Buyer 정보를 Embedding 하여 넣음. (Buyer 컬럼 수정 이유)
4. 영향도 (Impact)
Summary by CodeRabbit
New Features
Changes
Bug Fixes