Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend has been migrated to a separate repository; backend source is promoted from backend/ to the repo root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove frontend build job (migrated to separate repo) - Fix working-directory: drop backend/ prefix now that source is at root - Fix cache-dependency-path: use relative package-lock.json - deploy now only waits on test-backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile: remove backend/ path prefix now that source is at repo root. README: remove frontend/monorepo references. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace frontend/dist/ with dist/ now that this repo is backend-only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move all NestJS source (src/, test/) and config files (tsconfig*, nest-cli.json, eslint.config.mjs) from backend/ subdirectory to repo root. Remove old Express/JS files: industries.js, Consultation.js, consultations.js. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Swap the minimal monorepo root package with the full NestJS package (scripts, dependencies, jest config, lint-staged paths updated for root). 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>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Warning Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories. 📝 WalkthroughWalkthrough대규모 아키텍처 리팩토링으로, 통합된 사용자 인증 시스템과 MongoDB Mongoose 판별자 기반 스키마를 도입하고, 관리자/인사이트/컨설턴트 모듈을 제거하며, Redis 세션 및 XRPL 지갑 통합을 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Server as NestJS Server
participant Session as Redis Session
participant AuthService
participant XrplService
participant DB as MongoDB
Client->>Server: POST /auth/register (email, password, name)
Server->>AuthService: registerSeller/registerBuyer(dto)
AuthService->>AuthService: Hash password (bcrypt)
AuthService->>XrplService: generateWallet()
XrplService->>XrplService: Create XRPL wallet
XrplService->>XrplService: Encrypt seed (AES-256-GCM)
AuthService->>DB: Save Company/Buyer (status: PENDING_ACTIVATION)
AuthService->>XrplService: fundAccount(wallet.address)
XrplService->>XrplService: Fund testnet account
AuthService->>DB: Update status to ACTIVE
AuthService->>Server: Return user (without password/seed)
Server->>Client: 201 Created
sequenceDiagram
actor Client
participant Server as NestJS Server
participant Session as Redis Session
participant AuthService
participant DB as MongoDB
Client->>Server: POST /auth/login (email, password)
Server->>AuthService: login(dto)
AuthService->>DB: Find User by email
AuthService->>AuthService: Compare password (bcrypt)
AuthService->>AuthService: Return user (password-redacted)
Server->>Session: req.session.userId = user._id
Server->>Session: req.session.type = "buyer"/"seller"
Server->>Client: 200 로그인 성공
Note over Client,Session: Subsequent requests
Client->>Server: GET /api/resource
Server->>Server: SessionGuard.canActivate()
Server->>Session: Check req.session.userId
alt userId exists
Server->>Server: Allow request
else userId missing
Server->>Client: 401 Login required
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/modules/companies/dto/create-company.dto.ts (1)
57-67:⚠️ Potential issue | 🟡 Minor
companyIntroduction/productIntroduction필드의 필수 여부가 스키마 및 다른 DTO와 불일치합니다.
src/modules/users/schemas/user.schema.ts에서 두 필드는@Prop({ required: true })로 선언되어 있고,RegisterBuyerDto/RegisterSellerDto에서도@IsNotEmpty()로 필수 처리되어 있습니다. 그러나 본 DTO에서는@IsOptional()로 지정되어 있어, 컨트롤러 레벨에서는 검증을 통과하지만 Mongoose 단계에서 저장 실패가 발생합니다.DTO에서도 필수로 통일하십시오.
UpdateCompanyDto는 이미PartialType(CreateCompanyDto)를 사용하므로 자동으로 부분 업데이트를 지원합니다.제안된 변경
- `@ApiPropertyOptional`({ example: "한국 자동차 부품 제조사입니다." }) - `@IsOptional`() + `@ApiProperty`({ example: "한국 자동차 부품 제조사입니다." }) `@IsString`() + `@IsNotEmpty`() `@MaxLength`(2000) - companyIntroduction?: string; + companyIntroduction: string; - `@ApiPropertyOptional`({ example: "EV 배터리 팩 및 BMS를 생산합니다." }) - `@IsOptional`() + `@ApiProperty`({ example: "EV 배터리 팩 및 BMS를 생산합니다." }) `@IsString`() + `@IsNotEmpty`() `@MaxLength`(2000) - productIntroduction?: string; + productIntroduction: string;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/companies/dto/create-company.dto.ts` around lines 57 - 67, The CreateCompanyDto currently marks companyIntroduction and productIntroduction as optional but the schema (user.schema.ts with `@Prop`({ required: true })) and RegisterBuyerDto/RegisterSellerDto expect them to be required; remove `@IsOptional`() and add `@IsNotEmpty`() (keep `@IsString`() and `@MaxLength`(2000)) on both companyIntroduction and productIntroduction in CreateCompanyDto so validation aligns with the Mongoose schema and other DTOs; UpdateCompanyDto will continue to work via PartialType(CreateCompanyDto).src/modules/matches/matches.service.spec.ts (1)
32-41:⚠️ Potential issue | 🟡 Minor
makeCompany픽스처가 서비스 변경과 어긋남 — 새 스코어링 브랜치 미커버서비스가
company.offerings→company.exportItems로 바뀌고dart기반 보너스도 제거되었는데, 이 픽스처는 여전히offerings/dart를 들고 있습니다. 결과적으로:
offerings/dart는 테스트에서 dead field가 됩니다.- 어떤 테스트도
exportItems를 세팅하지 않아, 새 스코어링 라인needs-exportItems overlap(matches.service.ts L66–70)이 한 번도 검증되지 않습니다.리네이밍 회귀를 잡을 수 있도록 픽스처를 정리하고, 해당 브랜치에 대한 단언을 한 건 추가해 주세요.
♻️ 제안 수정
const makeCompany = (overrides = {}) => ({ _id: COMPANY_ID, tags: ["전기차"], industry: "자동차", - offerings: ["공급망"], + exportItems: ["공급망"], updatedAt: new Date(), - dart: null, embedding: [], ...overrides, });추가로
findMatchesdescribe 블록 안에needs-exportItems오버랩이 score/reason에 반영되는지 검증하는 케이스를 추가하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/matches/matches.service.spec.ts` around lines 32 - 41, The makeCompany test fixture still uses company.offerings and dart which are dead fields after the service refactor; update makeCompany to provide exportItems (replacing offerings) and remove dart, and ensure embedding/other defaults remain; then add a new spec inside the findMatches describe that constructs a company with exportItems overlapping the need and asserts the scoring branch in matches.service (the "needs-exportItems overlap" logic in findMatches / matches.service.ts lines ~66–70) produces the expected score and reason. Use the makeCompany helper and the findMatches test helpers so the new test targets the specific overlap branch (check score/reason) rather than relying on old offerings/dart fields.src/modules/matches/matches.service.ts (1)
50-70:⚠️ Potential issue | 🟠 Major테스트 데이터가 스키마와 맞지 않습니다. 저장된 회사 문서의
exportItems마이그레이션을 확인하세요.
Company스키마는 이미exportItems로 정의되어 있고offerings필드는 없습니다. 그러나 테스트 mock 데이터(makeCompany)는 여전히offerings을 사용하고 있으며, 실제exportItems채우기 테스트는 없습니다.저장된 회사 문서들이 정말로
exportItems필드를 가지고 있는지 확인이 필요합니다. 기존 데이터에 이 필드가 없으면 needs-exportItems 오버랩 점수가 조용히 0이 되어 매칭 품질이 저하될 수 있습니다.개선 사항
- 테스트 mock 데이터를
exportItems로 업데이트하세요 (offerings제거)- 저장된 모든 회사 문서가
exportItems를 포함하는지 데이터베이스 검증을 추가하세요🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/matches/matches.service.ts` around lines 50 - 70, The tests and runtime disagree on the company field name: test factory makeCompany still populates offerings while the code and Company schema use exportItems, causing intersectCount/buyerNeeds vs companyExportItems to be empty and needMatches to be 0; update the test mock makeCompany to populate exportItems (remove offerings) and add a DB validation/migration check to ensure persisted Company documents include exportItems (or backfill offerings->exportItems) so that the logic in matches.service (variables companyExportItems, needMatches, intersectCount) sees real data.src/modules/partners/partners.service.ts (1)
393-405:⚠️ Potential issue | 🟠 Major잔존하는
profileText참조 — 필드명 마이그레이션 누락
profileText→companyIntroduction리네이밍에 맞춰 위쪽 mapping(381–390)은companyIntroduction: item.content로 갱신되었지만, 여기 country 필터에서는 여전히item.profileText를 참조합니다. 그 결과 매핑된 웹 결과에는profileText가 존재하지 않아text는 사실상item.name만 포함하게 되고, 국가 매칭 페널티(score * 0.7)가 의도와 다르게 동작합니다.🐛 제안 수정
if (intentData?.country) { const countryLower = (intentData.country as string).toLowerCase(); mappedWebResults = mappedWebResults.map((item: any) => { const text = ( - (item.profileText || "") + + (item.companyIntroduction || "") + " " + (item.name || "") ).toLowerCase(); if (!text.includes(countryLower)) return { ...item, score: item.score * 0.7 }; return item; }); }🤖 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 393 - 405, The country filter still reads item.profileText which was renamed during mapping; update the block in the intentData?.country handling that builds text to use item.companyIntroduction (or fallback to item.content if you prefer the original source) instead of item.profileText so mappedWebResults entries include the company introduction when checking country and applying the score *= 0.7 penalty (refer to mappedWebResults, intentData, item.profileText, companyIntroduction, and score).
🧹 Nitpick comments (22)
.env.test (1)
1-37: 테스트 환경 디폴트가 운영 안전 수칙과 충돌합니다.
CORS_ORIGINS=*는 테스트 환경이라도 운영 설정에서 우연히 재사용되면 위험합니다. 테스트에서도 가급적http://localhost:4000등 명시적 origin을 권장합니다.MONGODB_URI=mongodb://localhost:27017는 DB 이름이 빠져 있어 다른 로컬 DB와 충돌할 수 있습니다. 전용 테스트 DB(...:27017/kstatra_test)로 분리하면 데이터 오염을 방지할 수 있습니다.dotenv-linter가 보고한 키 정렬 경고는 정렬 컨벤션을 강제할 계획이 없다면 무시해도 무방합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.env.test around lines 1 - 37, The .env.test has unsafe defaults: change CORS_ORIGINS from wildcard to a specific test origin (e.g., use localhost origin) and update MONGODB_URI to include a dedicated test database name (e.g., append /kstatra_test) to avoid accidental reuse of production/local DBs; locate and edit the CORS_ORIGINS and MONGODB_URI entries in the file to replace "*" and the bare host URI respectively, and ignore or address dotenv-linter ordering warnings per repo policy.src/modules/auth/dto/register.dto.ts (3)
32-35: 비밀번호 정책이MinLength(8)한 가지뿐입니다.회원가입 단계에서 영문/숫자/특수문자 조합 등 최소 복잡도 정책을 함께 강제하지 않으면
12345678같은 약한 비밀번호도 통과합니다. MVP 이후 정식 정책 도입 전이라도@Matches(/^(?=.*[A-Za-z])(?=.*\d).{8,}$/)정도의 가벼운 가드를 추가하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/auth/dto/register.dto.ts` around lines 32 - 35, The password field in RegisterDto currently only uses `@MinLength`(8) so weak passwords like "12345678" pass; update the password validation by adding a `@Matches`(...) decorator on the password property (the password field in register.dto.ts / RegisterDto) to enforce at least letters and digits (for example a regex like /^(?=.*[A-Za-z])(?=.*\d).{8,}$/) or a slightly stricter pattern including special characters, and adjust the `@ApiProperty` example/description accordingly so the API docs reflect the new complexity requirement.
38-113:RegisterBuyerDto/RegisterSellerDto의 공통 필드를 베이스로 끌어올리는 것을 권장합니다.
companyIntroduction,productIntroduction,websiteUrl,tags가 두 서브클래스에서 동일 검증 규칙으로 반복되고 있습니다.RegisterCommonDto로 옮기면 향후 검증 규칙 변경 시 한 곳만 수정하면 되고,create-company.dto.ts와의 일관성 유지도 쉬워집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/auth/dto/register.dto.ts` around lines 38 - 113, Move the duplicated fields companyIntroduction, productIntroduction, websiteUrl, and tags from RegisterBuyerDto and RegisterSellerDto into the shared RegisterCommonDto so both subclasses inherit the same validation/decorator rules; update RegisterCommonDto to declare these properties with the same class-validator and `@ApiProperty/`@ApiPropertyOptional decorators currently used, remove the duplicated declarations from RegisterBuyerDto and RegisterSellerDto, and adjust imports/usages if necessary to ensure the DTOs compile and remain consistent with create-company.dto.ts.
27-30:representativePhone형식 검증이 없습니다.
@IsString() +@isnotempty()만 있어 임의 문자열도 허용됩니다. 예시(010-1234-5678)에 맞춰class-validator의@IsPhoneNumber('KR')를 사용하거나,@Matches(/^\d{2,3}-\d{3,4}-\d{4}$/)를 추가할 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/auth/dto/register.dto.ts` around lines 27 - 30, The representativePhone DTO field currently only has `@IsString`() and `@IsNotEmpty`(), allowing arbitrary strings; update the Register DTO (the representativePhone property in register.dto.ts / the DTO class containing representativePhone) to enforce phone format by adding a validator such as `@IsPhoneNumber`('KR') or `@Matches`(/^\d{2,3}-\d{3,4}-\d{4}$/) alongside the existing decorators so the value must match the example "010-1234-5678".src/types/express-session.d.ts (1)
4-7:SessionData필드는 선택적(optional)으로 선언하는 것을 권장합니다.미인증 요청에서는
req.session.userId와req.session.type이 실제로undefined입니다. 현재처럼 필수(required)로 선언하면SessionGuard의if (!req.session.userId)같은 체크가 타입 시스템상 의미를 잃고, 다른 코드에서 미인증 세션의 값을 안전하게 사용한다고 잘못 가정할 위험이 생깁니다.♻️ 제안 diff
declare module "express-session" { interface SessionData { - userId: string; - type: "buyer" | "seller"; + userId?: string; + type?: "buyer" | "seller"; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/types/express-session.d.ts` around lines 4 - 7, Make SessionData's userId and type optional so unauthenticated requests can have undefined session fields: update the interface SessionData to declare userId?: string and type?: "buyer" | "seller"; then ensure related checks like in SessionGuard (e.g., if (!req.session.userId)) remain valid under the type system and adjust any downstream code that assumed non-null session values to handle undefined safely.src/main.ts (2)
20-21:session.secret의 non-null 단언(!) 대신 명시적 검증 권장.
env.config.ts의 기본값이"dev-secret-change-in-production"이라 현재는undefined가 되지 않지만, 향후 기본값이 제거되면!가 런타임에 무성한undefined시크릿을 그대로 통과시켜 세션 서명이 깨질 수 있습니다. 또한 운영 환경에서 기본 시크릿이 그대로 사용되는 것을 막는 가드도 함께 두면 안전합니다.🛡️ 제안 diff
- const sessionSecret = configService.get<string>("session.secret")!; - const sessionTtl = configService.get<number>("session.ttl")!; + const sessionSecret = configService.get<string>("session.secret"); + const sessionTtl = configService.get<number>("session.ttl"); + if (!sessionSecret || !sessionTtl) { + throw new Error("session.secret / session.ttl 설정이 누락되었습니다."); + } + if ( + process.env.NODE_ENV === "production" && + sessionSecret === "dev-secret-change-in-production" + ) { + throw new Error("운영 환경에서 기본 session secret 사용은 금지됩니다."); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.ts` around lines 20 - 21, Replace the non-null assertions for session.secret and session.ttl with explicit validation: read them via configService.get (sessionSecret and sessionTtl), verify sessionSecret is a non-empty string (and if in non-development environment ensure it is not the default dev value from env.config.ts), and verify sessionTtl is a positive number; if any check fails, throw an error or terminate startup with a clear message. Update the initialization logic around the symbols sessionSecret, sessionTtl and configService to perform these guards before using them.
24-32: Redis 클라이언트 설정에서ConfigService를 사용하지 않고process.env에 직접 접근하고 있습니다.같은 함수 내에서
sessionSecret/sessionTtl는ConfigService를 통해 읽으면서, Redis 호스트/포트/비밀번호는process.env를 직접 읽어 일관성이 깨져 있습니다.env.config.ts의redis블록을 통해 일관되게 가져오는 편이 단일 소스 원칙에 부합합니다.또한
REDIS_PASSWORD가 비어있을 때 빈 문자열을 그대로 전달하면, ACL 이 활성화된 Redis 에서는 빈 비밀번호로 AUTH 시도가 발생해 연결이 거절될 수 있습니다. 빈 값일 때는 필드를 생략하는 편이 안전합니다.♻️ 제안 diff
- // Redis session store - const redisClient = createClient({ - socket: { - host: process.env.REDIS_HOST || "localhost", - port: Number(process.env.REDIS_PORT) || 6379, - }, - password: process.env.REDIS_PASSWORD || "", - }); + // Redis session store + const redisHost = configService.get<string>("redis.host") ?? "localhost"; + const redisPort = Number(configService.get("redis.port")) || 6379; + const redisPassword = configService.get<string>("redis.password"); + const redisClient = createClient({ + socket: { host: redisHost, port: redisPort }, + ...(redisPassword ? { password: redisPassword } : {}), + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.ts` around lines 24 - 32, Currently createClient/redisClient reads Redis host/port/password directly from process.env while sessionSecret/sessionTtl use ConfigService; update the code to read Redis settings from ConfigService (the env.config.ts redis block) for consistency (use the same ConfigService instance used for sessionSecret/sessionTtl), pass host and port from those config values into createClient, and only include the password field when the configured password is non-empty (omit the password property entirely if blank) to avoid sending an empty AUTH to ACL-enabled Redis.docker-compose.yml (1)
11-17: Redis 이미지 태그 고정 및 인증 구성 검토 권장.
redis:alpine은 rolling 태그여서 시간이 지나면 재현 가능한 빌드가 깨질 수 있습니다.redis:7-alpine처럼 메이저 버전을 고정하는 것을 권장합니다.- 컨테이너가 호스트 6379 포트로 노출되지만 인증 설정이 없습니다.
env.config.ts의REDIS_PASSWORD가 실제로 사용될 환경이라면, Redis 서버에도--requirepass를 동일하게 적용해야 합니다(미적용 시 클라이언트의 password 옵션이 무력화됩니다).♻️ 제안 diff
redis: - image: redis:alpine + image: redis:7-alpine restart: unless-stopped + command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] ports: - "6379:6379" volumes: - redis_data:/data🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docker-compose.yml` around lines 11 - 17, Pin the Redis image to a stable tag (e.g., change the redis service image from "redis:alpine" to a fixed major version like "redis:7-alpine") and ensure the container enforces authentication by propagating REDIS_PASSWORD from env.config.ts into the Redis start command (use --requirepass or equivalent) so the client's password option is effective; update the docker-compose redis service definition to use the pinned image and pass the requirepass option referencing REDIS_PASSWORD.package.json (1)
22-22:husky || true는 prepare 스크립트 실패를 모두 삼켜 CI/설치 환경의 이상을 숨길 수 있습니다.git 디렉터리가 없는 환경(예: Docker 빌드 컨텍스트)에서 husky 설치 실패를 회피하려는 의도로 보입니다만, 모든 오류가 무시되어 husky 설정 자체의 회귀(regression)도 가려집니다. 더 좁게 처리하는 것을 권장합니다.
♻️ 제안 diff
- "prepare": "husky || true" + "prepare": "husky install || echo 'husky 설치 건너뜀 (git 디렉터리 없음)'"또는
is-ci를 활용해 CI 환경에서만 건너뛰는 구성도 일반적입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 22, The prepare script ("prepare") currently uses the broad fallback "husky || true" which masks all installation failures; replace it with a narrower guard that only skips husky when appropriate (e.g., when no git metadata is available or when running in CI). Update the "prepare" script to explicitly check for a git repo or an environment flag (e.g., use is-ci or test for .git/GIT_DIR) before running husky, so failures installing/configuring husky are surfaced in normal dev environments while still skipping in Docker/CI contexts.src/modules/auth/auth.module.ts (1)
9-9:ScheduleModule.forRoot()는 루트 모듈(AppModule)로 이동하세요.NestJS 공식 문서에서는
ScheduleModule.forRoot()를 오직 루트 AppModule에서만 등록하도록 권장합니다. 현재 auth.module.ts(기능 모듈)에서 등록하는 것은 NestJS 아키텍처 모범 사례를 위반합니다.현재는 단일 등록이라 기능상 문제없지만, 다른 모듈들이 같은 패턴을 따르면 스케줄러가 중복 초기화되어 cron 핸들러가 여러 번 트리거될 위험이 있습니다. 또한 ScheduleModule은 글로벌 모듈이므로 모든 서비스에서
@Cron,@Interval등의 데코레이터를 사용할 수 있어, 기능 모듈에서 명시적으로 등록할 필요가 없습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/auth/auth.module.ts` at line 9, Move ScheduleModule.forRoot() out of the feature module: remove ScheduleModule.forRoot() from the imports array in AuthModule (auth.module.ts) and add ScheduleModule.forRoot() to the imports of the root AppModule so the scheduler is initialized only once; keep using `@Cron/`@Interval in feature services without importing ScheduleModule again. Ensure no other feature modules import ScheduleModule.forRoot() to avoid duplicate initialization.src/modules/buyers/buyers.module.ts (1)
7-10:UsersModulere-export 패턴 — 의존성 명시화 검토 권장
BuyersModule이UsersModule을 re-export하면ConsultationsModule등 소비자가BuyersModule만 import해도Buyer/User디스크리미네이터 바인딩을 받게 되어 실제 의존성이 가려집니다. 디스크리미네이터 등록 자체는UsersModule의 책임이므로, 소비자 측에서UsersModule을 직접 import하도록 정리하면 모듈 경계가 더 명확해집니다. 단기 동작에는 문제 없으니 후속 리팩터로 검토해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/buyers/buyers.module.ts` around lines 7 - 10, BuyersModule currently re-exports UsersModule which hides the real dependency from consumers; change BuyersModule so that UsersModule remains in imports (so BuyersModule can use it) but is removed from the exports array — keep exports: [BuyersService] (and other local exports) so consumers like ConsultationsModule must explicitly import UsersModule; update BuyersModule's exports list and any module documentation to reflect that BuyersModule no longer re-exports UsersModule.src/common/guards/session.guard.ts (1)
10-16:req에 명시적 타입 부여 권장 (정적 분석 경고 해소)
getRequest()호출 시 타입 파라미터를 지정하지 않으면req가any로 추론되어 타입 안정성 경고가 발생합니다.<Request>제네릭을 추가하면src/types/express-session.d.ts에서 정의한SessionDataaugmentation이 활성화되어userId및type접근이 안전하게 타입체크됩니다.♻️ 제안 수정
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, } from "@nestjs/common"; +import type { Request } from "express"; `@Injectable`() export class SessionGuard implements CanActivate { canActivate(ctx: ExecutionContext): boolean { - const req = ctx.switchToHttp().getRequest(); + const req = ctx.switchToHttp().getRequest<Request>(); if (!req.session?.userId) { throw new UnauthorizedException("Login required"); } return true; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/common/guards/session.guard.ts` around lines 10 - 16, The guard's req is inferred as any because getRequest() lacks a generic; update canActivate to call ctx.switchToHttp().getRequest<Request>() and import Request from 'express' so the augmented SessionData types (userId/type) are picked up and static analysis warnings go away; ensure references to req.session?.userId remain the same and no other logic changes in canActivate (keep the UnauthorizedException behavior).src/modules/companies/companies.service.ts (1)
78-88: 불필요한items별칭 제거 권장DART post-processing 제거 이후
items는 단순히raw를 그대로 가리키는 의미 없는 별칭이 되었습니다. 가독성을 위해 별칭을 없애고data에raw를 직접 넣는 것이 더 명확합니다.♻️ 제안 리팩터
const [raw, total] = await Promise.all([findQuery.exec(), countQuery]); - const items = raw; - return { page, limit, total, totalPages: Math.ceil(total / limit), - data: items, + data: raw, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/companies/companies.service.ts` around lines 78 - 88, Remove the redundant alias "items" that simply references "raw": in the async block where you await Promise.all([findQuery.exec(), countQuery]) and assign const [raw, total], stop creating const items = raw and instead return data: raw directly in the returned object (preserving page, limit, total, totalPages using Math.ceil(total / limit)); this simplifies the function around findQuery.exec(), countQuery and the returned data payload.src/modules/partners/partners.service.ts (1)
175-186:ATLAS_VECTOR_INDEX미설정 시 명시적 가드 권장이전의
"vector_index"폴백이 제거되어, 환경변수가 비어 있으면$vectorSearch.index가undefined로 전달되어 매번 aggregation 에러가 발생합니다(현재는 226–229의 try/catch가 흡수). 운영상 디버깅이 어려워지므로, 미설정이면 벡터 검색 단계를 건너뛰고 텍스트 검색 fallback으로 바로 진입하도록 가드하는 편이 더 안전합니다.♻️ 제안 리팩터
- if (!forceWebSearch && vector.length > 0) { + const atlasIndex = process.env.ATLAS_VECTOR_INDEX; + if (!forceWebSearch && vector.length > 0 && atlasIndex) { const pipeline: any[] = [ { $vectorSearch: { - index: process.env.ATLAS_VECTOR_INDEX, + index: atlasIndex, path: "embedding", queryVector: vector, numCandidates: 100, limit: Number(limit) * 2, }, }, ];부팅 시
ATLAS_VECTOR_INDEX미설정에 대해 한 번만logger.warn(...)을 남기는 것도 운영 가시성에 도움이 됩니다.🤖 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 175 - 186, Guard against a missing ATLAS_VECTOR_INDEX by checking process.env.ATLAS_VECTOR_INDEX before building the $vectorSearch pipeline: if ATLAS_VECTOR_INDEX is falsy, skip the vector-search branch (the block using forceWebSearch, vector and the pipeline with $vectorSearch and limit) and fall back immediately to the text-search path; also emit a single logger.warn at startup (or the first time this code runs) to surface the misconfiguration. Update the code around the vector pipeline construction and the try/catch that currently swallows the aggregation error so you never pass undefined into $vectorSearch.index and you get deterministic fallback behavior.src/modules/payments/xrpl.service.ts (1)
70-78:wallet.seed!non-null 단언 — 방어적 체크 권장xrpl.js 4.6.0 API 문서에 따르면
Wallet.seed는 선택적 속성(seed?: string)입니다.Wallet.generate()가 시드로부터 지갑을 생성하더라도, 라이브러리는wallet.seed속성이 항상 접근 가능함을 보장하지 않습니다. 따라서 non-null 단언(!)보다는 명시적 검증이 안전합니다. 시드 누락은 이후Wallet.fromSeed, 펀딩 등 전체 워크플로우를 무력화할 수 있으므로, 조기에 실패시키는 방어적 처리가 권장됩니다.♻️ 제안 리팩터
generateWallet(): XrplWallet { const wallet = Wallet.generate(); + if (!wallet.seed) { + throw new Error("Failed to generate XRPL wallet seed"); + } return { address: wallet.address, - seed: wallet.seed!, + seed: wallet.seed, publicKey: wallet.publicKey, privateKey: wallet.privateKey, }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/payments/xrpl.service.ts` around lines 70 - 78, The generateWallet function uses Wallet.generate() and asserts wallet.seed! which can be undefined; replace the non-null assertion with an explicit defensive check in generateWallet: after calling Wallet.generate() (referencing Wallet.generate and the wallet variable), verify wallet.seed is present and if not throw a clear Error (or return a Result) so callers know wallet creation failed early; ensure the XrplWallet return only occurs when seed is defined to avoid downstream failures in Wallet.fromSeed or funding flows.src/common/decorators/current-user.decorator.ts (1)
8-13:CurrentUser데코레이터가 현재 사용되지 않음 - 향후 사용 시 타입 안정성 확보 권장현재
CurrentUser데코레이터는 정의되어 있지만 코드베이스 어디서도 import되거나 사용되지 않습니다. 다만 향후 사용될 경우를 대비해,ctx.switchToHttp().getRequest()에서 반환되는req가any로 추론되어req.session접근 시 ESLintno-unsafe-member-access규칙이 경고를 발생시킬 수 있습니다.데코레이터를 사용할 계획이 있다면,
Request타입을 명시하여src/types/express-session.d.ts에서 augment된SessionData와의 타입 일관성을 맞추는 것이 좋습니다.♻️ 제안 리팩터
import { createParamDecorator, ExecutionContext } from "@nestjs/common"; +import type { Request } from "express"; export interface SessionUser { userId: string; type: "buyer" | "seller"; } export const CurrentUser = createParamDecorator( (_: unknown, ctx: ExecutionContext): SessionUser => { - const req = ctx.switchToHttp().getRequest(); + const req = ctx.switchToHttp().getRequest<Request>(); return { userId: req.session.userId, type: req.session.type }; }, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/common/decorators/current-user.decorator.ts` around lines 8 - 13, The CurrentUser decorator currently retrieves req via ctx.switchToHttp().getRequest() without typing, which can trigger no-unsafe-member-access; update the decorator to explicitly type the request (e.g., import and use Express's Request or your augmented interface for session such as the one declared in src/types/express-session.d.ts) when calling getRequest/when casting, so that SessionUser creation (userId/type) is type-safe; modify the createParamDecorator callback signature to use that Request type (refer to CurrentUser, createParamDecorator, ExecutionContext, SessionUser) and ensure the Request type import is added.src/config/env.config.ts (1)
72-81:redis.port타입 일관성과session.secret운영 가드.
redis.port는 환경 변수 사용 시string, fallback 시number로 타입이 갈라져 다운스트림(ioredis/connect-redis)에서 미묘한 회귀를 만들 수 있습니다.parseInt로 일관화 권장.session.secret도 운영 환경에서 dev fallback이 적용되면 세션 쿠키 위조가 가능합니다.encryptionKey와 동일하게 production에서는 강제 검증을 권장합니다.♻️ 제안
redis: { password: process.env.REDIS_PASSWORD || "", host: process.env.REDIS_HOST || "localhost", - port: process.env.REDIS_PORT || 6379, + port: parseInt(process.env.REDIS_PORT || "6379", 10), }, session: { - secret: process.env.SESSION_SECRET || "dev-secret-change-in-production", + secret: + process.env.SESSION_SECRET ?? + (process.env.NODE_ENV === "production" + ? (() => { + throw new Error("SESSION_SECRET env var is required in production"); + })() + : "dev-secret-change-in-production"), ttl: Number(process.env.SESSION_TTL_SECONDS || 7 * 24 * 60 * 60), // 7일 },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/env.config.ts` around lines 72 - 81, Normalize redis.port to always be a number by parsing the environment value (e.g., use parseInt on process.env.REDIS_PORT) so redis.port has a consistent numeric type for downstream clients; additionally, enforce a production check for session.secret (the session.secret fallback "dev-secret-change-in-production") so that in production the secret cannot be the dev fallback—throw or exit if SESSION_SECRET is missing or equals the default—mirroring the existing encryptionKey validation pattern; update src/config/env.config.ts symbols redis.port and session.secret accordingly and ensure session.ttl remains Number(...) as before.src/modules/auth/auth.service.ts (2)
31-35: 중복 이메일 검사에 TOCTOU(check-then-act) 레이스가 있습니다.
exists응답과save사이 시간 동안 같은 이메일로 다른 요청이 통과할 수 있습니다. 이 경우 이메일에 걸린 unique index가MongoServerError(code: 11000)로 전파되어 클라이언트는ConflictException대신 500을 받게 됩니다. unique index를 신뢰하고 duplicate-key 에러를ConflictException으로 매핑하는 패턴을 권장합니다.♻️ 제안 (registerSeller에 동일 적용; registerBuyer 동일 패턴)
- const exists = await this.userModel.exists({ - email: dto.representativeEmail, - }); - if (exists) throw new ConflictException("Already registered Email"); - const wallet = this.xrplService.generateWallet(); const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_ROUNDS); const newSeller = new this.companyModel({ ... }); - 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; + } this.activateAccountInBackground(wallet, saved._id, "seller"); return this.toSafeResponse(saved.toObject());선제적
exists검사를 빠른 사용자 피드백 용도로 유지하고 싶다면 그대로 두되,save단계에서도 11000 핸들링을 함께 두는 “양쪽 다” 패턴이 안전합니다.Also applies to: 65-69
🤖 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 31 - 35, The registerSeller (and similarly registerBuyer) flow has a TOCTOU race: keep the pre-check (userModel.exists) if you want quick feedback but also wrap the persistent write (e.g., userModel.create/save in registerSeller and registerBuyer) in a try/catch that detects Mongo duplicate-key errors (MongoServerError with code === 11000 or error.name === 'MongoServerError') and rethrow a ConflictException (e.g., new ConflictException("Already registered Email") or include error.keyValue.email) instead of allowing a 500; update both registerSeller and registerBuyer to map 11000 -> ConflictException while leaving the exists check optional for UX.
138-141:err.message직접 접근은 unknown 에러에서 안전하지 않습니다.TS의 catch 변수는
unknown이며, XRPL/Mongo 드라이버는 종종 plain object 또는 string을 throw 합니다.err.message가undefined로 직렬화되면 운영 트러블슈팅이 어려워집니다.♻️ 제안
- } catch (err) { + } catch (err: any) { this.logger.error( - `Failed to activate ${type} (${String(id)}): ${err.message}`, + `Failed to activate ${type} (${String(id)})`, + err instanceof Error ? err.stack : String(err), );
retryOne의logger.warn호출도 동일하게 정리하면 좋습니다.Also applies to: 192-196
🤖 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 138 - 141, The catch blocks in auth.service.ts access err.message directly which is unsafe because the catch variable is unknown and external libs may throw strings or plain objects; update the catch handlers (the one that calls this.logger.error in the activate flow and the logger.warn in retryOne) to normalize the error before logging — e.g., derive a safe message via a helper like getErrorMessage(err) or using (err instanceof Error ? err.message : typeof err === 'string' ? err : JSON.stringify(err)), and log both the normalized message and the original error object to preserve details for debugging while avoiding undefined.message runtime issues.src/modules/auth/auth.service.spec.ts (1)
89-351: 테스트 커버리지 양호 —login및activateAccountInBackground보강 권장.등록/재시도 경로는 잘 다루고 있지만,
AuthService.login(이메일 미존재/비밀번호 불일치/성공 시 password·wallet.seed 제거)과activateAccountInBackground의 성공/실패 분기(상태가ACTIVE/FAILED_ACTIVATION로 전이되는지)를 직접 검증하는 케이스가 비어 있습니다. 회원가입과 함께 가장 보안 민감한 두 경로이므로 단위 테스트를 보강해 주시면 좋습니다.원하시면 위 두 경로에 대한 테스트 스니펫을 별도 코멘트/이슈로 제안해 드릴 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/auth/auth.service.spec.ts` around lines 89 - 351, Add unit tests in auth.service.spec.ts to cover AuthService.login and AuthService.activateAccountInBackground: for login add tests that calling login with a non-existent email throws UnauthorizedException, calling login with wrong password throws UnauthorizedException, and a successful login returns user object with sensitive fields removed (ensure returned object has no password and no wallet.seed) by mocking userModel.findOne and bcrypt/verification as needed; for activateAccountInBackground add tests that when xrplService.fundAccount resolves the relevant model (companyModel or buyerModel) updateOne is called to set status to "ACTIVE", and when fundAccount rejects the updateOne is called to set status to "FAILED_ACTIVATION" (mock companyModel.find/ buyerModel.find to return test documents and assert xrplService.fundAccount calls and subsequent updateOne calls).src/modules/users/schemas/user.schema.ts (1)
18-19:unique: true가 이미 인덱스를 생성하므로index: true는 제거하세요.
unique: true는 자동으로 고유 인덱스를 생성하므로index: true를 함께 지정하면 중복 인덱스 정의로 인식됩니다. Mongoose 8.9+에서는 이에 대한 명시적 경고가 발생합니다.♻️ 제안
- `@Prop`({ required: true, unique: true, index: true }) + `@Prop`({ required: true, unique: true }) email: string;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/users/schemas/user.schema.ts` around lines 18 - 19, Remove the redundant index option from the email property decorator: in user.schema.ts update the `@Prop` on the email field (the line with `@Prop`({ required: true, unique: true, index: true }) for email: string) to omit index: true since unique: true already creates an index; ensure only unique: true (and required: true) remain so Mongoose won't warn about duplicate index definitions.src/modules/auth/auth.controller.ts (1)
42-46:@Req() req: any대신Request & { session: SessionData }형태의 타입을 사용하세요.
src/types/express-session.d.ts에 이미 보강된SessionData로 인해req.session.userId/req.session.type이 정적으로 검증됩니다. 현재 코드는any타입으로 인해 타입 안전성이 손실되고 있습니다.♻️ 제안
import { Controller, Post, Body, Req, HttpCode } from "@nestjs/common"; +import type { Request } from "express"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";- async login(`@Body`() dto: LoginDto, `@Req`() req: any) { + async login(`@Body`() dto: LoginDto, `@Req`() req: Request) { const user = await this.authService.login(dto); req.session.userId = user._id.toString(); req.session.type = user.type; return { message: "로그인 성공", user }; }- logout(`@Req`() req: any) { + logout(`@Req`() req: Request) { return new Promise<{ message: string }>((resolve) => { req.session.destroy(() => resolve({ message: "로그아웃 성공" })); }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/modules/auth/auth.controller.ts` around lines 42 - 46, Replace the loose any type on the controller request in the login handler with the proper Express/session types: update the parameter of async login(...) (in auth.controller.ts) from `@Req`() req: any to `@Req`() req: Request & { session: SessionData }, import Request from 'express' and SessionData from your augmented session types file so that req.session.userId and req.session.type are statically typed and checked; keep the rest of the login logic unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8a2d45d2-1f1b-4c72-816e-3912c7f945fb
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (75)
.coderabbit.yaml.env.example.env.test.gitignoredocker-compose.ymlpackage.jsonsrc/app.module.tssrc/common/decorators/current-user.decorator.tssrc/common/guards/session.guard.tssrc/common/schemas/wallet-info.schema.tssrc/config/env.config.tssrc/main.tssrc/modules/admin/admin.controller.spec.tssrc/modules/admin/admin.controller.tssrc/modules/admin/admin.module.tssrc/modules/admin/admin.service.spec.tssrc/modules/admin/admin.service.tssrc/modules/admin/dto/audit-logs-query.dto.tssrc/modules/admin/dto/list-payments-query.dto.tssrc/modules/admin/dto/match-logs-query.dto.tssrc/modules/admin/dto/payment-stats-query.dto.tssrc/modules/admin/guards/admin-token.guard.spec.tssrc/modules/admin/guards/admin-token.guard.tssrc/modules/admin/schemas/audit-log.schema.tssrc/modules/auth/auth.controller.spec.tssrc/modules/auth/auth.controller.tssrc/modules/auth/auth.module.tssrc/modules/auth/auth.service.spec.tssrc/modules/auth/auth.service.tssrc/modules/auth/dto/login.dto.tssrc/modules/auth/dto/register.dto.tssrc/modules/buyers/buyers.controller.spec.tssrc/modules/buyers/buyers.controller.tssrc/modules/buyers/buyers.module.tssrc/modules/buyers/buyers.service.spec.tssrc/modules/buyers/buyers.service.tssrc/modules/buyers/dto/create-buyer.dto.tssrc/modules/buyers/dto/update-buyer.dto.tssrc/modules/buyers/schemas/buyer.schema.tssrc/modules/companies/companies.module.tssrc/modules/companies/companies.service.spec.tssrc/modules/companies/companies.service.tssrc/modules/companies/company-images.controller.spec.tssrc/modules/companies/company-images.controller.tssrc/modules/companies/company-images.service.spec.tssrc/modules/companies/company-images.service.tssrc/modules/companies/dto/create-company.dto.tssrc/modules/companies/dto/upload-image.dto.tssrc/modules/companies/schemas/company.schema.tssrc/modules/consultants/consultants.controller.spec.tssrc/modules/consultants/consultants.controller.tssrc/modules/consultants/consultants.module.tssrc/modules/consultants/consultants.service.spec.tssrc/modules/consultants/consultants.service.tssrc/modules/consultants/dto/create-consultant-request.dto.tssrc/modules/consultants/schemas/consultant-request.schema.tssrc/modules/consultations/consultations.service.tssrc/modules/insights/insights.controller.spec.tssrc/modules/insights/insights.controller.tssrc/modules/insights/insights.module.tssrc/modules/insights/insights.service.spec.tssrc/modules/insights/insights.service.tssrc/modules/matches/matches.service.spec.tssrc/modules/matches/matches.service.tssrc/modules/partners/partners.module.tssrc/modules/partners/partners.service.spec.tssrc/modules/partners/partners.service.tssrc/modules/payments/xrpl.service.tssrc/modules/users/schemas/buyer.schema.tssrc/modules/users/schemas/company.schema.tssrc/modules/users/schemas/user.schema.tssrc/modules/users/users.module.tssrc/types/express-session.d.tstest/jest-e2e.jsontest/xrpl.e2e-spec.ts
💤 Files with no reviewable changes (34)
- src/modules/admin/admin.controller.ts
- src/modules/buyers/dto/create-buyer.dto.ts
- src/modules/companies/dto/upload-image.dto.ts
- src/modules/buyers/dto/update-buyer.dto.ts
- src/modules/consultants/consultants.controller.spec.ts
- src/modules/admin/guards/admin-token.guard.ts
- src/modules/consultants/consultants.controller.ts
- .env.example
- src/modules/admin/admin.module.ts
- src/modules/admin/dto/match-logs-query.dto.ts
- src/modules/insights/insights.module.ts
- src/modules/insights/insights.controller.ts
- src/modules/consultants/consultants.service.ts
- src/modules/admin/dto/payment-stats-query.dto.ts
- src/modules/buyers/schemas/buyer.schema.ts
- src/modules/companies/company-images.service.spec.ts
- src/modules/consultants/consultants.module.ts
- src/modules/admin/schemas/audit-log.schema.ts
- src/modules/companies/company-images.controller.ts
- src/modules/consultants/schemas/consultant-request.schema.ts
- src/modules/consultants/dto/create-consultant-request.dto.ts
- src/modules/admin/admin.service.ts
- src/modules/insights/insights.service.ts
- src/modules/admin/guards/admin-token.guard.spec.ts
- src/modules/companies/company-images.controller.spec.ts
- src/modules/admin/dto/list-payments-query.dto.ts
- src/modules/companies/company-images.service.ts
- src/modules/insights/insights.controller.spec.ts
- src/modules/admin/dto/audit-logs-query.dto.ts
- src/modules/admin/admin.controller.spec.ts
- src/modules/insights/insights.service.spec.ts
- src/modules/admin/admin.service.spec.ts
- src/modules/companies/schemas/company.schema.ts
- src/modules/consultants/consultants.service.spec.ts
#4
개요
buyer/seller 회원가입 및 Redis 세션 기반 로그인/로그아웃을 구현했습니다.
구현 과정에서 드러난 도메인 구조 문제와 미사용 모듈도 함께 정리했습니다.
변경 사항
feat
refactor
users/schemas/로 이동 —UsersModule이 하위 도메인 스키마를 역방향 참조하던 구조 수정chore
범위 확대 사유
회원가입 구현 중
UsersModule → buyers/companies역방향 의존, buyer 모듈의 중복 CRUD가 확인되어 함께 정리했습니다.테스트
npm test98개 전체 통과Summary by CodeRabbit
릴리스 노트
새로운 기능
기반시설