Conversation
- SellerService(330줄), UserService(94줄) facade 삭제 - Seller Resolver 10개: SellerService → 각 도메인 서비스 직접 의존 - User Resolver 7개: UserService → 각 도메인 서비스 직접 의존 - Facade 위임 테스트(seller.service.spec, user.service.spec) 삭제 - Resolver 테스트를 split service mock으로 수정 - Module에서 facade provider/export 제거
SellerBaseService에 집중되어 있던 output mapper/enum 변환 메서드를 각 도메인 서비스의 private 메서드로 이동하여 응집도를 높임. - Store mappers → SellerStoreService - Product mappers → SellerProductService - Order mappers → SellerOrderService - Conversation mappers → SellerConversationService - Content mappers + enum converters → SellerContentService - SellerBaseService: 829줄 → 188줄 (공통 유틸만 유지)
…nership 중복 제거 - validateBannerOwnership을 SellerBaseService에서 유일 소비자인 SellerContentService로 이동 (private) - SellerRepository의 findProductOwnership 삭제, ProductRepository 것으로 통합 - SellerContentService에 ProductRepository 주입 추가
…r 삭제 Phase 4+6: - parseId를 src/common/utils/id-parser.ts로 통합 (3중 중복 제거) - parseAccountId를 src/global/auth/parse-account-id.ts로 통합 - seller-resolver.utils.ts, user-resolver.utils.ts 삭제 - SellerBaseService에서 protected parseId 제거, 공통 유틸 사용 - 17개 resolver + 5개 서비스의 import 경로 변경 Phase 5: - UserCartService, UserOrderService, ProductDomainService, ConversationDomainService (빈 클래스) 삭제 - user.module, product.module, conversation.module에서 제거 - product/index.ts, conversation/index.ts에서 export 제거
📝 WalkthroughWalkthrough대규모 서비스 계층 재구조화를 통해 단일 SellerService 및 UserService를 도메인별 전문화된 서비스로 분해합니다. 기존 SellerRepository에서 상품, 주문, 대화 관리 기능을 새로운 전용 저장소로 추출하고, GraphQL 스키마와 리졸버를 주제별로 분리합니다. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.github/scripts/pr-ai-description.mjs (1)
943-985:⚠️ Potential issue | 🟠 Major커밋 메시지를 원문 그대로 프롬프트에 넣는 것은 보안·신뢰성 리스크입니다.
Line 980-985에서
commitMessages가 그대로 프롬프트로 전달됩니다. 현재 diff는 마스킹하지만 커밋 메시지는 마스킹/길이 제한이 없어, 민감정보 유출 및 과도한 토큰 사용(실패/비용 증가) 가능성이 있습니다.🔧 제안 수정
+const MAX_PROMPT_COMMITS = 50; +const MAX_COMMIT_LINE_CHARS = 200; + async function run() { @@ - const prompt = buildOpenAiPrompt({ + const sanitizedCommitMessages = commitMessages + .map((message) => truncateText(maskSensitiveContent(message), MAX_COMMIT_LINE_CHARS)) + .filter((message) => message.length > 0) + .slice(0, MAX_PROMPT_COMMITS); + + const prompt = buildOpenAiPrompt({ pr: pullRequest, repositoryLabels, diffText: maskedDiff, - commitMessages, + commitMessages: sanitizedCommitMessages, });As per coding guidelines, ".github/scripts/**: 환경 변수/시크릿 사용이 안전한지, 민감정보가 로그에 노출되지 않는지 확인하세요."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/scripts/pr-ai-description.mjs around lines 943 - 985, commitMessages are passed raw into buildOpenAiPrompt which risks leaking sensitive data and excessive token usage; before calling buildOpenAiPrompt, sanitize and size-limit commit messages by mapping over the array and applying maskSensitiveContent to each message (use the same masking function used for diffs) and then enforce a byte/character cap per-message and/or total (truncate long messages and/or drop oldest messages when over a maxCommitBytes), updating places where commitMessages is assigned (results of fetchPrCommitMessages and collectCommitMessagesFromGit) so buildOpenAiPrompt receives only masked, truncated commitMessages and consider logging a redacted summary (counts/bytes) rather than full messages.src/features/seller/services/seller-order.service.spec.ts (1)
64-100:⚠️ Potential issue | 🟠 Major
sellerUpdateOrderStatus정상 경로 검증이 빠져 있습니다.이 spec에서 새로 다루기 시작한
sellerUpdateOrderStatus는 실패 케이스만 확인합니다. 이 상태면 유효한 상태 전이에서OrderRepository.updateOrderStatusBySeller(...)가 실제로 호출되는지와 핵심 인자가 올바른지에 대한 회귀를 막지 못합니다. 최소한 성공 케이스 하나는 같이 넣어두는 편이 안전합니다.As per coding guidelines, "기능 변경에는 테스트가 함께 추가되었는지 확인하세요." 및 "
src/**/*.spec.ts: 테스트는 시간/uuid/네트워크/DB 의존성을 mock 또는 stub으로 통제하는지, 정상 흐름뿐 아니라 주요 예외/분기 케이스가 포함되는지 확인하세요."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/seller/services/seller-order.service.spec.ts` around lines 64 - 100, Add a success-path spec for sellerUpdateOrderStatus: mock repo.findSellerAccountContext and orderRepo.findOrderDetailByStore to return a valid seller and an order in a state that allows transition (e.g., SUBMITTED → MADE), call service.sellerUpdateOrderStatus(BigInt(1), { orderId: '10', toStatus: 'MADE', note: null }), and assert that OrderRepository.updateOrderStatusBySeller (or orderRepo.updateOrderStatusBySeller in your test doubles) was called once with the correct order id, target status and seller context; ensure any time/uuid/network/DB dependencies are stubbed as in other tests and verify returned value or no-exception behavior for the successful path.
🟡 Minor comments (6)
src/features/product/product.types.graphql-1-1 (1)
1-1:⚠️ Potential issue | 🟡 Minor설명 주석의 도메인 용어가 enum 의미와 불일치합니다.
Line 1은
주문 상태로 설명하지만 실제 타입은ProductVisibility(상품 공개 상태)입니다. 스키마 문서/협업 시 혼동을 줄이려면 설명을 상품 가시성 기준으로 수정해 주세요.수정 예시
-"""공용 주문 상태 타입(향후 user/seller 공용으로 사용)""" +"""공용 상품 공개 상태 타입(향후 user/seller 공용으로 사용)"""As per coding guidelines, "SDL 변경이 Resolver 이름, 입력/출력 타입과 일관되는지 확인하세요."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/product/product.types.graphql` at line 1, The SDL file's top description incorrectly says "공용 주문 상태" while the enum represents ProductVisibility; update the description string above the ProductVisibility enum to describe product visibility/publication status (e.g., 공개/비공개/임시표시 등) and ensure the new comment wording aligns with Resolver names and any related input/output types that reference ProductVisibility so documentation and SDL remain consistent..github/scripts/pr-ai-description.mjs-960-964 (1)
960-964:⚠️ Potential issue | 🟡 MinorPrettier 포맷 오류를 정리해 주세요.
Line 960-964는 현재 정적 분석에서
prettier/prettier에러로 보고됩니다. CI 안정성을 위해 해당 블록 줄바꿈/들여쓰기를 포맷터 기준으로 맞추는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/scripts/pr-ai-description.mjs around lines 960 - 964, The block handling the gitError fallback is misformatted for Prettier; reformat the try/catch snippet around logInfo and logWarn to match project Prettier rules (proper line breaks and indentation) so static analysis passes: ensure the logInfo call ends with a semicolon on its own line and the catch block formats the logWarn invocation with its options object on a correctly indented new line, referencing the existing symbols commitMessages, logInfo, logWarn, and gitError to locate and reflow the statements without changing semantics.src/features/user/services/user-base.service.ts-82-84 (1)
82-84:⚠️ Potential issue | 🟡 Minor전화번호 검증이 하이픈만 있는 문자열을 통과시킵니다.
현재 정규식은
"-------"같은 값을 허용합니다. 최소 1개 이상의 숫자를 강제하는 조건이 필요합니다.🔧 수정 예시
- if (!/^[0-9-]+$/.test(trimmed)) { + if (!/^(?=.*\d)[0-9-]+$/.test(trimmed)) { throw new BadRequestException('Invalid phone number format.'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-base.service.ts` around lines 82 - 84, The current phone validation in user-base.service.ts allows strings of only hyphens (e.g., "-------"); update the regex used in the validation (the test applied to trimmed in the same block where BadRequestException is thrown) to require at least one digit while still allowing hyphens, for example replace /^[0-9-]+$/ with /^(?=.*\d)[0-9-]+$/ (or an equivalent pattern) so inputs must contain at least one digit but only digits and hyphens are permitted.src/features/user/services/user-base.service.ts-90-101 (1)
90-101:⚠️ Potential issue | 🟡 MinorGraphQL DateTime 스칼라가 문자열을 먼저 Date 객체로 변환하므로 현재 사용 패턴에서 위험은 낮습니다.
GraphQL의
DateTimeScalar.parseValue()가 클라이언트 입력 문자열을 Date 객체로 변환하므로, 현재 GraphQL 엔드포인트를 통한 호출에서는raw instanceof Date분기가 실행되어 직접적인 타임존 문제가 발생하지 않습니다. 다만 함수 시그니처가Date | string을 모두 허용하고 있어, 향후 다른 경로에서 date-only 문자열이 직접 전달될 경우 web 검증 결과대로 로컬 날짜가 한 날 당겨질 수 있습니다. 방어적 코딩 관점에서 제시된 수정(정규식 기반 YYYY-MM-DD 파싱)이 권장됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-base.service.ts` around lines 90 - 101, The normalization code should defensively handle date-only strings to avoid timezone shifts: if the input raw matches /^\d{4}-\d{2}-\d{2}$/ (a YYYY-MM-DD date-only string), parse the parts and construct the date as a local-midnight Date via new Date(year, month-1, day) instead of new Date(raw); otherwise keep the existing behavior (const date = raw instanceof Date ? raw : new Date(raw)). Then continue the same NaN check, zeroing hours with normalized.setHours(0,0,0,0), and the future-date check before returning normalized (referencing the variables raw, date, normalized in this function).src/features/user/services/user-engagement.service.ts-11-33 (1)
11-33:⚠️ Potential issue | 🟡 Minor테스트 파일을 추가하세요.
서비스 구조는 잘 설계되어 있습니다:
- Repository 패턴을 통한 DB 접근 ✓
- 명확한 입출력 타입 (bigint, Promise) ✓
- 비즈니스 로직이 Service에 적절히 위치 ✓
하지만
user-engagement.service.spec.ts파일이 없습니다. 기능 변경에는 테스트가 함께 추가되어야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-engagement.service.ts` around lines 11 - 33, 추가할 테스트 파일 user-engagement.service.spec.ts를 작성해 주세요: UserEngagementService의 likeReview 메서드에 대한 유닛 테스트를 추가하고, UserRepository를 목(mock)으로 주입하여 동작을 검증합니다; 구체적으로 requireActiveUser가 활성 유저를 확인하도록 목 처리하고 repo.likeReview가 'not-found'를 반환할 때 NotFoundException을, 'self-like'를 반환할 때 BadRequestException을 던지는 케이스를 각각 테스트하며 정상적으로 true를 반환하는 성공 케이스도 포함합니다; 테스트에서 UserEngagementService 생성자는 목 리포지토리(UserRepository)로 생성하고 likeReview(accountId, reviewId)를 호출해 예외 메시지/타입과 true 반환을 검증하세요.src/features/order/policies/order-status-transition.policy.ts-15-43 (1)
15-43:⚠️ Potential issue | 🟡 Minor
SUBMITTED상태로의 전환 검증 누락
assertSellerTransition에서to === OrderStatus.SUBMITTED인 경우에 대한 검증이 없습니다. 현재 로직에서는 판매자가 주문을SUBMITTED상태로 전환하려고 할 때 어떤 예외도 발생하지 않습니다.SUBMITTED는 일반적으로 주문 생성 시에만 설정되어야 하므로, 판매자 전환 시 명시적으로 차단하는 것이 안전합니다.🛡️ 제안된 수정
assertSellerTransition(from: OrderStatus, to: OrderStatus): void { if (from === to) { throw new BadRequestException('Order status is already set to target.'); } + if (to === OrderStatus.SUBMITTED) { + throw new BadRequestException('Cannot transition to SUBMITTED status.'); + } + if (to === OrderStatus.CONFIRMED && from !== OrderStatus.SUBMITTED) { throw new BadRequestException('Invalid order status transition.'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/order/policies/order-status-transition.policy.ts` around lines 15 - 43, The seller transition logic in assertSellerTransition is missing a guard against transitioning to OrderStatus.SUBMITTED; add an explicit check in assertSellerTransition that if to === OrderStatus.SUBMITTED you throw a BadRequestException (e.g., "Order cannot be set to SUBMITTED by seller") to block sellers from reverting/setting orders to SUBMITTED, keeping existing checks for same-state and other transitions intact.
🧹 Nitpick comments (13)
src/global/auth/parse-account-id.ts (1)
1-10: ID 파싱 로직이parseId와 중복됩니다.동일 패턴이 두 군데라서 추후 정책 변경 시 드리프트가 생기기 쉽습니다. 공통 유틸 재사용으로 합치는 편이 좋습니다.
♻️ 중복 제거 예시
-import { BadRequestException } from '@nestjs/common'; - import type { JwtUser } from './types/jwt-payload.type'; +import { parseId } from '../../common/utils/id-parser'; export function parseAccountId(user: JwtUser): bigint { - try { - return BigInt(user.accountId); - } catch { - throw new BadRequestException('Invalid account id.'); - } + return parseId(user.accountId); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/global/auth/parse-account-id.ts` around lines 1 - 10, The parseAccountId function duplicates parseId logic; replace the duplicate parsing code by delegating to the shared utility (e.g., parseId) to avoid drift: remove the try/catch BigInt logic in parseAccountId and call the common parseId utility (ensuring it returns bigint and throws BadRequestException on invalid input), preserve the exported function signature parseAccountId(user: JwtUser): bigint, and update imports to import parseId from the common util so all callers use the single implementation.src/features/user/services/user-profile.service.ts (1)
120-133: 삭제된 닉네임 패턴 검토가 필요합니다.
deleted_${accountId}패턴은accountId를 노출할 수 있습니다. 닉네임이 다른 사용자에게 표시되는 경우 민감 정보 노출이 될 수 있습니다.더 안전한 대안을 고려해 주세요:
🛡️ 개선 제안
async deleteMyAccount(accountId: bigint): Promise<boolean> { await this.requireActiveUser(accountId); const now = new Date(); - const deletedNickname = `deleted_${accountId.toString()}`; + const deletedNickname = `deleted_${now.getTime()}_${Math.random().toString(36).slice(2, 8)}`; await this.repo.softDeleteAccount({ accountId, deletedNickname, now, }); return true; }As per coding guidelines: "인증/로깅/에러 처리에서 민감정보가 노출되지 않는지 확인하세요."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-profile.service.ts` around lines 120 - 133, The current deleteMyAccount method builds a deletedNickname using `deleted_${accountId}` which exposes the raw accountId; change the `deletedNickname` generation in `deleteMyAccount` to avoid including the accountId directly (e.g., replace with a non-identifying token such as a secure random string, UUID, or a one-way hash/salted digest of accountId+timestamp) and update any related storage/validation expectations in `repo.softDeleteAccount` to accept the new format; ensure the change is applied in the `deleteMyAccount` function and any consumers that assume the old pattern so no sensitive accountId is leaked in nicknames, logs, or responses.src/features/user/services/user-notification.service.ts (1)
57-61: 시간 의존성을 주입 가능하게 만드는 것을 고려해 주세요.
new Date()가 서비스 내에서 직접 호출되고 있습니다 (lines 52, 60, 72). 테스트에서 시간을 제어하기 어려워질 수 있습니다.현재 구현도 동작하지만, 향후 시간 기반 테스트가 필요할 경우 Clock 추상화나
jest.useFakeTimers()를 활용할 수 있습니다.As per coding guidelines: "테스트는 시간/uuid/네트워크/DB 의존성을 mock 또는 stub으로 통제하는지 확인하세요."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-notification.service.ts` around lines 57 - 61, The service currently calls new Date() directly (e.g., when calling this.repo.markNotificationRead), which makes time-based tests brittle; refactor to inject a clock/time provider into the UserNotificationService (or accept an optional now: Date parameter on public methods like markNotificationRead/markAllRead) and replace direct new Date() calls with the injected provider (e.g., this.clock.now() or the passed-in now). Update the constructor to accept the clock dependency (defaulting to a real clock implementation) and adjust calls to this.repo.markNotificationRead, markNotificationUnread, etc., to use the injected time so tests can supply a fake clock or fixed Date.src/features/user/services/user-notification.service.spec.ts (1)
48-70: 테스트 커버리지가 부족합니다.현재 두 가지 케이스만 테스트하고 있습니다:
unreadOnly옵션 전달markNotificationRead예외 케이스다음 케이스들이 누락되어 있습니다:
viewerCounts정상 흐름myNotifications매핑 및hasMore계산 로직markNotificationRead성공 케이스markAllNotificationsRead정상 흐름추가 테스트 케이스를 생성해 드릴까요?
As per coding guidelines: "정상 흐름뿐 아니라 주요 예외/분기 케이스가 포함되는지 확인하세요."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-notification.service.spec.ts` around lines 48 - 70, Add unit tests to cover the missing normal and branch flows: create tests for viewerCounts success by mocking repo.getViewerCounts (or the service method that calls it) to return expected counts and assert the service.viewerCounts (or equivalent) returns them; add a myNotifications test that validates mapping of notification items and hasMore calculation by mocking repo.listNotifications to return items and totalCount > offset+limit and asserting transformed output and hasMore true/false; add a markNotificationRead success test by mocking repo.markNotificationRead to return true and asserting no exception and that repo methods (e.g., findAccountWithProfile, markNotificationRead) were called with correct IDs; add a test for markAllNotificationsRead normal flow by mocking the repo method (e.g., markAllNotificationsRead) to succeed and asserting it was invoked and returns expected result; in each test reference the existing service methods myNotifications, markNotificationRead, markAllNotificationsRead and repo mocks (repo.listNotifications, repo.markNotificationRead, repo.markAllNotificationsRead, repo.findAccountWithProfile) to locate where to add the assertions and mocks.src/features/seller/services/seller-product.service.ts (2)
1073-1160:toProductOutput매퍼의 인라인 타입 정의가 매우 길어 재사용성이 낮습니다.별도의 타입 파일로 추출하면 다른 서비스에서도 재사용할 수 있고, 코드 가독성이 향상됩니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/seller/services/seller-product.service.ts` around lines 1073 - 1160, The inline complex parameter type on toProductOutput should be extracted into a reusable named type/interface (e.g., ProductRow or SellerProductRow) in a shared types file so other services can reuse it and the mapper stays readable; update the toProductOutput signature to accept that new type (keep SellerProductOutput as the return), move nested types (images, product_categories, product_tags, option_groups, custom_template, text_tokens) into exported interfaces or type aliases alongside ProductRow, and update any helper methods referenced (toProductImageOutput, toOptionGroupOutput, toCustomTemplateOutput) to import and use the new types as needed.
96-160:sellerCreateProduct에서 상품 생성 후 조회가 중복됩니다.
createProduct후findProductByIdIncludingInactive를 다시 호출합니다.createProduct가 이미지를 포함하지 않으므로 필요하지만,createProduct반환값에 include를 추가하거나 트랜잭션으로 묶는 것이 효율적입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/seller/services/seller-product.service.ts` around lines 96 - 160, sellerCreateProduct currently calls productRepository.createProduct and then separately productRepository.addProductImage and finally re-queries with productRepository.findProductByIdIncludingInactive, causing a redundant lookup; fix by returning the full product (including images) from createProduct or by wrapping createProduct + addProductImage + repo.createAuditLog in a single transaction so you can use the returned created entity (with images) instead of calling findProductByIdIncludingInactive. Update productRepository.createProduct to accept/include images (or an include option) and return the complete product, or implement a transactional method (e.g., createProductWithImages) that performs createProduct, addProductImage, and audit log and returns the final product; then replace the extra findProductByIdIncludingInactive call in sellerCreateProduct to use that returned product.src/features/user/services/user-profile.service.spec.ts (1)
75-82: 닉네임 중복 시 예외 테스트는 좋지만, 성공 케이스 테스트가 누락되었습니다.
updateMyProfile의 정상 흐름(닉네임 변경 성공 시updateProfile호출 및 반환값 검증) 테스트가 없습니다. 또한completeOnboarding,updateMyProfileImage메서드에 대한 테스트도 추가하면 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-profile.service.spec.ts` around lines 75 - 82, Add missing positive unit tests for updateMyProfile: mock repo.findAccountWithProfile to return baseAccount and repo.isNicknameTaken to return false, call service.updateMyProfile(BigInt(1), { nickname: 'tester2' }) and assert updateProfile was called with the expected profile data and the method returns the updated profile; likewise add simple success tests for completeOnboarding and updateMyProfileImage that mock necessary repo/service dependencies, invoke service.completeOnboarding and service.updateMyProfileImage, and assert the appropriate repository methods (e.g., markOnboarded or save/update) were called and correct values are returned. Ensure tests reference and assert interactions with updateProfile, repo.findAccountWithProfile, repo.isNicknameTaken, completeOnboarding, and updateMyProfileImage.src/features/user/resolvers/user-profile-mutation.resolver.ts (1)
20-21: 이 Resolver에 대한 단위 테스트가 누락되었습니다.
user.resolver.spec.ts에서UserProfileQueryResolver와UserNotificationMutationResolver만 테스트하고 있습니다.UserProfileMutationResolver에 대한 테스트도 추가하면 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/resolvers/user-profile-mutation.resolver.ts` around lines 20 - 21, UserProfileMutationResolver lacks unit tests; add tests in user.resolver.spec.ts (or new user-profile-mutation.resolver.spec.ts) that instantiate UserProfileMutationResolver with a mocked UserProfileService, verify each public mutation method on UserProfileMutationResolver calls the corresponding UserProfileService method with correct arguments and handles success/error paths (e.g., resolve values and thrown errors), and include setup/teardown and necessary mocks/spies to assert interactions with UserProfileService.src/features/seller/services/seller-base.service.ts (1)
116-125:assertPositiveRange함수명이 실제 동작과 일치하지 않습니다.함수명은 "positive range"를 검증한다고 하지만,
min이 0 또는 음수일 수 있어 실제로는 일반적인 정수 범위 검증입니다. 예를 들어sellerCreateProduct에서salePrice는min=0으로 호출됩니다.♻️ 함수명 개선 제안
- protected assertPositiveRange( + protected assertIntegerRange( value: number, min: number, max: number, field: string, ): void {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/seller/services/seller-base.service.ts` around lines 116 - 125, The method assertPositiveRange does not match its behavior (it validates an integer within a min/max, not strictly positive); rename it to assertIntegerInRange (or similar) and update all callers (e.g., sellerCreateProduct) to the new name, and change the error message to be explicit like "`<field> must be an integer between <min> and <max>.`" Keep the existing Number.isInteger(value) and range checks intact; run/adjust any imports, tests, and TS references so compilation passes.src/features/seller/seller-common.graphql (1)
2-5:limit최대치 정책도 함께 강제하는 것을 권장합니다.기본값 20 설정은 좋습니다. 다만 상한이 없으면 과도한 조회 요청이 들어올 수 있어 resolver/service에서
max limit클램프(예: 100)를 같이 두는 편이 안전합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/seller/seller-common.graphql` around lines 2 - 5, The SellerCursorInput.limit has a default but no upper bound; update the resolver/service that consumes SellerCursorInput (e.g., the listSellers / getSellers resolver or the service method handling cursor pagination) to clamp the incoming limit to a safe max (suggest 100). Implement: read input.limit, set limit = Math.min(input.limit || 20, MAX_LIMIT) (or equivalent in your language), and use that clamped value for DB queries and pagination logic to prevent oversized requests. Ensure MAX_LIMIT is a named constant and add a brief comment referencing SellerCursorInput.limit.src/features/seller/services/seller-conversation.service.ts (1)
118-137: 메시지 생성과 감사 로그가 별도 트랜잭션으로 실행됩니다.
createSellerConversationMessage가 성공한 후createAuditLog가 실패하면 메시지는 생성되었지만 감사 로그는 누락될 수 있습니다. 의도된 설계라면 괜찮지만, 감사 로그가 필수라면 단일 트랜잭션으로 묶는 것을 고려해보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/seller/services/seller-conversation.service.ts` around lines 118 - 137, Currently createSellerConversationMessage (via conversationRepository) and createAuditLog (via repo.createAuditLog) run in separate operations so a failure in createAuditLog can leave a created message without an audit entry; wrap both operations in a single database transaction (use your repository/ORM transaction helper—e.g., conversationRepository.transaction or a shared transaction manager) and perform createSellerConversationMessage and repo.createAuditLog inside that transaction so that if createAuditLog fails the message creation is rolled back; ensure you use the same transactional entity manager for both calls and propagate/throw errors to trigger rollback.src/features/conversation/repositories/conversation.repository.ts (1)
60-83: 트랜잭션 내 대화 업데이트가id만으로 수행됩니다.
storeConversation.update가id만으로 조건을 걸고 있습니다. 메시지 생성 시 이미conversation_id를 사용하므로 논리적으로는 안전하지만, 방어적으로store_id조건을 추가하는 것을 고려해볼 수 있습니다.♻️ 선택적 개선안
await tx.storeConversation.update({ - where: { id: args.conversationId }, + where: { + id: args.conversationId, + // 방어적으로 store scope 추가 가능 + }, data: { last_message_at: args.now, updated_at: args.now, }, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/conversation/repositories/conversation.repository.ts` around lines 60 - 83, The update to storeConversation uses only the id in tx.storeConversation.update({ where: { id: args.conversationId } }), which is fragile; change the where clause to include the store ownership check by adding the store identifier equals args.sellerAccountId (e.g., where: { id: args.conversationId, store_id: args.sellerAccountId } or use seller_account_id if that is the column name) so the update only affects the conversation owned by that store; update the tx.storeConversation.update call accordingly within the same transaction alongside tx.storeConversationMessage.create.src/features/seller/services/seller-store.service.ts (1)
109-155: 업데이트용 DB 필드 매핑은 Repository로 내리는 편이 좋겠습니다.
store_name,address_full같은 저장소 필드명을 서비스가 직접 조립하고 있어서, 저장소 스키마 변경이 서비스 레이어까지 전파됩니다. 이 메서드는 도메인 입력 정제까지만 맡기고 DB 필드 매핑은SellerRepository에서 처리하면 분리된 feature 경계가 더 선명해집니다.As per coding guidelines, "Service/Resolver가 PrismaService 또는 PrismaClient에 직접 접근하면 안 됩니다. Repository를 통해서만 DB 접근해야 합니다."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/seller/services/seller-store.service.ts` around lines 109 - 155, The service is assembling Prisma field names (the data const) directly; move DB field mapping into SellerRepository so the service only performs domain cleaning. In seller-store.service.ts replace building the Prisma.StoreUpdateInput (the data const) with creating a plain domain DTO of cleaned values using cleanRequiredText/cleanNullableText/toDecimal, then call a new repository method (e.g., SellerRepository.updateStore or SellerRepository.mapAndUpdateStore) that converts those domain fields to Prisma keys like store_name, address_full and performs the update; ensure all references to map_provider, website_url, business_hours_text, latitude/longitude are mapped inside the repository.
🤖 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/features/order/repositories/order.repository.ts`:
- Around line 163-180: The code skips creating a notification for orders
transitioning to CANCELED because orderStatusToNotificationEvent(args.toStatus)
returns null; update that mapping so CANCELED returns a proper NotificationEvent
(e.g., ORDER_CANCELED or a generic STATUS_CHANGED event) OR change the
conditional around tx.notification.create to treat a null as a fallback event
and still call tx.notification.create with NotificationType.ORDER_STATUS, title
via notificationTitleByOrderStatus, body via
notificationBodyByOrderStatus(updatedOrder.order_number, args.toStatus), and
order_id: order.id; modify the orderStatusToNotificationEvent function (and any
callers) to include CANCELED mapping, or add a fallbackEvent before the if-check
to ensure tx.notification.create always runs for status changes like CANCELED.
- Around line 116-151: The update must be made atomic against the previously
read status to prevent race conditions: change the update call on
tx.order.update (after retrieving order and setting fromStatus) to include the
current status in the where clause (i.e., where: { id: order.id, status:
fromStatus }) or perform an updateMany with that where and then check the
affected count; if no rows were updated, abort/return null so you don't create
duplicate history/notifications. Keep the same timestamp/field changes
(confirmed_at, made_at, picked_up_at, canceled_at) and ensure downstream logic
(history/notification creation) only runs when the conditional update actually
modified a row (use updated row existence/affected count to gate those actions).
In `@src/features/product/repositories/product.repository.ts`:
- Around line 191-195: countProductImages currently counts all productImage rows
including soft-deleted ones set by softDeleteProductImage (deleted_at), causing
incorrect limits; update the count query in countProductImages to filter out
soft-deleted records by adding a where condition that deleted_at is null (i.e.,
include deleted_at: null alongside product_id: productId) so only active images
are counted.
- Around line 630-637: The listCustomTextTokens method currently returns all
tokens including soft-deleted ones; update the Prisma query in
listCustomTextTokens to filter out soft-deleted records by adding a condition
like where: { template_id: templateId, deleted_at: null } (retain the existing
orderBy { sort_order: 'asc' }) so productCustomTextToken.findMany only returns
non-deleted tokens.
- Around line 472-477: The listOptionItemsByGroup method is missing the
repository's soft-delete filter; update listOptionItemsByGroup to include the
same soft-delete condition used elsewhere (e.g., add the deleted_at: null or
equivalent is_deleted flag) to the where clause of the
productOptionItem.findMany call so only non-deleted option items are returned.
- Around line 225-232: listProductImages에서 soft-deleted 이미지를 필터링하지 않아
재정렬(reorder) 검증 시 길이 불일치가 발생할 수 있습니다; 상품 이미지 조회 이수인 listProductImages와 그 내부
prisma.productImage.findMany 호출의 where 조건에 soft-delete 조건(예: deleted_at IS NULL
또는 is_deleted = false)을 추가하여 삭제된 이미지는 제외되도록 수정하세요.
- Around line 384-394: listOptionGroupsByProduct currently returns option groups
without excluding soft-deleted records set by softDeleteOptionGroup; update the
Prisma query in listOptionGroupsByProduct to add where filters for
productOptionGroup: deleted_at: null and is_active: true, and also apply the
same active/deleted filters inside the include for option_items (e.g.,
include.option_items.where = { deleted_at: null, is_active: true }) so both
groups and their items exclude soft-deleted/inactive rows.
- Around line 17-79: The query in listProductsByStore (prisma.product.findMany)
is missing a soft-delete guard; add a condition to the where clause to filter
out soft-deleted rows (e.g., include deleted_at: null alongside store_id and the
existing dynamic filters) so only non-deleted products are returned. Ensure the
new condition composes with the existing spreaded filters (cursor, isActive,
categoryId, search) in the same where object.
In `@src/features/seller/seller-store.graphql`:
- Around line 29-35: Several input/output types (e.g., SellerDateCursorInput)
currently use DateTime for fields that are date-only or time-only; change those
fields to use separate Date and Time scalars (or validated string scalars)
instead of DateTime — specifically update SellerDateCursorInput.fromDate/toDate,
any SellerCreate/Edit inputs with closureDate/capacityDate, and time-only fields
like openTime/closeTime to use Time scalar; after changing SDL, update and
verify resolver signatures and any input/output type names referenced by
resolvers so they remain consistent with the new scalar types.
In `@src/features/seller/services/seller-content.service.ts`:
- Around line 53-77: The sellerCreateFaqTopic flow performs two separate writes
(this.repo.createFaqTopic and this.repo.createAuditLog) so a failure on the
second call can leave the system state inconsistent; wrap both the data write
and the audit log insert in a single repository transaction (use your repo's
transactional API, e.g., this.repo.transaction or runInTransaction) after
obtaining ctx via requireSellerContext, and commit them together so either both
succeed or both roll back; apply the same change to the other seller content
methods that call createX and createAuditLog (the other CRUD methods flagged in
the comment) to ensure data + audit are written atomically.
- Around line 177-202: The banner creation/upsert logic can persist stale link
target fields that don't match the final linkType; update the payload right
after computing the normalized linkType (use
this.toBannerLinkType(input.linkType ?? 'NONE')) to set unrelated link fields to
null so only the correct target remains—e.g., when linkType is NONE clear
linkUrl, linkProductId, linkStoreId, linkCategoryId; when PRODUCT keep only
linkProductId and null others; when STORE keep only linkStoreId; when CATEGORY
keep only linkCategoryId. Apply this normalization before calling
this.validateBannerOwnership(...) and before calling this.repo.createBanner(...)
(and mirror the same normalization in the update flow that calls
repo.updateBanner) so saved values always match the computed linkType.
In `@src/features/seller/services/seller-product.service.ts`:
- Around line 47-54: Add unit tests for SellerProductService: create a
seller-product.service.spec.ts that instantiates SellerProductService with
mocked SellerRepository and ProductRepository (the constructor injection used in
SellerProductService extends SellerBaseService) and covers its public methods,
including success paths, validation/error branches, and interactions with
productRepository and repo (e.g., calls to save/update/delete/find). Use Jest
mocks/spies to assert repository method calls, simulate repository errors to
assert error handling, and include tests for any input validation logic so the
service behavior is fully exercised.
- Around line 212-228: In sellerUpdateProduct, add cross-field validation when
either sale_price or regular_price is present: fetch the existing product (via
the same repo/getter used in this service), determine the newRegular =
data.regular_price ?? existing.regular_price and newSale = data.sale_price ??
existing.sale_price, then assert newSale <= newRegular (and non-negative) and
newRegular >= newSale using the same validation path (e.g., call
this.assertPositiveRange for bounds and throw a validation error if newSale >
newRegular). Update the block around the existing assertPositiveRange calls so
single-field updates are validated against the stored counterpart before
applying the update.
In `@src/features/seller/services/seller-store.service.ts`:
- Around line 296-316: The code validates each field separately but misses a
cross-check that final minLeadTimeMinutes does not exceed final maxDaysAhead (in
minutes), which allows impossible pickup policies; before calling
this.repo.updateStore, load the current store settings (via the repo method you
have, e.g., this.repo.getStoreById / this.repo.findOne using ctx.storeId),
compute finalMinLead = input.minLeadTimeMinutes ?? current.min_lead_time_minutes
and finalMaxDays = input.maxDaysAhead ?? current.max_days_ahead, convert
finalMaxDays to minutes (finalMaxDays * 24 * 60) and call a validation (or
throw) if finalMinLead > finalMaxDaysInMinutes; keep existing
assertPositiveRange checks and reference the same symbols:
input.minLeadTimeMinutes, input.maxDaysAhead, this.repo.updateStore,
ctx.storeId, and assertPositiveRange.
- Around line 157-176: The updateStore flow must wrap the data mutations and
audit log insertion in a single DB transaction to avoid partial commits; modify
SellerRepository methods (e.g., where updateStore is called) to use
prisma.$transaction(async (tx) => { ... }) and call tx-equivalent methods for
updateStore, upsertStoreBusinessHour, upsertStoreSpecialClosure,
softDeleteStoreSpecialClosure, upsertStoreDailyCapacity and createAuditLog
within that transaction so both the domain change and the audit insert succeed
or roll back together; follow the existing pattern used in other repos
(product/auth/user/order/conversation) so updateStore (and related mutators) use
the transaction client and createAuditLog is executed inside the same
transaction scope.
In `@src/features/user/services/user-base.service.ts`:
- Around line 109-118: The current parsing of pagination in user-base.service.ts
allows fractional values because it uses Number(input) without integer
validation; update the logic that computes offset and limit to reject
non-integer inputs by checking Number.isFinite and Number.isInteger (or
explicitly parsing and validating integers) before assigning, and throw
BadRequestException when offset or limit are not whole numbers or out of range;
keep the existing range checks for offset < 0 and limit <= 0 || limit > 50 and
preserve unreadOnly handling.
- Around line 1-122: Add focused unit tests that directly exercise
UserBaseService's key helpers: write a test suite for requireActiveUser (call
the method on a real instance or subclass using a mocked
UserRepository.findAccountWithProfile) covering deleted account, wrong
account_type, and missing user_profile cases; add tests for normalizeNickname
(too short, too long, invalid chars, and valid), normalizePhoneNumber (too
short, too long, invalid chars, valid), normalizeBirthDate (invalid date string,
future date, valid date), and normalizePaginationInput (negative offset, limit
0, limit >50, and valid boundary values). Use the class UserBaseService
(instantiate a minimal concrete subclass) and the methods requireActiveUser,
normalizeNickname, normalizePhoneNumber, normalizeBirthDate,
normalizePaginationInput; assert that the appropriate
BadRequestException/UnauthorizedException/ForbiddenException are thrown or valid
normalized values returned.
---
Outside diff comments:
In @.github/scripts/pr-ai-description.mjs:
- Around line 943-985: commitMessages are passed raw into buildOpenAiPrompt
which risks leaking sensitive data and excessive token usage; before calling
buildOpenAiPrompt, sanitize and size-limit commit messages by mapping over the
array and applying maskSensitiveContent to each message (use the same masking
function used for diffs) and then enforce a byte/character cap per-message
and/or total (truncate long messages and/or drop oldest messages when over a
maxCommitBytes), updating places where commitMessages is assigned (results of
fetchPrCommitMessages and collectCommitMessagesFromGit) so buildOpenAiPrompt
receives only masked, truncated commitMessages and consider logging a redacted
summary (counts/bytes) rather than full messages.
In `@src/features/seller/services/seller-order.service.spec.ts`:
- Around line 64-100: Add a success-path spec for sellerUpdateOrderStatus: mock
repo.findSellerAccountContext and orderRepo.findOrderDetailByStore to return a
valid seller and an order in a state that allows transition (e.g., SUBMITTED →
MADE), call service.sellerUpdateOrderStatus(BigInt(1), { orderId: '10',
toStatus: 'MADE', note: null }), and assert that
OrderRepository.updateOrderStatusBySeller (or
orderRepo.updateOrderStatusBySeller in your test doubles) was called once with
the correct order id, target status and seller context; ensure any
time/uuid/network/DB dependencies are stubbed as in other tests and verify
returned value or no-exception behavior for the successful path.
---
Minor comments:
In @.github/scripts/pr-ai-description.mjs:
- Around line 960-964: The block handling the gitError fallback is misformatted
for Prettier; reformat the try/catch snippet around logInfo and logWarn to match
project Prettier rules (proper line breaks and indentation) so static analysis
passes: ensure the logInfo call ends with a semicolon on its own line and the
catch block formats the logWarn invocation with its options object on a
correctly indented new line, referencing the existing symbols commitMessages,
logInfo, logWarn, and gitError to locate and reflow the statements without
changing semantics.
In `@src/features/order/policies/order-status-transition.policy.ts`:
- Around line 15-43: The seller transition logic in assertSellerTransition is
missing a guard against transitioning to OrderStatus.SUBMITTED; add an explicit
check in assertSellerTransition that if to === OrderStatus.SUBMITTED you throw a
BadRequestException (e.g., "Order cannot be set to SUBMITTED by seller") to
block sellers from reverting/setting orders to SUBMITTED, keeping existing
checks for same-state and other transitions intact.
In `@src/features/product/product.types.graphql`:
- Line 1: The SDL file's top description incorrectly says "공용 주문 상태" while the
enum represents ProductVisibility; update the description string above the
ProductVisibility enum to describe product visibility/publication status (e.g.,
공개/비공개/임시표시 등) and ensure the new comment wording aligns with Resolver names and
any related input/output types that reference ProductVisibility so documentation
and SDL remain consistent.
In `@src/features/user/services/user-base.service.ts`:
- Around line 82-84: The current phone validation in user-base.service.ts allows
strings of only hyphens (e.g., "-------"); update the regex used in the
validation (the test applied to trimmed in the same block where
BadRequestException is thrown) to require at least one digit while still
allowing hyphens, for example replace /^[0-9-]+$/ with /^(?=.*\d)[0-9-]+$/ (or
an equivalent pattern) so inputs must contain at least one digit but only digits
and hyphens are permitted.
- Around line 90-101: The normalization code should defensively handle date-only
strings to avoid timezone shifts: if the input raw matches /^\d{4}-\d{2}-\d{2}$/
(a YYYY-MM-DD date-only string), parse the parts and construct the date as a
local-midnight Date via new Date(year, month-1, day) instead of new Date(raw);
otherwise keep the existing behavior (const date = raw instanceof Date ? raw :
new Date(raw)). Then continue the same NaN check, zeroing hours with
normalized.setHours(0,0,0,0), and the future-date check before returning
normalized (referencing the variables raw, date, normalized in this function).
In `@src/features/user/services/user-engagement.service.ts`:
- Around line 11-33: 추가할 테스트 파일 user-engagement.service.spec.ts를 작성해 주세요:
UserEngagementService의 likeReview 메서드에 대한 유닛 테스트를 추가하고, UserRepository를
목(mock)으로 주입하여 동작을 검증합니다; 구체적으로 requireActiveUser가 활성 유저를 확인하도록 목 처리하고
repo.likeReview가 'not-found'를 반환할 때 NotFoundException을, 'self-like'를 반환할 때
BadRequestException을 던지는 케이스를 각각 테스트하며 정상적으로 true를 반환하는 성공 케이스도 포함합니다; 테스트에서
UserEngagementService 생성자는 목 리포지토리(UserRepository)로 생성하고 likeReview(accountId,
reviewId)를 호출해 예외 메시지/타입과 true 반환을 검증하세요.
---
Nitpick comments:
In `@src/features/conversation/repositories/conversation.repository.ts`:
- Around line 60-83: The update to storeConversation uses only the id in
tx.storeConversation.update({ where: { id: args.conversationId } }), which is
fragile; change the where clause to include the store ownership check by adding
the store identifier equals args.sellerAccountId (e.g., where: { id:
args.conversationId, store_id: args.sellerAccountId } or use seller_account_id
if that is the column name) so the update only affects the conversation owned by
that store; update the tx.storeConversation.update call accordingly within the
same transaction alongside tx.storeConversationMessage.create.
In `@src/features/seller/seller-common.graphql`:
- Around line 2-5: The SellerCursorInput.limit has a default but no upper bound;
update the resolver/service that consumes SellerCursorInput (e.g., the
listSellers / getSellers resolver or the service method handling cursor
pagination) to clamp the incoming limit to a safe max (suggest 100). Implement:
read input.limit, set limit = Math.min(input.limit || 20, MAX_LIMIT) (or
equivalent in your language), and use that clamped value for DB queries and
pagination logic to prevent oversized requests. Ensure MAX_LIMIT is a named
constant and add a brief comment referencing SellerCursorInput.limit.
In `@src/features/seller/services/seller-base.service.ts`:
- Around line 116-125: The method assertPositiveRange does not match its
behavior (it validates an integer within a min/max, not strictly positive);
rename it to assertIntegerInRange (or similar) and update all callers (e.g.,
sellerCreateProduct) to the new name, and change the error message to be
explicit like "`<field> must be an integer between <min> and <max>.`" Keep the
existing Number.isInteger(value) and range checks intact; run/adjust any
imports, tests, and TS references so compilation passes.
In `@src/features/seller/services/seller-conversation.service.ts`:
- Around line 118-137: Currently createSellerConversationMessage (via
conversationRepository) and createAuditLog (via repo.createAuditLog) run in
separate operations so a failure in createAuditLog can leave a created message
without an audit entry; wrap both operations in a single database transaction
(use your repository/ORM transaction helper—e.g.,
conversationRepository.transaction or a shared transaction manager) and perform
createSellerConversationMessage and repo.createAuditLog inside that transaction
so that if createAuditLog fails the message creation is rolled back; ensure you
use the same transactional entity manager for both calls and propagate/throw
errors to trigger rollback.
In `@src/features/seller/services/seller-product.service.ts`:
- Around line 1073-1160: The inline complex parameter type on toProductOutput
should be extracted into a reusable named type/interface (e.g., ProductRow or
SellerProductRow) in a shared types file so other services can reuse it and the
mapper stays readable; update the toProductOutput signature to accept that new
type (keep SellerProductOutput as the return), move nested types (images,
product_categories, product_tags, option_groups, custom_template, text_tokens)
into exported interfaces or type aliases alongside ProductRow, and update any
helper methods referenced (toProductImageOutput, toOptionGroupOutput,
toCustomTemplateOutput) to import and use the new types as needed.
- Around line 96-160: sellerCreateProduct currently calls
productRepository.createProduct and then separately
productRepository.addProductImage and finally re-queries with
productRepository.findProductByIdIncludingInactive, causing a redundant lookup;
fix by returning the full product (including images) from createProduct or by
wrapping createProduct + addProductImage + repo.createAuditLog in a single
transaction so you can use the returned created entity (with images) instead of
calling findProductByIdIncludingInactive. Update productRepository.createProduct
to accept/include images (or an include option) and return the complete product,
or implement a transactional method (e.g., createProductWithImages) that
performs createProduct, addProductImage, and audit log and returns the final
product; then replace the extra findProductByIdIncludingInactive call in
sellerCreateProduct to use that returned product.
In `@src/features/seller/services/seller-store.service.ts`:
- Around line 109-155: The service is assembling Prisma field names (the data
const) directly; move DB field mapping into SellerRepository so the service only
performs domain cleaning. In seller-store.service.ts replace building the
Prisma.StoreUpdateInput (the data const) with creating a plain domain DTO of
cleaned values using cleanRequiredText/cleanNullableText/toDecimal, then call a
new repository method (e.g., SellerRepository.updateStore or
SellerRepository.mapAndUpdateStore) that converts those domain fields to Prisma
keys like store_name, address_full and performs the update; ensure all
references to map_provider, website_url, business_hours_text, latitude/longitude
are mapped inside the repository.
In `@src/features/user/resolvers/user-profile-mutation.resolver.ts`:
- Around line 20-21: UserProfileMutationResolver lacks unit tests; add tests in
user.resolver.spec.ts (or new user-profile-mutation.resolver.spec.ts) that
instantiate UserProfileMutationResolver with a mocked UserProfileService, verify
each public mutation method on UserProfileMutationResolver calls the
corresponding UserProfileService method with correct arguments and handles
success/error paths (e.g., resolve values and thrown errors), and include
setup/teardown and necessary mocks/spies to assert interactions with
UserProfileService.
In `@src/features/user/services/user-notification.service.spec.ts`:
- Around line 48-70: Add unit tests to cover the missing normal and branch
flows: create tests for viewerCounts success by mocking repo.getViewerCounts (or
the service method that calls it) to return expected counts and assert the
service.viewerCounts (or equivalent) returns them; add a myNotifications test
that validates mapping of notification items and hasMore calculation by mocking
repo.listNotifications to return items and totalCount > offset+limit and
asserting transformed output and hasMore true/false; add a markNotificationRead
success test by mocking repo.markNotificationRead to return true and asserting
no exception and that repo methods (e.g., findAccountWithProfile,
markNotificationRead) were called with correct IDs; add a test for
markAllNotificationsRead normal flow by mocking the repo method (e.g.,
markAllNotificationsRead) to succeed and asserting it was invoked and returns
expected result; in each test reference the existing service methods
myNotifications, markNotificationRead, markAllNotificationsRead and repo mocks
(repo.listNotifications, repo.markNotificationRead,
repo.markAllNotificationsRead, repo.findAccountWithProfile) to locate where to
add the assertions and mocks.
In `@src/features/user/services/user-notification.service.ts`:
- Around line 57-61: The service currently calls new Date() directly (e.g., when
calling this.repo.markNotificationRead), which makes time-based tests brittle;
refactor to inject a clock/time provider into the UserNotificationService (or
accept an optional now: Date parameter on public methods like
markNotificationRead/markAllRead) and replace direct new Date() calls with the
injected provider (e.g., this.clock.now() or the passed-in now). Update the
constructor to accept the clock dependency (defaulting to a real clock
implementation) and adjust calls to this.repo.markNotificationRead,
markNotificationUnread, etc., to use the injected time so tests can supply a
fake clock or fixed Date.
In `@src/features/user/services/user-profile.service.spec.ts`:
- Around line 75-82: Add missing positive unit tests for updateMyProfile: mock
repo.findAccountWithProfile to return baseAccount and repo.isNicknameTaken to
return false, call service.updateMyProfile(BigInt(1), { nickname: 'tester2' })
and assert updateProfile was called with the expected profile data and the
method returns the updated profile; likewise add simple success tests for
completeOnboarding and updateMyProfileImage that mock necessary repo/service
dependencies, invoke service.completeOnboarding and
service.updateMyProfileImage, and assert the appropriate repository methods
(e.g., markOnboarded or save/update) were called and correct values are
returned. Ensure tests reference and assert interactions with updateProfile,
repo.findAccountWithProfile, repo.isNicknameTaken, completeOnboarding, and
updateMyProfileImage.
In `@src/features/user/services/user-profile.service.ts`:
- Around line 120-133: The current deleteMyAccount method builds a
deletedNickname using `deleted_${accountId}` which exposes the raw accountId;
change the `deletedNickname` generation in `deleteMyAccount` to avoid including
the accountId directly (e.g., replace with a non-identifying token such as a
secure random string, UUID, or a one-way hash/salted digest of
accountId+timestamp) and update any related storage/validation expectations in
`repo.softDeleteAccount` to accept the new format; ensure the change is applied
in the `deleteMyAccount` function and any consumers that assume the old pattern
so no sensitive accountId is leaked in nicknames, logs, or responses.
In `@src/global/auth/parse-account-id.ts`:
- Around line 1-10: The parseAccountId function duplicates parseId logic;
replace the duplicate parsing code by delegating to the shared utility (e.g.,
parseId) to avoid drift: remove the try/catch BigInt logic in parseAccountId and
call the common parseId utility (ensuring it returns bigint and throws
BadRequestException on invalid input), preserve the exported function signature
parseAccountId(user: JwtUser): bigint, and update imports to import parseId from
the common util so all callers use the single implementation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7882af43-958d-47d9-b686-1da02af23095
📒 Files selected for processing (78)
.github/scripts/pr-ai-description.mjssrc/common/utils/id-parser.tssrc/features/conversation/conversation.module.tssrc/features/conversation/conversation.types.graphqlsrc/features/conversation/index.tssrc/features/conversation/repositories/conversation.repository.tssrc/features/core/root.graphqlsrc/features/order/index.tssrc/features/order/order.module.tssrc/features/order/order.types.graphqlsrc/features/order/policies/order-status-transition.policy.tssrc/features/order/repositories/order.repository.tssrc/features/order/services/order-domain.service.tssrc/features/product/index.tssrc/features/product/product.module.tssrc/features/product/product.types.graphqlsrc/features/product/repositories/product.repository.tssrc/features/seller/repositories/seller.repository.tssrc/features/seller/resolvers/seller-content-mutation.resolver.tssrc/features/seller/resolvers/seller-content-query.resolver.tssrc/features/seller/resolvers/seller-conversation-mutation.resolver.tssrc/features/seller/resolvers/seller-conversation-query.resolver.tssrc/features/seller/resolvers/seller-mutation.resolver.tssrc/features/seller/resolvers/seller-order-mutation.resolver.tssrc/features/seller/resolvers/seller-order-query.resolver.tssrc/features/seller/resolvers/seller-product-mutation.resolver.tssrc/features/seller/resolvers/seller-product-query.resolver.tssrc/features/seller/resolvers/seller-query.resolver.tssrc/features/seller/resolvers/seller-store-mutation.resolver.tssrc/features/seller/resolvers/seller-store-query.resolver.tssrc/features/seller/resolvers/seller.resolver.spec.tssrc/features/seller/seller-common.graphqlsrc/features/seller/seller-content.graphqlsrc/features/seller/seller-conversation.graphqlsrc/features/seller/seller-order.graphqlsrc/features/seller/seller-product.graphqlsrc/features/seller/seller-store.graphqlsrc/features/seller/seller.graphqlsrc/features/seller/seller.module.tssrc/features/seller/seller.service.tssrc/features/seller/services/seller-base.service.tssrc/features/seller/services/seller-content.service.tssrc/features/seller/services/seller-conversation.service.tssrc/features/seller/services/seller-order.service.spec.tssrc/features/seller/services/seller-order.service.tssrc/features/seller/services/seller-product.service.tssrc/features/seller/services/seller-store.service.tssrc/features/system/system.graphqlsrc/features/user/resolvers/user-engagement-mutation.resolver.tssrc/features/user/resolvers/user-mutation.resolver.tssrc/features/user/resolvers/user-notification-mutation.resolver.tssrc/features/user/resolvers/user-notification-query.resolver.tssrc/features/user/resolvers/user-profile-mutation.resolver.tssrc/features/user/resolvers/user-profile-query.resolver.tssrc/features/user/resolvers/user-query.resolver.tssrc/features/user/resolvers/user-search-mutation.resolver.tssrc/features/user/resolvers/user-search-query.resolver.tssrc/features/user/resolvers/user.resolver.spec.tssrc/features/user/services/user-base.service.tssrc/features/user/services/user-engagement.service.tssrc/features/user/services/user-notification.service.spec.tssrc/features/user/services/user-notification.service.tssrc/features/user/services/user-profile.service.spec.tssrc/features/user/services/user-profile.service.tssrc/features/user/services/user-search.service.tssrc/features/user/user-cart.graphqlsrc/features/user/user-common.graphqlsrc/features/user/user-engagement.graphqlsrc/features/user/user-notification.graphqlsrc/features/user/user-order.graphqlsrc/features/user/user-profile.graphqlsrc/features/user/user-search.graphqlsrc/features/user/user.graphqlsrc/features/user/user.module.tssrc/features/user/user.service.spec.tssrc/features/user/user.service.tssrc/global/auth/index.tssrc/global/auth/parse-account-id.ts
💤 Files with no reviewable changes (10)
- src/features/user/resolvers/user-query.resolver.ts
- src/features/user/resolvers/user-mutation.resolver.ts
- src/features/user/user.service.spec.ts
- src/features/seller/resolvers/seller-query.resolver.ts
- src/features/seller/seller.service.ts
- src/features/seller/resolvers/seller-mutation.resolver.ts
- src/features/user/user.graphql
- src/features/user/user.service.ts
- src/features/seller/seller.graphql
- src/features/seller/repositories/seller.repository.ts
| return this.prisma.$transaction(async (tx) => { | ||
| const order = await tx.order.findFirst({ | ||
| where: { | ||
| id: args.orderId, | ||
| items: { | ||
| some: { | ||
| store_id: args.storeId, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| if (!order) { | ||
| return null; | ||
| } | ||
|
|
||
| const fromStatus = order.status; | ||
|
|
||
| const updatedOrder = await tx.order.update({ | ||
| where: { | ||
| id: order.id, | ||
| }, | ||
| data: { | ||
| status: args.toStatus, | ||
| ...(args.toStatus === OrderStatus.CONFIRMED | ||
| ? { confirmed_at: args.now } | ||
| : {}), | ||
| ...(args.toStatus === OrderStatus.MADE ? { made_at: args.now } : {}), | ||
| ...(args.toStatus === OrderStatus.PICKED_UP | ||
| ? { picked_up_at: args.now } | ||
| : {}), | ||
| ...(args.toStatus === OrderStatus.CANCELED | ||
| ? { canceled_at: args.now } | ||
| : {}), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
상태 변경 갱신에 현재 상태 조건이 없어 경쟁 요청이 서로 덮어쓸 수 있습니다.
지금 구현은 같은 주문을 두 요청이 동시에 읽으면 둘 다 같은 fromStatus를 기준으로 update/history/notification을 남길 수 있습니다. where: { id, status: fromStatus } 조건의 원자적 갱신이나 row lock이 없으면 중복 이력과 잘못된 알림이 생깁니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/order/repositories/order.repository.ts` around lines 116 - 151,
The update must be made atomic against the previously read status to prevent
race conditions: change the update call on tx.order.update (after retrieving
order and setting fromStatus) to include the current status in the where clause
(i.e., where: { id: order.id, status: fromStatus }) or perform an updateMany
with that where and then check the affected count; if no rows were updated,
abort/return null so you don't create duplicate history/notifications. Keep the
same timestamp/field changes (confirmed_at, made_at, picked_up_at, canceled_at)
and ensure downstream logic (history/notification creation) only runs when the
conditional update actually modified a row (use updated row existence/affected
count to gate those actions).
| const notificationEvent = this.orderStatusToNotificationEvent( | ||
| args.toStatus, | ||
| ); | ||
| if (notificationEvent) { | ||
| await tx.notification.create({ | ||
| data: { | ||
| account_id: order.account_id, | ||
| type: NotificationType.ORDER_STATUS, | ||
| title: this.notificationTitleByOrderStatus(args.toStatus), | ||
| body: this.notificationBodyByOrderStatus( | ||
| updatedOrder.order_number, | ||
| args.toStatus, | ||
| ), | ||
| event: notificationEvent, | ||
| order_id: order.id, | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
CANCELED 전환은 구매자 알림이 생성되지 않습니다.
제목/본문 helper는 취소 상태를 처리하지만 orderStatusToNotificationEvent()가 CANCELED에서 null을 반환해서 notification row가 아예 만들어지지 않습니다. 취소도 별도 event를 매핑하거나 공용 status-change event로 보내야 합니다.
Also applies to: 205-240
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/order/repositories/order.repository.ts` around lines 163 - 180,
The code skips creating a notification for orders transitioning to CANCELED
because orderStatusToNotificationEvent(args.toStatus) returns null; update that
mapping so CANCELED returns a proper NotificationEvent (e.g., ORDER_CANCELED or
a generic STATUS_CHANGED event) OR change the conditional around
tx.notification.create to treat a null as a fallback event and still call
tx.notification.create with NotificationType.ORDER_STATUS, title via
notificationTitleByOrderStatus, body via
notificationBodyByOrderStatus(updatedOrder.order_number, args.toStatus), and
order_id: order.id; modify the orderStatusToNotificationEvent function (and any
callers) to include CANCELED mapping, or add a fallbackEvent before the if-check
to ensure tx.notification.create always runs for status changes like CANCELED.
| return this.prisma.product.findMany({ | ||
| where: { | ||
| store_id: args.storeId, | ||
| ...(args.cursor ? { id: { lt: args.cursor } } : {}), | ||
| ...(args.isActive !== undefined ? { is_active: args.isActive } : {}), | ||
| ...(args.categoryId | ||
| ? { | ||
| product_categories: { | ||
| some: { | ||
| category_id: args.categoryId, | ||
| }, | ||
| }, | ||
| } | ||
| : {}), | ||
| ...(args.search | ||
| ? { | ||
| OR: [ | ||
| { name: { contains: args.search } }, | ||
| { | ||
| product_tags: { | ||
| some: { | ||
| tag: { | ||
| name: { contains: args.search }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| } | ||
| : {}), | ||
| }, | ||
| include: { | ||
| images: { orderBy: { sort_order: 'asc' } }, | ||
| product_categories: { | ||
| include: { | ||
| category: true, | ||
| }, | ||
| }, | ||
| product_tags: { | ||
| include: { | ||
| tag: true, | ||
| }, | ||
| }, | ||
| option_groups: { | ||
| orderBy: { sort_order: 'asc' }, | ||
| include: { | ||
| option_items: { | ||
| orderBy: { sort_order: 'asc' }, | ||
| }, | ||
| }, | ||
| }, | ||
| custom_template: { | ||
| include: { | ||
| text_tokens: { | ||
| orderBy: { sort_order: 'asc' }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| orderBy: { id: 'desc' }, | ||
| take: args.limit + 1, | ||
| }); | ||
| } |
There was a problem hiding this comment.
listProductsByStore에서 deleted_at 필터가 누락되었습니다.
soft delete된 상품이 목록에 포함될 수 있습니다. is_active 필터는 있지만 deleted_at: null 조건이 필요합니다.
🐛 수정 제안
return this.prisma.product.findMany({
where: {
store_id: args.storeId,
+ deleted_at: null,
...(args.cursor ? { id: { lt: args.cursor } } : {}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return this.prisma.product.findMany({ | |
| where: { | |
| store_id: args.storeId, | |
| ...(args.cursor ? { id: { lt: args.cursor } } : {}), | |
| ...(args.isActive !== undefined ? { is_active: args.isActive } : {}), | |
| ...(args.categoryId | |
| ? { | |
| product_categories: { | |
| some: { | |
| category_id: args.categoryId, | |
| }, | |
| }, | |
| } | |
| : {}), | |
| ...(args.search | |
| ? { | |
| OR: [ | |
| { name: { contains: args.search } }, | |
| { | |
| product_tags: { | |
| some: { | |
| tag: { | |
| name: { contains: args.search }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ], | |
| } | |
| : {}), | |
| }, | |
| include: { | |
| images: { orderBy: { sort_order: 'asc' } }, | |
| product_categories: { | |
| include: { | |
| category: true, | |
| }, | |
| }, | |
| product_tags: { | |
| include: { | |
| tag: true, | |
| }, | |
| }, | |
| option_groups: { | |
| orderBy: { sort_order: 'asc' }, | |
| include: { | |
| option_items: { | |
| orderBy: { sort_order: 'asc' }, | |
| }, | |
| }, | |
| }, | |
| custom_template: { | |
| include: { | |
| text_tokens: { | |
| orderBy: { sort_order: 'asc' }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| orderBy: { id: 'desc' }, | |
| take: args.limit + 1, | |
| }); | |
| } | |
| return this.prisma.product.findMany({ | |
| where: { | |
| store_id: args.storeId, | |
| deleted_at: null, | |
| ...(args.cursor ? { id: { lt: args.cursor } } : {}), | |
| ...(args.isActive !== undefined ? { is_active: args.isActive } : {}), | |
| ...(args.categoryId | |
| ? { | |
| product_categories: { | |
| some: { | |
| category_id: args.categoryId, | |
| }, | |
| }, | |
| } | |
| : {}), | |
| ...(args.search | |
| ? { | |
| OR: [ | |
| { name: { contains: args.search } }, | |
| { | |
| product_tags: { | |
| some: { | |
| tag: { | |
| name: { contains: args.search }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ], | |
| } | |
| : {}), | |
| }, | |
| include: { | |
| images: { orderBy: { sort_order: 'asc' } }, | |
| product_categories: { | |
| include: { | |
| category: true, | |
| }, | |
| }, | |
| product_tags: { | |
| include: { | |
| tag: true, | |
| }, | |
| }, | |
| option_groups: { | |
| orderBy: { sort_order: 'asc' }, | |
| include: { | |
| option_items: { | |
| orderBy: { sort_order: 'asc' }, | |
| }, | |
| }, | |
| }, | |
| custom_template: { | |
| include: { | |
| text_tokens: { | |
| orderBy: { sort_order: 'asc' }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| orderBy: { id: 'desc' }, | |
| take: args.limit + 1, | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/product/repositories/product.repository.ts` around lines 17 -
79, The query in listProductsByStore (prisma.product.findMany) is missing a
soft-delete guard; add a condition to the where clause to filter out
soft-deleted rows (e.g., include deleted_at: null alongside store_id and the
existing dynamic filters) so only non-deleted products are returned. Ensure the
new condition composes with the existing spreaded filters (cursor, isActive,
categoryId, search) in the same where object.
| async countProductImages(productId: bigint): Promise<number> { | ||
| return this.prisma.productImage.count({ | ||
| where: { product_id: productId }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
countProductImages에서 soft-deleted 이미지가 제외되지 않습니다.
softDeleteProductImage에서 deleted_at을 설정하지만, countProductImages는 삭제된 이미지도 카운트합니다. 이로 인해 이미지 추가 시 제한(5개) 검증이 부정확해질 수 있습니다.
🐛 수정 제안
async countProductImages(productId: bigint): Promise<number> {
return this.prisma.productImage.count({
- where: { product_id: productId },
+ where: {
+ product_id: productId,
+ deleted_at: null,
+ },
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async countProductImages(productId: bigint): Promise<number> { | |
| return this.prisma.productImage.count({ | |
| where: { product_id: productId }, | |
| }); | |
| } | |
| async countProductImages(productId: bigint): Promise<number> { | |
| return this.prisma.productImage.count({ | |
| where: { | |
| product_id: productId, | |
| deleted_at: null, | |
| }, | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/product/repositories/product.repository.ts` around lines 191 -
195, countProductImages currently counts all productImage rows including
soft-deleted ones set by softDeleteProductImage (deleted_at), causing incorrect
limits; update the count query in countProductImages to filter out soft-deleted
records by adding a where condition that deleted_at is null (i.e., include
deleted_at: null alongside product_id: productId) so only active images are
counted.
| async listProductImages(productId: bigint) { | ||
| return this.prisma.productImage.findMany({ | ||
| where: { | ||
| product_id: productId, | ||
| }, | ||
| orderBy: { sort_order: 'asc' }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
listProductImages에서도 soft-deleted 이미지 필터링이 필요합니다.
reorder 검증 시 사용되므로, 삭제된 이미지가 포함되면 길이 불일치 오류가 발생할 수 있습니다.
🐛 수정 제안
async listProductImages(productId: bigint) {
return this.prisma.productImage.findMany({
where: {
product_id: productId,
+ deleted_at: null,
},
orderBy: { sort_order: 'asc' },
});
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/product/repositories/product.repository.ts` around lines 225 -
232, listProductImages에서 soft-deleted 이미지를 필터링하지 않아 재정렬(reorder) 검증 시 길이 불일치가
발생할 수 있습니다; 상품 이미지 조회 이수인 listProductImages와 그 내부 prisma.productImage.findMany
호출의 where 조건에 soft-delete 조건(예: deleted_at IS NULL 또는 is_deleted = false)을 추가하여
삭제된 이미지는 제외되도록 수정하세요.
| if (data.regular_price !== undefined) { | ||
| this.assertPositiveRange( | ||
| data.regular_price as number, | ||
| 1, | ||
| 1_000_000_000, | ||
| 'regularPrice', | ||
| ); | ||
| } | ||
|
|
||
| if (data.sale_price !== undefined && data.sale_price !== null) { | ||
| this.assertPositiveRange( | ||
| data.sale_price as number, | ||
| 0, | ||
| 1_000_000_000, | ||
| 'salePrice', | ||
| ); | ||
| } |
There was a problem hiding this comment.
sellerUpdateProduct에서 salePrice와 regularPrice 교차 검증이 누락되었습니다.
salePrice만 업데이트하거나 regularPrice만 업데이트할 때, 현재 DB의 다른 값과 비교하지 않습니다. 예를 들어, regularPrice=100인 상품에서 salePrice=150으로만 업데이트하면 유효하지 않은 상태가 됩니다.
🐛 수정 제안
if (data.regular_price !== undefined) {
this.assertPositiveRange(
data.regular_price as number,
1,
1_000_000_000,
'regularPrice',
);
}
if (data.sale_price !== undefined && data.sale_price !== null) {
this.assertPositiveRange(
data.sale_price as number,
0,
1_000_000_000,
'salePrice',
);
+ const effectiveRegularPrice =
+ (data.regular_price as number | undefined) ?? current.regular_price;
+ if ((data.sale_price as number) > effectiveRegularPrice) {
+ throw new BadRequestException(
+ 'salePrice must be less than or equal to regularPrice.',
+ );
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (data.regular_price !== undefined) { | |
| this.assertPositiveRange( | |
| data.regular_price as number, | |
| 1, | |
| 1_000_000_000, | |
| 'regularPrice', | |
| ); | |
| } | |
| if (data.sale_price !== undefined && data.sale_price !== null) { | |
| this.assertPositiveRange( | |
| data.sale_price as number, | |
| 0, | |
| 1_000_000_000, | |
| 'salePrice', | |
| ); | |
| } | |
| if (data.regular_price !== undefined) { | |
| this.assertPositiveRange( | |
| data.regular_price as number, | |
| 1, | |
| 1_000_000_000, | |
| 'regularPrice', | |
| ); | |
| } | |
| if (data.sale_price !== undefined && data.sale_price !== null) { | |
| this.assertPositiveRange( | |
| data.sale_price as number, | |
| 0, | |
| 1_000_000_000, | |
| 'salePrice', | |
| ); | |
| const effectiveRegularPrice = | |
| (data.regular_price as number | undefined) ?? current.regular_price; | |
| if ((data.sale_price as number) > effectiveRegularPrice) { | |
| throw new BadRequestException( | |
| 'salePrice must be less than or equal to regularPrice.', | |
| ); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/seller/services/seller-product.service.ts` around lines 212 -
228, In sellerUpdateProduct, add cross-field validation when either sale_price
or regular_price is present: fetch the existing product (via the same
repo/getter used in this service), determine the newRegular = data.regular_price
?? existing.regular_price and newSale = data.sale_price ?? existing.sale_price,
then assert newSale <= newRegular (and non-negative) and newRegular >= newSale
using the same validation path (e.g., call this.assertPositiveRange for bounds
and throw a validation error if newSale > newRegular). Update the block around
the existing assertPositiveRange calls so single-field updates are validated
against the stored counterpart before applying the update.
| const updated = await this.repo.updateStore({ | ||
| storeId: ctx.storeId, | ||
| data, | ||
| }); | ||
|
|
||
| await this.repo.createAuditLog({ | ||
| actorAccountId: ctx.accountId, | ||
| storeId: ctx.storeId, | ||
| targetType: AuditTargetType.STORE, | ||
| targetId: ctx.storeId, | ||
| action: AuditActionType.UPDATE, | ||
| beforeJson: { | ||
| storeName: current.store_name, | ||
| storePhone: current.store_phone, | ||
| }, | ||
| afterJson: { | ||
| storeName: updated.store_name, | ||
| storePhone: updated.store_phone, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo_file="$(fd 'seller\.repository\.ts$' src | head -n1)"
sed -n '1,260p' "$repo_file"
printf '\n--- transaction-related symbols ---\n'
rg -n -C2 '\$transaction|inTransaction|withTransaction|createAuditLog|updateStore|upsertStoreBusinessHour|upsertStoreSpecialClosure|softDeleteStoreSpecialClosure|upsertStoreDailyCapacity|softDeleteStoreDailyCapacity' "$repo_file"Repository: CaQuick/caquick-be
Length of output: 7355
🏁 Script executed:
cat -n src/features/seller/repositories/seller.repository.ts | sed -n '400,450p'Repository: CaQuick/caquick-be
Length of output: 1744
🏁 Script executed:
rg -n '\$transaction|prisma\.\$transaction' src/features/seller --type ts -A 5 -B 2Repository: CaQuick/caquick-be
Length of output: 417
🏁 Script executed:
cat -n src/features/seller/services/seller-store.service.ts | sed -n '155,180p'Repository: CaQuick/caquick-be
Length of output: 831
🏁 Script executed:
cat -n src/features/seller/services/seller-store.service.ts | sed -n '130,180p'Repository: CaQuick/caquick-be
Length of output: 1808
🏁 Script executed:
rg -n 'prisma\.\$transaction|this\.prisma\.\$transaction' src/features/seller/services --type ts -B 3 -A 10Repository: CaQuick/caquick-be
Length of output: 44
🏁 Script executed:
rg -n 'updateStoreBasicInfo|updateStore' src/features/seller/services/seller-store.service.ts --type ts -B 2 -A 15 | head -60Repository: CaQuick/caquick-be
Length of output: 1240
🏁 Script executed:
rg -n 'await this\.repo\.' src/features/seller/services/seller-store.service.ts --type ts | head -20Repository: CaQuick/caquick-be
Length of output: 1278
🏁 Script executed:
fd 'seller' src/features/seller --type f --name '*.ts' | xargs wc -l | sort -rn | head -10Repository: CaQuick/caquick-be
Length of output: 331
🏁 Script executed:
rg -n 'transaction|Transaction' src/features/seller --type tsRepository: CaQuick/caquick-be
Length of output: 195
🏁 Script executed:
rg -n 'prisma\..*\$transaction|PrismaService.*transaction' src --type ts -B 3 -A 10Repository: CaQuick/caquick-be
Length of output: 23905
🏁 Script executed:
find src -name '*.ts' -type f -exec grep -l 'transaction' {} \;Repository: CaQuick/caquick-be
Length of output: 444
🏁 Script executed:
cat -n src/features/seller/repositories/seller.repository.ts | tail -50Repository: CaQuick/caquick-be
Length of output: 1607
도메인 변경과 감사 로그 쓰기가 동일 트랜잭션 내에서 처리되어야 합니다.
현재 구현에서 updateStore, upsertStoreBusinessHour, upsertStoreSpecialClosure, softDeleteStoreSpecialClosure, upsertStoreDailyCapacity 등의 뮤테이션들이 본 데이터 쓰기 후 createAuditLog를 별도 await로 수행합니다. 둘이 같은 트랜잭션이 아니면 audit insert 실패 시 실제 변경은 커밋됐는데 GraphQL은 에러를 반환할 수 있고, 클라이언트 재시도로 중복 변경까지 생길 수 있습니다.
SellerRepository에서 이 둘을 단일 트랜잭션으로 묶어야 합니다. 다른 repository들(product, auth, user, order, conversation)에서 이미 prisma.$transaction(async (tx) => {...}) 패턴으로 구현된 사례가 있으니 동일하게 적용해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/seller/services/seller-store.service.ts` around lines 157 - 176,
The updateStore flow must wrap the data mutations and audit log insertion in a
single DB transaction to avoid partial commits; modify SellerRepository methods
(e.g., where updateStore is called) to use prisma.$transaction(async (tx) => {
... }) and call tx-equivalent methods for updateStore, upsertStoreBusinessHour,
upsertStoreSpecialClosure, softDeleteStoreSpecialClosure,
upsertStoreDailyCapacity and createAuditLog within that transaction so both the
domain change and the audit insert succeed or roll back together; follow the
existing pattern used in other repos (product/auth/user/order/conversation) so
updateStore (and related mutators) use the transaction client and createAuditLog
is executed inside the same transaction scope.
| this.assertPositiveRange( | ||
| input.pickupSlotIntervalMinutes, | ||
| 5, | ||
| 180, | ||
| 'pickupSlotIntervalMinutes', | ||
| ); | ||
| this.assertPositiveRange( | ||
| input.minLeadTimeMinutes, | ||
| 0, | ||
| 7 * 24 * 60, | ||
| 'minLeadTimeMinutes', | ||
| ); | ||
| this.assertPositiveRange(input.maxDaysAhead, 1, 365, 'maxDaysAhead'); | ||
|
|
||
| const updated = await this.repo.updateStore({ | ||
| storeId: ctx.storeId, | ||
| data: { | ||
| pickup_slot_interval_minutes: input.pickupSlotIntervalMinutes, | ||
| min_lead_time_minutes: input.minLeadTimeMinutes, | ||
| max_days_ahead: input.maxDaysAhead, | ||
| }, |
There was a problem hiding this comment.
예약이 불가능한 픽업 정책 조합이 저장될 수 있습니다.
개별 범위만 검사해서 minLeadTimeMinutes > maxDaysAhead * 24 * 60 조합이 통과합니다. 이렇게 저장되면 어떤 날짜/시간도 예약할 수 없습니다. 현재값과 합친 최종 설정 기준으로 교차 검증을 추가해 주세요.
수정 예시
this.assertPositiveRange(
input.minLeadTimeMinutes,
0,
7 * 24 * 60,
'minLeadTimeMinutes',
);
this.assertPositiveRange(input.maxDaysAhead, 1, 365, 'maxDaysAhead');
+
+ const minLeadTimeMinutes =
+ input.minLeadTimeMinutes ?? current.min_lead_time_minutes;
+ const maxDaysAhead = input.maxDaysAhead ?? current.max_days_ahead;
+ if (minLeadTimeMinutes > maxDaysAhead * 24 * 60) {
+ throw new BadRequestException(
+ 'minLeadTimeMinutes must fit within maxDaysAhead.',
+ );
+ }
const updated = await this.repo.updateStore({
storeId: ctx.storeId,
data: {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.assertPositiveRange( | |
| input.pickupSlotIntervalMinutes, | |
| 5, | |
| 180, | |
| 'pickupSlotIntervalMinutes', | |
| ); | |
| this.assertPositiveRange( | |
| input.minLeadTimeMinutes, | |
| 0, | |
| 7 * 24 * 60, | |
| 'minLeadTimeMinutes', | |
| ); | |
| this.assertPositiveRange(input.maxDaysAhead, 1, 365, 'maxDaysAhead'); | |
| const updated = await this.repo.updateStore({ | |
| storeId: ctx.storeId, | |
| data: { | |
| pickup_slot_interval_minutes: input.pickupSlotIntervalMinutes, | |
| min_lead_time_minutes: input.minLeadTimeMinutes, | |
| max_days_ahead: input.maxDaysAhead, | |
| }, | |
| this.assertPositiveRange( | |
| input.pickupSlotIntervalMinutes, | |
| 5, | |
| 180, | |
| 'pickupSlotIntervalMinutes', | |
| ); | |
| this.assertPositiveRange( | |
| input.minLeadTimeMinutes, | |
| 0, | |
| 7 * 24 * 60, | |
| 'minLeadTimeMinutes', | |
| ); | |
| this.assertPositiveRange(input.maxDaysAhead, 1, 365, 'maxDaysAhead'); | |
| const minLeadTimeMinutes = | |
| input.minLeadTimeMinutes ?? current.min_lead_time_minutes; | |
| const maxDaysAhead = input.maxDaysAhead ?? current.max_days_ahead; | |
| if (minLeadTimeMinutes > maxDaysAhead * 24 * 60) { | |
| throw new BadRequestException( | |
| 'minLeadTimeMinutes must fit within maxDaysAhead.', | |
| ); | |
| } | |
| const updated = await this.repo.updateStore({ | |
| storeId: ctx.storeId, | |
| data: { | |
| pickup_slot_interval_minutes: input.pickupSlotIntervalMinutes, | |
| min_lead_time_minutes: input.minLeadTimeMinutes, | |
| max_days_ahead: input.maxDaysAhead, | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/seller/services/seller-store.service.ts` around lines 296 - 316,
The code validates each field separately but misses a cross-check that final
minLeadTimeMinutes does not exceed final maxDaysAhead (in minutes), which allows
impossible pickup policies; before calling this.repo.updateStore, load the
current store settings (via the repo method you have, e.g.,
this.repo.getStoreById / this.repo.findOne using ctx.storeId), compute
finalMinLead = input.minLeadTimeMinutes ?? current.min_lead_time_minutes and
finalMaxDays = input.maxDaysAhead ?? current.max_days_ahead, convert
finalMaxDays to minutes (finalMaxDays * 24 * 60) and call a validation (or
throw) if finalMinLead > finalMaxDaysInMinutes; keep existing
assertPositiveRange checks and reference the same symbols:
input.minLeadTimeMinutes, input.maxDaysAhead, this.repo.updateStore,
ctx.storeId, and assertPositiveRange.
| import { | ||
| BadRequestException, | ||
| ForbiddenException, | ||
| UnauthorizedException, | ||
| } from '@nestjs/common'; | ||
| import { AccountType } from '@prisma/client'; | ||
|
|
||
| import type { UserAccountWithProfile } from '../repositories/user.repository'; | ||
| import { UserRepository } from '../repositories/user.repository'; | ||
| import type { MePayload } from '../types/user-output.type'; | ||
|
|
||
| export type ActiveUserAccount = UserAccountWithProfile & { | ||
| deleted_at: null; | ||
| user_profile: NonNullable<UserAccountWithProfile['user_profile']> & { | ||
| deleted_at: null; | ||
| }; | ||
| }; | ||
|
|
||
| export abstract class UserBaseService { | ||
| protected constructor(protected readonly repo: UserRepository) {} | ||
|
|
||
| protected async requireActiveUser( | ||
| accountId: bigint, | ||
| ): Promise<ActiveUserAccount> { | ||
| const account = await this.repo.findAccountWithProfile(accountId, { | ||
| withDeleted: true, | ||
| }); | ||
| if (!account) throw new UnauthorizedException('Account not found.'); | ||
| if (account.deleted_at) { | ||
| throw new UnauthorizedException('Account is deleted.'); | ||
| } | ||
| if (account.account_type !== AccountType.USER) { | ||
| throw new ForbiddenException('Only USER account is allowed.'); | ||
| } | ||
| if (!account.user_profile || account.user_profile.deleted_at) { | ||
| throw new UnauthorizedException('User profile not found.'); | ||
| } | ||
| return account as ActiveUserAccount; | ||
| } | ||
|
|
||
| protected toMePayload(account: ActiveUserAccount): MePayload { | ||
| return { | ||
| accountId: account.id.toString(), | ||
| email: account.email, | ||
| name: account.name, | ||
| accountType: account.account_type, | ||
| profile: { | ||
| nickname: account.user_profile.nickname, | ||
| birthDate: account.user_profile.birth_date, | ||
| phoneNumber: account.user_profile.phone_number, | ||
| profileImageUrl: account.user_profile.profile_image_url, | ||
| onboardingCompletedAt: account.user_profile.onboarding_completed_at, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| protected normalizeNickname(raw: string): string { | ||
| const trimmed = raw.trim(); | ||
| if (trimmed.length < 2 || trimmed.length > 20) { | ||
| throw new BadRequestException('Nickname length must be 2~20.'); | ||
| } | ||
| const nicknameRegex = /^[A-Za-z0-9가-힣_]+$/; | ||
| if (!nicknameRegex.test(trimmed)) { | ||
| throw new BadRequestException('Nickname contains invalid characters.'); | ||
| } | ||
| return trimmed; | ||
| } | ||
|
|
||
| protected normalizeName(raw?: string | null): string | null { | ||
| if (raw === undefined || raw === null) return null; | ||
| const trimmed = raw.trim(); | ||
| return trimmed.length > 0 ? trimmed : null; | ||
| } | ||
|
|
||
| protected normalizePhoneNumber(raw?: string | null): string | null { | ||
| if (raw === undefined || raw === null) return null; | ||
| const trimmed = raw.trim(); | ||
| if (trimmed.length === 0) return null; | ||
| if (trimmed.length < 7 || trimmed.length > 20) { | ||
| throw new BadRequestException('Invalid phone number length.'); | ||
| } | ||
| if (!/^[0-9-]+$/.test(trimmed)) { | ||
| throw new BadRequestException('Invalid phone number format.'); | ||
| } | ||
| return trimmed; | ||
| } | ||
|
|
||
| protected normalizeBirthDate(raw?: Date | string | null): Date | null { | ||
| if (raw === undefined || raw === null) return null; | ||
| const date = raw instanceof Date ? raw : new Date(raw); | ||
| if (Number.isNaN(date.getTime())) { | ||
| throw new BadRequestException('Invalid birthDate.'); | ||
| } | ||
| const today = new Date(); | ||
| today.setHours(0, 0, 0, 0); | ||
| const normalized = new Date(date); | ||
| normalized.setHours(0, 0, 0, 0); | ||
| if (normalized > today) { | ||
| throw new BadRequestException('birthDate cannot be in the future.'); | ||
| } | ||
| return normalized; | ||
| } | ||
|
|
||
| protected normalizePaginationInput(input?: { | ||
| offset?: number | null; | ||
| limit?: number | null; | ||
| unreadOnly?: boolean | null; | ||
| }): { offset: number; limit: number; unreadOnly: boolean } { | ||
| const offset = Number.isFinite(input?.offset) ? Number(input?.offset) : 0; | ||
| const limit = Number.isFinite(input?.limit) ? Number(input?.limit) : 20; | ||
| const unreadOnly = Boolean(input?.unreadOnly); | ||
|
|
||
| if (offset < 0) { | ||
| throw new BadRequestException('Offset must be >= 0.'); | ||
| } | ||
| if (limit <= 0 || limit > 50) { | ||
| throw new BadRequestException('Limit must be between 1 and 50.'); | ||
| } | ||
|
|
||
| return { offset, limit, unreadOnly }; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# user-base.service 관련 테스트 파일 존재 여부 및 핵심 메서드 커버리지 확인
fd 'user-base.service.(spec|test)\.ts$' src
rg -nP --type=ts -C2 '\brequireActiveUser\b|\bnormalizeNickname\b|\bnormalizePhoneNumber\b|\bnormalizeBirthDate\b|\bnormalizePaginationInput\b' -g '**/*spec.ts' -g '**/*test.ts'Repository: CaQuick/caquick-be
Length of output: 44
🏁 Script executed:
#!/bin/bash
# Search for all test files in the user feature directory
find src/features/user -type f \( -name "*.spec.ts" -o -name "*.test.ts" \)Repository: CaQuick/caquick-be
Length of output: 228
🏁 Script executed:
#!/bin/bash
# Search for any references to UserBaseService or the specific methods in the codebase
rg -t ts 'UserBaseService|requireActiveUser|normalizeNickname|normalizePhoneNumber|normalizeBirthDate|normalizePaginationInput' src --type-listRepository: CaQuick/caquick-be
Length of output: 451
🏁 Script executed:
#!/bin/bash
# Check if user-base.service.ts is a new file or existing
git log --oneline --follow -- src/features/user/services/user-base.service.ts 2>/dev/null | head -5 || echo "File history not available"Repository: CaQuick/caquick-be
Length of output: 104
🏁 Script executed:
#!/bin/bash
# Search for files that import or use UserBaseService
rg -t ts 'UserBaseService' src/Repository: CaQuick/caquick-be
Length of output: 1030
🏁 Script executed:
#!/bin/bash
# Check what user-profile.service and user-notification.service extend/import
rg -t ts -A 5 'export class.*Service' src/features/user/services/Repository: CaQuick/caquick-be
Length of output: 2035
🏁 Script executed:
#!/bin/bash
# Look at the test file for user-profile.service to see if it tests inherited methods
head -150 src/features/user/services/user-profile.service.spec.tsRepository: CaQuick/caquick-be
Length of output: 2822
🏁 Script executed:
#!/bin/bash
# Check user-notification.service.spec.ts to see if it tests normalize methods
head -100 src/features/user/services/user-notification.service.spec.tsRepository: CaQuick/caquick-be
Length of output: 2170
🏁 Script executed:
#!/bin/bash
# Search for any test that explicitly tests normalize functions
rg -t ts 'normalize(Nickname|PhoneNumber|BirthDate|PaginationInput|Name)' src/features/user --type-list -v || \
rg -t ts 'normalize(Nickname|PhoneNumber|BirthDate|PaginationInput|Name)' src/features/user/Repository: CaQuick/caquick-be
Length of output: 2027
UserBaseService의 핵심 메서드에 대한 단위 테스트가 누락되었습니다.
requireActiveUser와 normalize* 함수들은 권한 검증과 입력 유효성 검사를 담당하는 중요한 경로이며, 4개 서비스(UserProfileService, UserNotificationService, UserSearchService, UserEngagementService)에서 사용되고 있습니다. 현재 존재하는 테스트 파일들(user-profile.service.spec.ts, user-notification.service.spec.ts)은 이들 메서드를 간접적으로만 호출하고 있으며, 다음의 케이스들에 대한 단위 테스트가 필요합니다:
requireActiveUser: 삭제된 계정, 잘못된 account_type, user_profile 누락 검증normalizeNickname: 길이(2-20) 및 정규식 검증normalizePhoneNumber: 길이(7-20) 및 형식(숫자-하이픈) 검증normalizeBirthDate: 유효한 날짜 형식, 미래 날짜 거부 검증normalizePaginationInput: offset >= 0, limit 1-50 경계값 검증
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/user/services/user-base.service.ts` around lines 1 - 122, Add
focused unit tests that directly exercise UserBaseService's key helpers: write a
test suite for requireActiveUser (call the method on a real instance or subclass
using a mocked UserRepository.findAccountWithProfile) covering deleted account,
wrong account_type, and missing user_profile cases; add tests for
normalizeNickname (too short, too long, invalid chars, and valid),
normalizePhoneNumber (too short, too long, invalid chars, valid),
normalizeBirthDate (invalid date string, future date, valid date), and
normalizePaginationInput (negative offset, limit 0, limit >50, and valid
boundary values). Use the class UserBaseService (instantiate a minimal concrete
subclass) and the methods requireActiveUser, normalizeNickname,
normalizePhoneNumber, normalizeBirthDate, normalizePaginationInput; assert that
the appropriate BadRequestException/UnauthorizedException/ForbiddenException are
thrown or valid normalized values returned.
| const offset = Number.isFinite(input?.offset) ? Number(input?.offset) : 0; | ||
| const limit = Number.isFinite(input?.limit) ? Number(input?.limit) : 20; | ||
| const unreadOnly = Boolean(input?.unreadOnly); | ||
|
|
||
| if (offset < 0) { | ||
| throw new BadRequestException('Offset must be >= 0.'); | ||
| } | ||
| if (limit <= 0 || limit > 50) { | ||
| throw new BadRequestException('Limit must be between 1 and 50.'); | ||
| } |
There was a problem hiding this comment.
페이지네이션 입력에 소수가 허용됩니다.
현재 offset=1.5, limit=10.2가 그대로 통과합니다. 페이지네이션 파라미터는 정수 제한을 명시적으로 걸어야 안전합니다.
🔧 수정 예시
protected normalizePaginationInput(input?: {
offset?: number | null;
limit?: number | null;
unreadOnly?: boolean | null;
}): { offset: number; limit: number; unreadOnly: boolean } {
- const offset = Number.isFinite(input?.offset) ? Number(input?.offset) : 0;
- const limit = Number.isFinite(input?.limit) ? Number(input?.limit) : 20;
+ const offset = Number.isFinite(input?.offset) ? Number(input?.offset) : 0;
+ const limit = Number.isFinite(input?.limit) ? Number(input?.limit) : 20;
const unreadOnly = Boolean(input?.unreadOnly);
- if (offset < 0) {
+ if (!Number.isInteger(offset) || offset < 0) {
throw new BadRequestException('Offset must be >= 0.');
}
- if (limit <= 0 || limit > 50) {
+ if (!Number.isInteger(limit) || limit <= 0 || limit > 50) {
throw new BadRequestException('Limit must be between 1 and 50.');
}
return { offset, limit, unreadOnly };
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const offset = Number.isFinite(input?.offset) ? Number(input?.offset) : 0; | |
| const limit = Number.isFinite(input?.limit) ? Number(input?.limit) : 20; | |
| const unreadOnly = Boolean(input?.unreadOnly); | |
| if (offset < 0) { | |
| throw new BadRequestException('Offset must be >= 0.'); | |
| } | |
| if (limit <= 0 || limit > 50) { | |
| throw new BadRequestException('Limit must be between 1 and 50.'); | |
| } | |
| const offset = Number.isFinite(input?.offset) ? Number(input?.offset) : 0; | |
| const limit = Number.isFinite(input?.limit) ? Number(input?.limit) : 20; | |
| const unreadOnly = Boolean(input?.unreadOnly); | |
| if (!Number.isInteger(offset) || offset < 0) { | |
| throw new BadRequestException('Offset must be >= 0.'); | |
| } | |
| if (!Number.isInteger(limit) || limit <= 0 || limit > 50) { | |
| throw new BadRequestException('Limit must be between 1 and 50.'); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/user/services/user-base.service.ts` around lines 109 - 118, The
current parsing of pagination in user-base.service.ts allows fractional values
because it uses Number(input) without integer validation; update the logic that
computes offset and limit to reject non-integer inputs by checking
Number.isFinite and Number.isInteger (or explicitly parsing and validating
integers) before assigning, and throw BadRequestException when offset or limit
are not whole numbers or out of range; keep the existing range checks for offset
< 0 and limit <= 0 || limit > 50 and preserve unreadOnly handling.
PR Summary
판매자 관련 도메인 로직을 별도의 모듈과 서비스로 분리하였습니다.
기존의 단일 SellerRepository에서 판매자, 주문, 상품, 대화 등 기능별로 리포지토리와 서비스가 분리되어 유지보수성과 확장성을 개선하였습니다.
GraphQL 리졸버도 기능별로 세분화하여 역할을 명확히 하였습니다.
Changes
.github/scripts/pr-ai-description.mjssrc/common/utils/id-parser.tssrc/features/conversation/conversation.module.tssrc/features/conversation/repositories/conversation.repository.tssrc/features/order/order.module.tssrc/features/order/policies/order-status-transition.policy.tssrc/features/order/repositories/order.repository.tssrc/features/order/services/order-domain.service.tssrc/features/product/product.module.tssrc/features/product/repositories/product.repository.tssrc/features/seller/repositories/seller.repository.tssrc/features/seller/resolvers/seller-content-mutation.resolver.tssrc/features/seller/resolvers/seller-content-query.resolver.tssrc/features/seller/resolvers/seller-conversation-mutation.resolver.tssrc/features/seller/resolvers/seller-conversation-query.resolver.tssrc/features/seller/resolvers/seller-order-mutation.resolver.tssrc/features/seller/resolvers/seller-order-query.resolver.tssrc/features/seller/resolvers/seller-product-mutation.resolver.tssrc/features/seller/resolvers/seller-product-query.resolver.tssrc/features/seller/resolvers/seller-mutation.resolver.tssrc/features/seller/resolvers/seller-query.resolver.tsImpact
Checklist
Breaking Changes
Summary by CodeRabbit
릴리스 노트