MD 개발 · 그룹5 · 운영·노출 · MD-26 · PLD-1034 · 개발 기획 (정정판)

장바구니 품절·미노출 대응 (사장님 앱)

장바구니에 담긴 상품이 품절 / 장기품절 / 미노출로 바뀌었을 때 배지로 알리고, 같은 그룹의 대체 상품을 추천하고, 주문 직전에 "빠지는 상품"을 확인시킨다. 강제 교체는 하지 않는다. · rev 2 · 2026-09-03

이 판은 실측 정정판이다. 초안(rev 1)의 미노출 판정 컬럼이 틀렸고(is_original_displayed → use_yn), 앱 현황을 QA 번들 코드로 다시 확인했으며, 개발DB 행 수 인용은 전부 지웠다(구조만 남김). 바뀐 내용은 맨 아래 「변경 이력」에 있다.
실측 코드·스키마·화면에서 직접 확인   추론·미정 확인 못 했거나 결정이 필요한 것   의존 다른 결정에 묶인 것
한눈 결론무엇을, 왜, 어디까지선행 없음
범위
사장님 앱 장바구니 화면 + [주문하기] 직후. 상품 상세·검색·푸시 사전통지는 범위 밖.
지금 문제
앱은 품절 상품에 Sold out 오버레이는 띄우지만, [주문하기]를 누르면 그 상품을 말없이 빼고 주문한다(코드 확인). 장기품절·미노출은 API에 필드가 없어 구분 자체가 불가. 대체 추천 없음.
판정 축 (정정)
품절 sold_out · 장기품절 long_term_sold_out · 미노출 use_yn ≠ 'Y' 실측 — is_original_displayed는 복제 원본 노출, open_yn은 특정 업장 노출 범위라 둘 다 아님.
추천
같은 group_id의 판매 가능 히어로를 가격 오름차순 상위 3건. 그룹 없으면 카테고리 폴백 🟠. 「담기」는 추가만, 원본 자동 삭제·강제 교체 금지.
백엔드 작업
① 장바구니 조회 응답에 longTermSoldOut·useYn(또는 상태 enum) 필드 2개 추가 — soldOut·groupId는 이미 내려옴 ② 대체 추천 API(group_id 기준 top3) ③ (선택) 노출·전환 로그 member_cart_alert 신설.
프론트 작업
배지 3종 + 상단 요약 밴드 + 대체 추천 카드 + [주문하기] 시 제외 확인 바텀시트. 재사용 후보: 주문 전 인터스티셜(OrderRecommendView)의 상품 카드·담기 패턴.
미정 🟠 4
반영 시점(실시간/배치) · 미노출 결제 허용 · 중복 억제 단위 · 카테고리 폴백 범위 → 06
01개요 · 현황QA 앱 번들 청크 + 스크린샷 2026-09-03
QA 사장님 앱 장바구니 화면(2026-09-03)
QA 장바구니 실화면. 담긴 2건 모두 정상 상태라 품절 UI는 이 캡처에 없음(프로모션 팝업이 덮인 채 캡처). 하단은 무료배송 바 + [주문하기].

사장님은 여러 날에 걸쳐 장바구니를 유지한다. 그 사이 공급사·어드민이 상품을 품절 처리하거나(sold_out), 장기 품절로 돌리거나(long_term_sold_out), 노출을 내리면(use_yn) 담긴 상품을 그대로 주문할 수 없다.

현재 앱이 실제로 하는 일 실측

장바구니 스토어는 주문 페이로드를 만들 때 품절 항목을 건너뛴다. 화면(CartView)도 주문 요청 직전에 같은 필터를 한 번 더 건다. 어느 쪽도 "N건이 빠졌다"는 안내를 하지 않는다.

// Cart 스토어 — 주문 페이로드 생성 computed
for(let e of t.cartProductList){
  if(e.soldOut||!f.value.some(t=>t.heroCode===e.heroCode))continue;
  …
  a={heroCode:e.heroCode,groupId:e.groupId??null,name:e.name, … ,parentHeroCode:e.parentHeroCode??null,issue:e.issue??null}

// CartView — [주문하기] 요청 본문
e.cartProductList.filter(e=>W.value.some(t=>t.heroCode===e.heroCode)&&!e.soldOut)

Cart.E6ostIJO.js · CartView.B4gMTDcs.js (QA 번들, 2026-09-03)

이미 있는 것 실측

  • 장바구니 API 아이템에 soldOut·groupId·sellerId·category1/2·orderLimit·totalAvailableQuantity 존재
  • 품절 항목: Sold out 텍스트 오버레이(영문) + 삭제(X) 버튼 + 재입고 알림 토글(ProductRestockAlert, "알림 신청은 30일간 유지돼요")
  • 품절 항목: 체크박스 pointer-events-none, 수량 스테퍼 invisible, 합계·배송비 계산에서 제외
  • 상품 상세: 품절 시 CTA 문구 "품절된 상품입니다."
  • [주문하기] 직후 주문 전 추천 인터스티셜 OrderRecommendView("잊으신 식자재는 없나요?") — GET /recommend/order/product?heroProductCodes=가 상품을 돌려주면 진입, 「담기」·「N원 담고 같이 결제하기」·「주문서 작성하기」
  • 서버 검증 POST /cart/validate가 주문서 진입 전에 이미 호출됨

없는 것 = 이번 작업

  • [주문하기] 시 "품절 N건 제외됨" 안내 — 현재는 말없이 제외
  • 장기품절·미노출 구분 — 응답에 longTermSoldOut·useYn 없음. 미노출 상품이 장바구니에 남아 있으면 앱은 정상처럼 보여준다
  • 한국어 상태 배지("품절"/"장기품절"/"판매 종료")와 상단 요약 밴드
  • 대체 상품 추천 — 장바구니 화면 안에는 추천 UI 없음(추천은 주문 전 인터스티셜에만)
  • 노출·추천·전환 로그(어드민 성과 추적)
초안·티켓 문구 정정 2건. ① "배지·안내 없음"은 부정확 — 품절 오버레이·재입고 알림은 이미 있다. 정확한 문제는 "화면엔 보이는데 주문에서 말없이 빠진다"와 "장기품절·미노출은 아예 구분이 안 된다". ② "장바구니 하단 추천 레일 기존 존재"는 이 번들의 CartView 청크에서 확인되지 않았다(레일 컴포넌트 import 없음). 확인된 추천 UI는 [주문하기] 직후 인터스티셜뿐 — 다른 청크에 레일이 있을 가능성은 남긴다 확인 못 함.
02정의 · 정책seller_product 기준 판정 · 사용자 동작 규칙

상태 3종 판정 조건 (정정표)

상태판정 (seller_product)장바구니 API 필드배지사용자 동작
품절sold_out = true 실측soldOut · 기존품절선택·수량 비활성(기존 유지) + 대체 추천(신규) + 주문 시 제외 안내(신규). 재입고 알림은 기존 그대로.
장기품절long_term_sold_out = true 실측longTermSoldOut · 추가장기품절 · 당분간 입고 없음품절과 동일 + 대체 추천을 우선 노출(재입고 기대 낮음).
미노출use_yn ≠ 'Y' — 어드민 상품관리의 "노출 여부"가 이 컬럼 실측useYn(또는 상태 enum) · 추가판매 종료배지 + 추천. 결제 허용 여부 🟠(경고 후 허용 권고).
정상위 조건 모두 아님——평소대로.
use_yn 값 도메인은 Y / N / U이고 기본값은 'U'다(스키마 실측; 개발DB에서 관측된 값, 행 수 비인용). "≠ 'Y'"로 판정하면 N과 U가 모두 미노출로 잡힌다. U의 의미(미등록·대기 상태로 추정)는 확인 못 했다 — 장바구니에 U 상품이 담길 수 있는 경로가 있는지 개발에서 한 번 확인. 확인 필요

아닌 컬럼 — 초안이 헷갈린 것

컬럼실제 의미왜 아닌가
is_original_displayed bool, NULL 허용, 기본 true복제 상품(parent_hero_code)이 있을 때 원본을 노출할지복제 관계가 없는 상품엔 의미가 없다. 초안의 "NULL 판정 미정(U-2)"은 질문 자체가 사라짐.
open_yn varchar(1), 기본 'Y'특정 업장(mscd 전용상품) 노출 범위 — mscd_yn/mscd와 세트전체 노출 여부가 아니라 "누구에게" 보이느냐다.

컬럼 존재·타입·기본값 = app-db 스키마 실측 / 의미 = 스테이지 어드민 화면 대조(상위 세션, 2026-09-03)

배지 우선순위 (한 상품에 여러 상태가 겹칠 때)

품절 > 장기품절 > 미노출 — 권고 권고. 대표 배지 하나만 보이고 나머지는 카드 안 보조 문구. 대안: 장기품절을 대표로 올리는 안(정보량은 많지만 "지금 못 산다"는 신호는 품절이 더 직관적) — 결정되면 프로토 1줄 수정.

대체 추천 규칙

  • 1순위: 같은 group_id의 판매 가능 히어로(use_yn='Y' ∧ sold_out=false ∧ long_term_sold_out=false)를 가격 오름차순 상위 3건. 자기 자신 제외.
  • 2순위 폴백 🟠: group_id가 NULL이거나 그룹 안에 대체재가 0건이면 카테고리 기준(category1/2/3 중 어느 레벨까지 — 미정 U-4).
  • 추천 없음: 둘 다 없으면 배지만 노출, 추천 카드 생략. (선택) 로그에 recommended_codes=[]로 남겨 그룹핑 품질 피드백.
  • 「담기」는 추가만: 기존 POST /cart/add. 원본 품절 상품은 자동 삭제하지 않는다.
불가침 — 자동 강제 교체 금지. 사장님 발주 금액·수량이 그대로 매입/정산으로 간다. 시스템이 상품을 바꿔 담으면 오발주가 된다. 교체는 반드시 사용자 행동(대체 담기 + 원본 삭제)으로만.

결제 전 바텀시트

  • [주문하기] 시 선택된 항목 중 품절·장기품절 N건 → "제외하고 주문", 미노출 N건 → 확인 후 포함 여부 🟠. 버튼: [제외하고 주문] [돌아가기].
  • 판정에 필요한 데이터(선택 여부·soldOut·추가 필드 2개)는 이미 클라이언트에 있다 → 시트는 서버 왕복 없이 띄울 수 있고, 기존 POST /cart/validate는 그 뒤에 그대로 탄다 추론(코드 흐름 기반).
  • 원본은 장바구니에 남긴다(재입고 알림 → 재담기 경로 유지).
03화면 흐름보라색 = 신규 단계
1장바구니 진입
GET /cart
→
2아이템별 상태 판정
(응답 필드로)
→
3배지 + 상단 요약 밴드
"담을 수 없는 상품 N개"
→
4대체 추천 카드
(group → category)
→
5[주문하기] → 제외 확인
바텀시트
→
6기존 흐름
추천 인터스티셜 → validate → 주문서

요약 밴드를 탭하면 해당 카드로 스크롤. 추천 카드의 「담기」는 즉시 /cart/add, 원본은 그대로. 단계 5의 시트는 단계 6의 기존 [주문하기] 핸들러(fi) 앞에 끼워 넣는 위치다.

md-g5-26-app — 클릭 가능한 프로토타입 (탭 ① 앱 장바구니 · ② 결제 전 차단 시트 · ③ 어드민 대응 로그)

프로토는 기획 산출물이며 실제 앱 토큰이 아니다. 우측 설명 패널의 번호 핀이 개발 항목. 「대체 담기」·「주문하기」는 동작한다. 별도 창: md-g5-26-app.pages.dev ↗

04데이터 모델app-db 스키마 실측 · 구조만, 행 수 없음

기존 member_cart — 장바구니

컬럼타입비고
member_id PKvarchar(20) NNFK → member.id
hero_product_code PKvarchar(100) NNFK → seller_product.hero_product_code ← 판정 조인 키(FK가 실제로 걸려 있음)
product_qtyinteger NN수량
product_option_jsonjsonb옵션
requesttext상품 요청사항
selected_delivery_datedate희망 납기
created_attimestamp NNKST 기본값

기존 seller_product — 판정 소스 (PK hero_product_code)

컬럼타입역할
sold_outboolean NN, 기본 false품절 판정
long_term_sold_outboolean NN, 기본 false장기품절 판정
use_ynvarchar(1) NN, 기본 'U', 인덱스 있음미노출 판정 — ≠ 'Y'. 값 도메인 Y/N/U
group_idinteger NULL, 인덱스 있음추천 1순위 축 — NULL이면 폴백
category1 · category2 · category3varchar(10) NULL폴백 축(레벨 🟠). 초안 U-7 "연결 컬럼 미확인"은 해소 — 코드 컬럼이 상품 행에 직접 있다
price · net_unit_priceinteger NULL · bigint NULL추천 정렬. 어느 컬럼을 "가격"으로 볼지는 개발 시 확정(net_unit_price는 NULL 허용)
parent_hero_code · is_original_displayed · open_yn · mscd_yn/mscd—이번 판정에 쓰지 않음(02 참조)

기존 seller_product_sold_out_log — 품절 상태 변경 이력 (재사용)

컬럼: id · hero_product_code · main_product_name · group_id(NN) · seller_id · change_type · sold_out · created_by(기본 'auto') · created_at. 인덱스는 created_at뿐, FK 없음.

change_type실측(값 도메인)해석
OUT_OF_STOCKcreated_by는 항상 'auto', sold_out은 true만 실측재고 소진 자동 품절(해제 방향 기록 없음 — 해제는 다른 타입으로 남는 듯)
MANUAL'auto' · 공급사 자신 id · 그 외 계정 id 모두 존재, true/false 양방향수동 품절/해제. "어드민" 귀속은 추론 — created_by가 어드민 계정인지 공급사 계정인지 값만으로는 못 가른다
ORDER_LIMIT'auto' · 그 외 계정 id, true/false 양방향(seller_id와 같은 created_by는 없음)수량제한(order_limit) 소진/해제. "공급사 계정" 귀속은 추론

재사용 방식: 배치안(U-1)일 때 "마지막 배치 이후 품절 상태가 바뀐 히어로" 감지 소스로 쓸 수 있다(created_at 인덱스 활용). 단 미노출(use_yn) 변경은 이 로그에 남지 않는다 — 배치안을 고르면 use_yn 변경 감지를 따로 설계해야 한다. 실시간안이면 이 로그는 어드민 "대응 로그" 화면의 원천 표시(왜 품절됐나)로만 쓴다 추론.

신규·선택 member_cart_alert — 노출·추천·전환 로그

현재 스키마에 없다(member_cart% 테이블은 member_cart뿐). 배지·추천의 성과 추적과 중복 억제에만 필요하므로 1차 출시에서 빼도 기능은 성립한다.

컬럼(제안)타입용도
idbigint PK—
member_idvarchar(20)회원
hero_product_codevarchar(100)대상 상품
statevarchar(20)SOLD_OUT / LONG_TERM_SOLD_OUT / HIDDEN
recommended_codesjsonb노출한 추천 히어로 코드 배열(빈 배열 = 추천 없음)
shown_attimestamp배지·추천 노출 시각
converted_attimestamp NULL추천 「담기」 전환 시각
dedup_keyvarchar, UNIQUE중복 억제 단위 🟠(U-3) — 예: member_id:hero:yyyymmdd
05API · 백엔드기존 엔드포인트 위에 필드·API 추가

① 장바구니 조회 응답 — 필드 2개 추가

기존 GET /cart(목록) · GET /cart/data?heroCodes=(선택 항목 재계산, 진입 시 항상 재조회)의 cartProductList[] 아이템에:

필드상태원천
soldOut기존 실측seller_product.sold_out
groupId기존 실측seller_product.group_id (앱은 ?? null로 받음)
longTermSoldOut추가seller_product.long_term_sold_out
useYn추가seller_product.use_yn 원값(Y/N/U)
대안: 서버가 판정해 cartState: AVAILABLE | SOLD_OUT | LONG_TERM_SOLD_OUT | HIDDEN enum 하나로 내려주기 — 우선순위 규칙이 서버 한 곳에 모이는 장점. 둘 중 하나로 확정.

기존 soldOut 분기(오버레이·비활성·페이로드 제외)는 그대로 두고, 새 필드로 배지 문구와 시트 분류만 나눈다. 장기품절은 sold_out과 별개 컬럼이므로 "장기품절인데 soldOut=false"인 조합이 데이터상 가능하다 — 그 경우도 주문 불가로 다뤄야 하면 프론트 필터를 soldOut || longTermSoldOut으로 넓힌다 추론·확인 필요.

② 대체 추천 API (신규)

예시: GET /recommend/cart/substitute?heroCodes=A,B,C → 히어로별 [{heroCode, name, price, seller, arrivalDate, …}] 최대 3건. 판정식은 02의 추천 규칙 그대로(같은 group_id ∧ 판매 가능 ∧ 자기 제외 ∧ 가격 오름차순 ∧ LIMIT 3). 응답 카드 필드는 기존 인터스티셜(/recommend/order/product)의 상품 카드 스키마를 그대로 쓰면 프론트 재사용이 쉽다 권고. 장바구니 진입 시 문제 상품이 있을 때만 호출(0건이면 호출 안 함).

③ 반영 시점 🟠 U-1

실시간 (권고)

장바구니 조회가 이미 seller_product를 조인해 soldOut을 내려주므로 같은 쿼리에 컬럼 2개 얹는 것. 추가 부하는 사실상 없음. 최신성 문제 없음.

배치

변경분만 미리 계산해 캐시. 품절 변경은 seller_product_sold_out_log로 감지 가능하지만 미노출 변경은 로그가 없어 별도 감지 필요. 이 티켓 규모에선 과설계.

④ 결제 전 검증 훅

[주문하기] 핸들러 순서: 시트(신규) → /recommend/order/product → (인터스티셜) → POST /cart/validate → 주문서. 시트는 클라이언트 데이터만으로 뜨고, 서버 validate가 미노출·품절을 이중으로 막을지는 U-2(미노출 결제 허용)에 따라 정한다.

06🟠 미정 4프로토는 아래 「가정」으로 만들었다 · 뒤집히면 무엇이 달라지는지 함께 적음

U-1 반영 시점 — 장바구니 진입 시 실시간 vs 배치 사전 계산

권고 실시간: 기존 조회 쿼리에 컬럼 2개만 추가. 배치는 use_yn 변경 감지 설계가 따로 붙는다.

▶ 프로토 가정: 진입 시 실시간. 배치로 뒤집히면 → 캐시 테이블 + sold_out_log 폴링 + use_yn 변경 감지 3가지가 추가된다.

U-2 미노출 상품의 결제 허용 여부

미노출은 "앱에서 내렸지만 상품 행은 살아 있음". 이미 담긴 건은 경고 후 허용할지, 품절처럼 제외할지.

▶ 프로토 가정: 경고 후 허용(시트에서 포함/제외 선택). 차단으로 뒤집히면 → 시트 분기 단순화 + 프론트 페이로드 필터에 useYn!=='Y' 추가 + 서버 validate에도 동일 규칙.

U-3 중복 알림 억제 단위 — member_cart_alert.dedup_key

같은 품절 상품에 배지·추천을 얼마나 자주 "새로" 셀지: 회원×상품×일자(권고) / 세션당 1회 / 회원×상품 1회.

▶ 프로토 가정: 회원×상품×일자 1회. 배지 자체는 항상 보이고, 억제 단위는 로그·성과 집계에만 영향 → 뒤집혀도 화면은 안 바뀐다.

U-4 카테고리 폴백 범위 — 그룹에 대체재가 없을 때

같은 category3(소분류)까지만 vs category2(중분류)까지. 좁을수록 정확하고 넓을수록 "추천 없음"이 줄어든다.

▶ 프로토 가정: 소분류(category3) 동일. 중분류로 뒤집히면 → 추천 쿼리의 WHERE 한 줄 + 카드에 "비슷한 분류" 라벨.

초안의 미정 7 + MD-15 의존은 4로 줄었다. U-2(is_original_displayed NULL 판정)·U-7(카테고리 연결 컬럼)은 실측으로 소멸, U-5(배지 우선순위)·U-6(추천 tie-break)은 권고로 흡수. MD-15 의존 해제: 현행 group_id로 바로 개발하고, 그룹3에서 그룹 정의가 바뀌면 추천 결과만 따라간다(추천 로직은 group_id 값만 읽는다).
07산출물 · 근거
구분항목위치
프로토클릭 프로토타입 v2 (탭 ①②③)md-g5-26-app.pages.dev
문서이 문서 (개발 기획 정정판)md-g5-26-doc.pages.dev
지라PLD-1034 [MD-26] [그룹5] 장바구니 품절·미노출 대응 (앱) — 수정안docs/TICKET_MD26_PLD-1034_수정안.md
근거 · 앱QA 번들 청크(읽기 전용)Cart.E6ostIJO.js · CartView.B4gMTDcs.js · OrderRecommendView.CMDmInYs.js · ProductRestockAlert.Cj0XF_N5.js · ProductDetailsView.Bt2VIwnk.js
근거 · DBapp-db 스키마(테이블·컬럼·타입·기본값·인덱스·FK · 값 도메인)seller_product · member_cart · seller_product_sold_out_log · member_cart_alert 부재 확인
근거 · 화면QA 장바구니 스크린샷 / 스테이지 어드민 "노출 여부" 대조qa-cart.png(01에 삽입) / 상위 세션 실측
08변경 이력
rev · 일자변경
rev 1 · 2026-08-21초안. 스펙 기반 정책·데이터 모델·미정 7 + MD-15 의존.
rev 2 · 2026-09-03
  • 미노출 판정 정정: is_original_displayed=false → use_yn ≠ 'Y'(어드민 "노출 여부"). open_yn·is_original_displayed는 각각 특정 업장 노출 범위·복제 원본 노출이라 제외.
  • 앱 현황 실측: 장바구니 API에 soldOut·groupId 이미 존재, 주문 생성 시 if(e.soldOut…)continue로 말없이 제외. "배지·안내 없음" → "품절 오버레이·재입고 알림은 있고, 제외 안내·장기품절/미노출 구분·대체 추천이 없음"으로 정밀화.
  • 백엔드 작업 명시: 응답 필드 longTermSoldOut·useYn 추가 + 추천 API. 추천 재사용 후보를 "장바구니 하단 레일" → "주문 전 인터스티셜 OrderRecommendView"로 정정.
  • 품절 원천 로그: seller_product_sold_out_log.change_type 3종 값 도메인 실측(OUT_OF_STOCK=auto·품절 방향만), 귀속 해석은 추론으로 표시. 미노출 변경은 로그 없음 명시.
  • 개발DB 행 수 인용 전부 삭제(구조만). 카테고리 연결 컬럼(category1/2/3) 실측으로 U-7 소멸.
  • 배지 우선순위 권고 변경: 장기품절 > 미노출 > 품절 → 품절 > 장기품절 > 미노출. 미정 7+의존 → 4, MD-15 의존 해제. 스타일을 하우스 §1로 통일.
MD-26 · PLD-1034 · 그룹5 운영·노출 · rev 2 · 2026-09-03
실측 = QA 앱 번들 청크 · app-db 스키마(개발DB, 구조·값 도메인만) · 스테이지 어드민 대조 · 개발DB 행 수는 인용하지 않음