Repository navigation
Conversation
- CredentialLoginResult.expiresInSeconds(필수) — 판매자·관리자 로그인/refresh, USER /auth/refresh 응답 공통 - 값은 TokenService.getAccessExpiresSeconds() 한 곳(authConfig 단일 소스), dev/issue-token과 같은 이름·의미 - Swagger: 자격증명 응답 스키마·/auth/refresh ApiOkResponse에 필드 추가, 스냅샷 갱신(5 operation) - spec: login·refresh 결과 900, 컨트롤러 JSON 정확 일치 갱신, 반증 — jwtAccessExpiresSeconds 60으로 덮으면 60(상수 하드코딩 방지)
- SellerOrderSummary·AdminOrderSummary에 firstItemName·firstItemImageUrl(nullable) 추가. 값은 order_item의 product_name_snapshot·product_thumbnail_url_snapshot — 스키마·마이그레이션 변경 없음 - order.repository: sellerOrderSummaryInclude(storeId)로 내 매장 활성 첫 품목(id asc, take 1) 2컬럼만 include. listOrdersByStore·updateOrderStatusBySeller 반환에 적용(별도 재조회 없음). adminOrderInclude.items.select에 2컬럼 추가 - 매퍼·output type 반영 - spec: 스냅샷 유지(상품명·이미지 변경 뒤에도 유지), 다른 매장 품목 id가 작아도 내 매장 품목, soft-delete 품목 제외, 썸네일 null, 상태 변경 반환 필드, 사전 검사 뒤 품목 삭제 경합 시 null(nullable 계약), 관리자 목록·취소 반환, resolver 배선
feat: 로그인·재발급 응답에 expiresInSeconds 추가
- store_conversation에 seller_last_read_at·buyer_nickname_snapshot 추가. 백필은 활성 프로필(deleted_at IS NULL) 조인 + NULLIF만 — 탈퇴는 닉네임 덮어쓰기와 deleted_at을 함께 쓰므로 접두어 가드 불필요. 기존 대화 마커는 null(전부 미읽음) 시작 - sellerMarkConversationRead: 구매자 읽음과 같은 FOR UPDATE 잠금 아래 최신 메시지 created_at까지 단조 전진, activeWhere, updated_at 고정. 이벤트 미발행 - SellerConversation에 buyerNickname·lastMessagePreview·sellerLastReadAt·unreadCount, SellerConversationListUpdate에 buyerNickname·sellerLastReadAt·unreadCount - 판매자 목록은 getStoreConversationPageWithExtras(REPEATABLE READ)로 부가정보까지 단일 스냅샷. getConversationListExtras를 readAt·발신 조건으로 일반화해 구매자(not USER)·판매자(USER) 공유 - 판매자 답장은 seller_last_read_at을 함께 전진. fetchMonotonicNow GREATEST에 판매자 마커 추가 - 구매자 읽음(listBuyerMessagesAndMarkRead)도 updated_at 고정 — 읽기만으로 판매자 목록 정렬이 흔들리던 동작 수정 - countUnansweredConversationsByStore: 활성 대화 AND 미읽음 USER 메시지 EXISTS(마지막 발신자 기준 아님 — FAQ 자동응답이 같은 시각으로 저장됨) - 스냅샷은 대화 생성 시점만(soft-delete 재사용 포함 불변). index.ts에 ConversationRepository export, roles-coverage seller 48→49 - spec: repository(마커 전진/없음/멱등/후퇴 금지/updated_at 불변/soft-delete/잠금 레이스, 답장 마커, GREATEST 반증, 목록 extras, countUnanswered it.each 7건, 스냅샷 2건, 구매자 읽음 updated_at), 백필 마이그레이션 2건, seller·inquiry·center 서비스, resolver 1건
- 로그인에 `X-Client: mobile`(trim·lowercase)이면 Set-Cookie 대신 바디 `refreshToken`·`refreshExpiresAt`(ISO). 역할 게이트 `BODY_TRANSPORT_ROLES = {SELLER}` — ADMIN·USER는 헤더·바디 무시.
- refresh·logout은 바디 `refreshToken`(hex 64)이 있으면 바디 모드(쿠키는 읽지도 회전하지도 않음), 없으면 쿠키 모드. 바디 모드 logout은 clearCookie 생략.
- 신규 `refresh-transport.helper`(순수)·`RefreshTokenInput`. `TokenService.issueAuthTokens/rotateRefresh`가 transport로 Set-Cookie/반환값을 가르고, `readRefreshCookie`는 `readPresentedRefreshToken`으로 치환(auth.service 포함). 세션 생성·회전 DB 경로는 불변.
- 컨트롤러 `sellerRefresh/sellerLogout`에 `@Body() RefreshTokenInput`(ValidationPipe 통과용). Swagger: X-Client 헤더·선택 바디·응답 optional 필드, 스냅샷 갱신(판매자 3경로만).
- 테스트: 헬퍼 it.each 전수(역할×헤더, 역할×바디×쿠키), DTO, token/credential-auth 바디 모드·역할 게이트 반증, real DB 시나리오 2건, 컨트롤러 매핑. 기존 쿠키 spec은 무수정 통과.
…rker feat: 판매자 대화 읽음 마커·미읽음 수·구매자 닉네임 스냅샷
…ansport feat: 판매자 모바일용 refresh 토큰 바디 전달 경로
- dashboard feature에 sellerDashboard(input: { date? }) 쿼리. 날짜는 KST 달력일, 생략·null이면 오늘.
빈 문자열·형식 밖·존재하지 않는 날짜는 service가 parseKstDate로 INVALID_DATE(DTO @matches 없음).
- 지표: SUBMITTED 전체 건수, 기준일 픽업·생성 기준 건수·금액(CANCELED 제외), capacity(catalog 정본)·
예약 수량(BOOKED_QUANTITY_QUERY)·남은 수량(max(capacity−booked, 0)), 활성 상품 수(매장 노출 무관),
답변 필요 대화 수(countUnansweredConversationsByStore).
- OrderRepository.aggregateStoreOrdersInRange(basis: pickup|created), StoreSellerRepository.findStoreDailyCapacityByDate 추가.
- DashboardModule에 ConversationModule import. roles-coverage seller 49 → 50.
- spec: service(real DB) 19건 — KST 경계·빈 문자열·초과 수량 0 절단·매장 비활성 반증 포함, resolver 1건, DTO 6건, repo 신규 메서드 5건.
feat: 판매자 홈 집계 sellerDashboard 추가
앱 기동 시 토큰만으로 계정·매장 ID·비밀번호 상태를 한 번에 받는 쿼리.
- SDL auth-seller-account.graphql: sellerMe: SellerAccount! { accountId, username, displayName, storeId, mustChangePassword, accountStatus }
- displayName = account.name ?? seller_profile.business_name ?? null. 매장명은 sellerMyStore가 답한다
- 매장이 없거나 삭제됐으면 storeId null(STORE_NOT_FOUND 아님)
- 가드는 다른 seller* 필드와 같이 JwtAuthGuard + RolesGuard + @roles('SELLER'). 초기 비밀번호 상태는 PASSWORD_CHANGE_REQUIRED로 막히므로 앱은 refresh 응답의 mustChangePassword로 판단
- AccountAdminRepository.findSellerSelfById: 타입 조건 없는 좁은 select 1쿼리(SellerSelfRow). 서비스가 SELLER_ONLY·ACCOUNT_NOT_ACTIVE·SESSION_ACCOUNT_MISSING 판정
- read-boundary 허용 목록 추가 없음(같은 파일의 nested Account.store->Store 키 재사용)
- roles-coverage seller 개수 49 → 50
- spec: 서비스 real DB 12건, 리졸버 전체 경로 1건 + RolesGuard PASSWORD_CHANGE_REQUIRED 1건
rebase 뒤 develop SDL 기준으로 seller·admin 루트 필드 수 갱신.
feat: 판매자·관리자 주문 요약에 첫 품목명·썸네일 스냅샷 추가
- sellerSearchTags(input: { keyword, limit=10 ≤20 }): [SellerTagSuggestion!]! — SELLER 전용, 타이프어헤드라 커서 없음(adminCategories 선례)
- tag-name.helper normalizeTagName: trim → 선행 '#' 전부 제거 → 연속 공백 1칸 → 소문자 → NFC, 비면 null (it.each 전수표)
- repository searchTagsByName: 정확 일치 findFirst + contains 이름순 take limit 를 Promise.all 로, _count 는 활성 연결·노출 상품만
- 정확 일치는 DB collation(ci) 행 id 로 판정해 관리자 생성 'Cake' 도 'cake' 검색에 exact — 이름순 limit 밖이어도 맨 앞에 포함
- 길이 상한은 DTO 가 아니라 서비스 cleanRequiredText(80) — 이모지 코드포인트 기준 유지
- roles-coverage seller 접두 48 → 49
rebase 뒤 develop SDL 기준으로 seller·admin 루트 필드 수 갱신.
판매자 앱 푸시(후속 PR의 SellerPushOutboxConsumer)가 구독할 원천 이벤트 2종을 도메인 write와 같은 tx에 적재한다. - order.submitted: OrderRepository.insertSubmittedOrder 안에서 발행. aggregate는 order(orderId)라 order.status_changed와 같은 파티션 — 릴레이가 "접수 → 상태 전이" 순서를 지킨다. payload는 args 스냅샷(orderId·orderNumber·buyerAccountId·storeId·storeName·productId·productName·quantity·pickupAt·totalPrice)이라 추가 쿼리 없음. capacity 거절·P2002 롤백·멱등 replay는 이벤트 0건. NotificationOutboxConsumer는 구독하지 않는다(수정 0). - conversation.buyer_message_sent: ConversationRepository.createBuyerMessages 안에서 호출 1회당 1건(마지막 USER 메시지 기준, 인사말·FAQ 자동응답 제외). preview는 toLastMessagePreview 재사용 후 100자. 판매자 답장은 이벤트 없음. ConversationRepository에 OutboxPublisher 주입, ConversationModule이 OutboxModule import. - 배럴 export: order/index.ts(ORDER_SUBMITTED·parseOrderSubmittedPayload), conversation/index.ts(CONVERSATION_BUYER_MESSAGE_SENT·parseConversationBuyerMessageSentPayload). - spec: 파서 it.each 반증표 2개, order.repository(1건·capacity 0건·P2002 롤백 0건), order-checkout(1건 스냅샷·capacity 0건·replay 0건), conversation.repository(USER 1건·FAQ 쌍 1건·발행 실패 롤백·판매자 답장 0건), conversation-inquiry(텍스트·FAQ 칩 1건), topology(같은 event_type 두 소비자 → 큐 2개 각각 바인딩). conversation spec 8개에 outboxPublisherProviders 추가.
feat: 판매자 내 계정 조회 sellerMe 추가
feat: 판매자 태그 검색 sellerSearchTags 추가
- RateLimitGuard: 정책 배열(전부 INCR 뒤 판정), subject 'ip+username'(바디 username trim·lowercase → sha256 16자), code 지정 시 { minutes }로 DomainException. 메타데이터는 항상 배열 저장, 가드가 정규화
- /auth/seller/login·/auth/admin/login: 아이디+IP 5회/15분 + IP 30회/15분, LOGIN_RATE_LIMITED(429). Redis 장애 시 통과 유지
- Swagger ApiTooManyRequestsResponse + 스냅샷, 컨트롤러·스냅샷 spec은 overrideGuard로 통과
- spec: 가드 real Redis 14건(주체 분리·정규화·정책 2개·코드), HTTP 경로 6건(real 가드+Redis+supertest, 반증 포함), 컨트롤러 메타데이터, RENDER_PARAMS
rebase 뒤 develop SDL 기준으로 seller·admin 루트 필드 수 갱신.
feat: 푸시 원천 outbox 이벤트 order.submitted·conversation.buyer_message_sent
feat: 판매자·관리자 로그인 rate limit
판매자 앱(Expo)이 푸시를 받으려면 디바이스 토큰을 서버가 알아야 한다. 토큰 등록·해제 mutation과 전달 추적 테이블을 notification feature에 둔다(푸시는 알림 전달 채널). 전송 소비자는 후속 PR. - Prisma: enum PushPlatform·SellerPushDeliveryStatus, model SellerPushDevice(토큰 unique, 소유 계정은 마지막 등록자, client_device_id는 앱 식별자)·SellerPushDelivery(source_event_id×push_device_id unique, FK). deleted_at 없음 — 활성 판정은 disabled_at IS NULL. 마이그레이션 1개. - SDL: sellerRegisterPushToken(멱등 upsert by token — 다른 판매자 토큰은 소유권 교체, 해제됐던 토큰은 재활성, 형식 불량 INVALID_PUSH_TOKEN) / sellerUnregisterPushToken(본인 토큰만, 없거나 남의 것이면 true). 해제는 매장 컨텍스트 없이 계정 타입만 본다 — 매장 없는 판매자도 로그아웃 해제가 막히지 않게. - repository(upsertByToken·disableByTokenForAccount·listActiveByStore·disableByIds), service(등록은 SellerBaseService.requireSellerContext), resolver(RolesGuard SELLER), NotificationModule에 StoreModule. - error-catalog INVALID_PUSH_TOKEN(400), model-ownership 2건, roles-coverage seller 52, 팩토리 device·delivery. - spec: service real DB 17건(소유권 교체·재활성·남의 토큰 해제 불변 반증 포함), repository 7건(FK·unique 포함), DTO it.each 27건, resolver 1건.
Codex 리뷰 2건 반영. - seller_push_device.expo_push_token을 utf8mb4_bin으로. 테이블 기본 utf8mb4_unicode_ci를 상속하면 대소문자만 다른 토큰이 unique에서 같은 행으로 잡혀 두 번째 등록이 첫 행의 소유 계정만 바꿨다(토큰 값은 그대로 → 엉뚱한 기기로 발송). 아직 운영에 안 나간 마이그레이션이라 CREATE TABLE 안의 컬럼 정의를 고쳤고, schema.prisma에는 Prisma가 collation을 못 적으니 주석만 남겼다. 로컬 dev DB는 같은 ALTER를 직접 적용하고 _prisma_migrations 체크섬을 맞췄다. - SellerRegisterPushTokenInput의 @matches 제거. 전역 ValidationPipe가 먼저 걸어 VALIDATION_FAILED로 나가는 바람에 SDL이 약속한 INVALID_PUSH_TOKEN에 도달할 수 없었다. 문자열·길이는 DTO, Expo 형식은 서비스가 본다. - 테스트: 대소문자만 다른 토큰 2개 → 행 2개·각자 소유(수정 전 1행 합쳐짐 확인), DTO 정규식 표는 서비스 spec으로 이동(형식 위반 7건·통과 2건), resolver spec에 main.ts와 같은 ValidationPipe 옵션을 통과한 malformed 토큰이 INVALID_PUSH_TOKEN으로 끝나는 전체 경로 1건.
rebase 뒤 develop SDL 기준으로 seller·admin 루트 필드 수 갱신.
feat: 판매자 푸시 디바이스 토큰 등록·해제
- sellerSetProductTagsByName(input: { productId, names }): SellerProduct! — 이름 목록으로 연결을 통째로 교체, 없는 이름은 만들고 삭제행은 복구. 기존 sellerSetProductTags(tagIds)는 유지
- repository replaceProductTagsByName: writeWithAudit 한 tx 안에서 raw INSERT … ON DUPLICATE KEY UPDATE(updated_at 판정을 deleted_at 복구보다 앞에) → tag.findMany(name in) 결과를 그대로 tagIds로(collation ci가 동일성 판정) → 기존 본문을 replaceProductTagsInTx로 뽑아 공유
- 서비스: cleanRequiredText(80) → normalizeTagName(null이면 TEXT_REQUIRED) → dedupe → 20개 초과 PRODUCT_TAG_LIMIT_EXCEEDED(error-catalog + RENDER_PARAMS). DTO는 @isarray @IsString({each})만
- 감사는 상품 UPDATE 1건(afterJson.tagNames·tagIds), 태그 CREATE 감사 없음
- AdminTag·adminCreateTag description을 판매자 생성 태그 포함으로 갱신
- spec: 신규/재사용 tag.count 대조, 정규화·중복, 'Cake' 재사용(updated_at 불변), soft-delete 복구(updated_at 갱신), ['café','cafe'] 1개, 빈 배열 해제, 21개 400·중복 25개→20개 통과, '#'·공백·81자, 멱등, 두 판매자 동시 생성 1행, 같은 상품 동시 2회 현 동작 기록, 감사 1건, 감사 콜백 throw 시 전부 롤백
- roles-coverage seller 접두 51 → 53(현재 SDL 기준 재계산)
- sellerSetProductTagsByName: normalizeTagName 뒤 결과에 cleanRequiredText(80)을 한 번 더 적용. 'İ'처럼 소문자화로 코드 포인트가 늘면(80 → 160) 정규화 전 검사만으로는 VARCHAR(80) raw INSERT가 MySQL 1406으로 터졌다 — 이제 TEXT_TOO_LONG - sellerSearchTags는 이미 정규화 뒤 검사라 변경 없음 - SDL: 길이 규칙을 "80자(코드 포인트, 정규화 전후 모두)"로, 개수 규칙은 정규화 문자열 기준(악센트만 다른 café·cafe는 저장 시 하나로 합쳐져도 각각 센다)임을 mutation·names description에 명시(Codex 리뷰 기록용, 코드 변경 없음) - spec: ['İ'×80] → TEXT_TOO_LONG, tag 행 0 케이스 추가(재검사를 빼면 PrismaClientKnownRequestError 1406으로 실패하는 것 확인)
- replaceProductTagsByName: 다중 행 INSERT ... ON DUPLICATE KEY UPDATE의 VALUES를 조립하기 전에 이름을 코드 포인트 순으로 정렬. 두 동시 mutation이 같은 기존 태그들을 반대 순서(['birthday','cake'] vs ['cake','birthday'])로 보내면 unique(name) 레코드 잠금 순서가 엇갈려 InnoDB 교착이 날 수 있었다 — 서비스가 이미 dedupe한 배열이라 repository는 정렬만 - spec: 같은 기존 태그 2개를 반대 순서로 두 판매자가 동시에 보내는 케이스 추가(둘 다 성공, tag 행 2개 유지, 활성 연결 각 2건). 교착은 비결정적이라 회귀 보호용
rebase 뒤 develop SDL 기준으로 seller·admin 루트 필드 수 갱신.
판매자 앱이 주문 목록·배지를 실시간 갱신하도록 매장 단위 토픽 구독을 연다(P13).
- order-subscription.graphql: Subscription.sellerOrderUpdated → SellerOrderUpdate { orderId, orderNumber, status, pickupAt, buyerName, totalPrice, productName, updatedAt }. 폐기 규칙(같은 orderId의 더 오래된 updatedAt 폐기)·유실 폴백(sellerOrderList 재조회)·connectionParams 인증을 description에 명시
- OrderEventsService: 토픽 order.seller.<storeId> 단일 소스, Redis PubSub 직접 발행, 실패는 warn 로그로 삼킴(대화 구독과 동일)
- OrderSubscriptionService/Resolver: 매장 없음·삭제 매장은 STORE_NOT_FOUND, is_active=false는 구독 유지. JwtAuthGuard + RolesGuard + @roles('SELLER')
- 발행 3곳: 체크아웃은 createWithOrderNumberRetry 루프의 return created 직전만(멱등 키 사전 조회·capacity replay·P2002 replay 제외), 판매자 상태 변경·관리자 취소는 커밋 뒤. 관리자 취소는 AdminOrderRow.items[0] 매장 토픽, 활성 품목 없으면 생략
- OrderRepository: insertSubmittedOrder·findOrderByIdempotencyKey select에 buyer_name·updated_at 추가(CreatedOrderRow 확장). 판매자 상태 변경의 productName은 sellerOrderSummaryInclude의 items[0].product_name_snapshot 재사용(추가 쿼리 0)
- roles-coverage seller 50 → 51(Subscription 포함)
테스트: order-events.service.spec(real Redis 왕복·타 매장 미수신·발행 실패 삼킴), order-subscription.service.spec(real DB), checkout/seller/admin service spec에 발행 1건·replay 3경로 0건·capacity 거절 0건·PubSub 실패 시 주문 성공·전이 거절 0건·취소 불가 0건·품목 없음 생략. 가드를 지운 변이본으로 6개 suite 실패 확인. resolver spec 3개는 provider만 보강.
rebase 뒤 develop SDL 기준으로 seller·admin 루트 필드 수 갱신.
같은 상품에 서로 다른 태그 집합이 동시에 들어오면 updateMany→findMany→createMany가 교차해 합집합이 남거나 한쪽이 실패한다(잠금 없이 돌리면 createMany에서 교착으로 한쪽 실패 재현). - replaceProductTags·replaceProductTagsByName 트랜잭션 첫 단계에서 product 행을 FOR UPDATE로 잠가 같은 상품 교체를 직렬화(마지막 쓰기가 완전 교체로 남음) - 잠금은 태그 upsert보다 앞 — upsert 뒤에 두면 tagIds 경로의 FK 공유 잠금과 순서가 엇갈려 교착(1213) 재현 - 회귀 2건: 서로 다른 집합 동시 호출(최종 연결이 한 집합과 정확히 일치), tagIds·이름 경로 혼합 동시 호출
…iption feat: 판매자 주문 갱신 구독 sellerOrderUpdated 추가
feat: 판매자 이름 기반 상품 태그 설정 sellerSetProductTagsByName 추가
판매자 앱에 새 주문·새 문의 푸시를 보낸다. #507의 outbox 이벤트를 #510의 디바이스로 전달하는 마지막 조각.
- expo-push.config: EXPO_PUSH_ENABLED(기본 false)·ACCESS_TOKEN·TIMEOUT_MS(기본 5000). 비표준 값은 기본값, 부팅 실패 조건 없음.
app.module load 등록, README 환경 변수 표·infra/app.env.example에 키 3개.
- src/global/expo-push: EXPO_PUSH_TRANSPORT 토큰·ExpoPushTransport 계약·fetch 기본 구현. send ≤100·getReceipts ≤1000(넘기면
throw), withTimeout, 비 2xx는 상태 코드를 담은 ExpoPushHttpError, 401/403은 ExpoPushAuthError, 200+errors·ticket 수
불일치도 거절. kakao-local과 같이 모듈 없이 NotificationModule.providers에 useValue로 등록.
- SellerPushOutboxConsumer(@SubscribeOutbox order.submitted·conversation.buyer_message_sent, worker): enabled=false면 debug
로그 후 ack(이력 없음), payload 파싱 실패 throw, 매장 활성 디바이스 → claim(createFromEvent 방식 — 기존 행 제외 후
createMany, skipDuplicates 없음, unique가 최종 방어, PENDING만 전송) → 100개 배치 send → TICKET_OK(ticket_id·sent_at) /
TICKET_ERROR(error_code), DeviceNotRegistered는 즉시 디바이스 비활성. 전송 예외는 던져 호스트 retry/DLQ, 인증 실패는
expo-push:auth 경보 뒤 throw. 메트릭 caquick_expo_push_sends_total{result}.
- SellerPushReceiptScheduler(5분, worker 역할 가드·enabled 가드·동시 실행 플래그): 15분 지난 TICKET_OK 최대 300건 →
RECEIPT_OK / RECEIPT_ERROR(+DeviceNotRegistered 비활성), 24h 넘게 영수증 없으면 RECEIPT_UNKNOWN, 그 전엔 유지. 실패는 warn +
expo-push:receipts(인증 실패는 expo-push:auth), 행은 그대로.
- SellerPushDeliveryRepository(claim·markTickets·listForReceipt·markReceipts), seller-push-messages.helper(주문 "새 주문" /
"{상품} {n}개 · 픽업 M/d HH:mm"(KST), 문의 "새 문의" / preview, data kind·id, channelId default).
- order·conversation 배럴에 payload 타입 export.
- spec: config 19, transport 20, helper 6, delivery repo 9(FK 오류 전파 반증), consumer 14(재전달 send 0회·150→100+50·
배치 중간 실패 뒤 남은 50만 재전송·401 경보·enabled=false·payload 오류·구독 외 throw), scheduler 10(15분 미만 미조회·
24h UNKNOWN·api/ws 미실행·동시 실행 건너뜀), metrics 지표 표 1행. 가드 5개(PENDING 필터·enabled·running·15분·100개 상한)를
지우면 기대한 7건만 실패하는 것 확인.
Codex 2라운드 리뷰 반영. - 전송 어댑터: withTimeout이 fetch를 취소하지 않아 기한 초과 뒤에도 원 POST가 완료돼 재시도와 겹치면 중복 발송될 수 있고, 헤더 도착 뒤 response.json()은 기한 밖이라 무한 대기가 가능했다. AbortController를 fetch에 넘기고 기한이 바디(JSON) 읽기까지 덮게 한 뒤 TimeoutError면 abort한다. 던지는 에러는 기존과 같은 TimeoutError(fetch가 AbortError로 끝나도 호출자는 TimeoutError). - 소비자·영수증 스케줄러: markTickets/markReceipts 뒤에 disableByIds를 하던 순서를 뒤집는다. 전달 행을 종료 상태로 바꾼 뒤 죽으면 재시도·다음 틱이 그 행을 다시 보지 않아 DeviceNotRegistered 디바이스가 영영 비활성되지 않았다. disableByIds는 멱등(disabled_at IS NULL 조건)이라 먼저 해도 안전하다. - 회귀 테스트 5건: 전송 3(기한 초과 시 signal abort·AbortError여도 TimeoutError, 바디 읽기 지연도 기한에 걸림, 제때 끝나면 abort 안 함), 소비자 1(markTickets가 던져도 디바이스는 이미 비활성·행은 PENDING), 스케줄러 1(markReceipts가 던져도 디바이스는 이미 비활성·행은 다음 틱 대상). 운영 코드를 HEAD로 되돌리면 이 5건만 실패하는 것을 확인.
rebase 뒤 develop SDL 기준으로 seller·admin 루트 필드 수 갱신.
feat: 판매자 푸시 발송 소비자(Expo Push Service)·영수증 스케줄러
|
Important Review skippedToo many files! This PR contains 147 files, which is 47 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configuration
⛔ Files ignored due to path filters (1)
📒 Files selected for processing (147)
You can disable this status message by setting the
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🧹 knip — dead-code 리포트전체 리포트
|
🩺 NestJS Doctor — 90/100 (Excellent)진단 484건 (error 12).
architecture / security 상위 항목
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Coverage report
Show new covered files 🐣
Show files with reduced coverage 🔻
Test suite run success4236 tests passing in 383 suites. Report generated by 🧪jest coverage report action from 918cb6f |
판매자 앱(caquick-seller-fe) 착수 플랜의 BE 선행 변경 13건을 운영에 반영합니다.
expiresInSecondsX-Client: mobile)sellerMeLOGIN_RATE_LIMITED)sellerSearchTagssellerSetProductTagsByName(상품 행 잠금으로 교체 직렬화)sellerOrderUpdated구독sellerDashboardorder.submitted·conversation.buyer_message_sentexpo_push_tokenutf8mb4_bin)운영 반영 사항입니다.
production시크릿APP_ENV에EXPO_PUSH_ENABLED=true를 추가했습니다(이 PR 머지 전 반영 완료).store_conversation컬럼 2개 + 백필,seller_push_device·seller_push_delivery)이 배포 잡의migrate로 적용됩니다.