이용 가능한 모든 상품 옵션 찾기

MongoDBBeginner
지금 연습하기

소개

RIDGE & REED는 아웃도어 의류를 판매합니다. 고객은 재고가 있고 가격이 5000센트 이하인 초록색 M 사이즈 상품을 원합니다. 한 상품에 초록색 옵션과 재고가 있는 M 사이즈 옵션이 각각 있어도, 구매 가능한 초록색 M 사이즈 옵션이 있는 것은 아닐 수 있습니다. 올바른 상품을 선택한 뒤에도 전체 옵션 배열이나 품절된 최저가 옵션을 표시하면 고객을 오도하게 됩니다.

재사용 가능한 상품 선택 함수를 저장한 다음, 해당하는 모든 SKU를 반환하고 바로 그 옵션들에서 최저가를 계산하는 MongoDB 집계 파이프라인을 만듭니다. 기본적인 터미널 탐색, MongoDB 연결, 객체/배열, 기본 find/update 지식이 필요합니다. JavaScript 함수 템플릿, 집계 표현식과 단계는 사용하기 전에 설명합니다. 전체 과정에서 Terminal을 사용하세요. MongoDB, 합성 재고, 읽기 전용 클라이언트는 준비되어 있습니다.

selector.js와 finder.js만 변경하세요. 원래 상품, 옵션 ID, 속성, 재고, 가격과 제공된 도구를 모두 보존하세요. 가용 여부는 준비된 재고 수량이며 예약이나 구매 완료를 보장하지 않습니다. 실제 옵션마다 텍스트 SKU/색상/사이즈와 음이 아닌 정수 재고/센트 가격이 있습니다. 상품의 옵션은 배열, null 또는 누락된 값일 수 있습니다. 그 밖의 잘못된 입력 형식은 이 실습의 범위 밖입니다.

모든 조건을 하나의 옵션에 적용하기

이 단계에서는 고객 요청을 실제 상품과 비교하고, 서로 다른 옵션을 조합한 잘못된 일치를 거부하는 재사용 가능한 선택 함수를 저장합니다. Terminal에서 시작하세요. cd는 디렉터리를 변경합니다. 잘못된 디렉터리에 들어갔다면 이 절대 경로를 다시 사용하세요:

cd /home/labex/project/variant-desk

./catalog는 이 디렉터리에 제공된 읽기 도구이며, ./는 현재 디렉터리를 뜻합니다. 상품 ID를 전달해 실제 저장된 문서를 읽으세요:

./catalog TRAIL-1

Trail Jacket에는 서로 다른 다음 세 옵션이 있습니다:

SKU 색상 사이즈 재고 센트 가격
T-GM green M 0 1800
T-BM blue M 5 2200
T-GL green L 4 2100

초록색 M 사이즈 옵션은 품절입니다. 파란색 M 사이즈와 초록색 L 사이즈 옵션은 재고가 있지만 어느 것도 전체 요청을 만족하지 않습니다. SKU는 옵션 하나를 식별하고 상품 ID는 그 부모 상품을 식별합니다. 이 관계를 유지하세요.

./db는 MongoDB 셸을 이 과정의 읽기 전용 카탈로그에 연결합니다. --eval은 따옴표 안의 JavaScript를 실행하고 종료합니다. 셸의 작은따옴표는 공백과 달러 기호로 시작하는 MongoDB 연산자를 셸 확장으로부터 보호합니다. 그 안의 큰따옴표는 JavaScript 문자열에 속합니다. "variants.color" 같은 점 경로는 중첩 필드에 접근합니다. $gt: 0은 0보다 큼을 뜻하고, $lte: 5000은 예산 경계를 포함해 5000 이하임을 뜻합니다. 숫자 값은 따옴표로 감싼 통화 문자열이 아니라 숫자입니다.

다음 일반 점 경로 쿼리는 의도적으로 범위가 너무 넓습니다. find의 두 번째 인수는 상품 ID만 투영합니다. 1은 필드를 포함하고 _id: 0은 MongoDB 내부 ID를 제외합니다. sort의 1은 오름차순을 지정하고, toArray()는 반환된 문서를 읽기 쉬운 배열 하나로 모읍니다:

./db --eval 'db.products.find({"variants.color":"green","variants.size":"M","variants.stock":{$gt:0},"variants.price_cents":{$lte:5000}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'

CAMP-3, RIDGE-2, TRAIL-1이 나와야 합니다. TRAIL-1은 잘못된 일치입니다. 서로 다른 배열 요소가 서로 다른 조건을 충족합니다. 쿼리는 성공적으로 실행되었지만 업무 관점의 답은 잘못되었습니다.

$elemMatch는 모든 기준을 하나의 배열 요소에 적용합니다. MongoDB 배열 기초에서 이미 익숙해진 연산자일 수 있습니다. 여기서는 고객에게 제공할 완전한 결과를 만드는 첫 구성 요소입니다:

./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'

이제 CAMP-3와 RIDGE-2만 남습니다. 일치하는 상품에는 해당하는 옵션이 적어도 하나 있습니다. 그렇다고 그 상품의 모든 옵션이 조건에 맞는다는 뜻은 아닙니다.

쿼리 안에 green/M/5000을 고정하지 않고 요청을 인수로 받는 선택 함수를 저장하세요. JavaScript 함수는 재사용 가능한 코드입니다. function buildSelector(request)는 함수의 이름을 지정하고 request라는 객체를 받습니다. request.color는 그 객체의 색상 속성을 읽습니다. return은 쿼리 객체를 준비된 클라이언트에 제공합니다. 중괄호는 객체나 함수 본문을 감싸고, 콜론은 객체 키와 값을 연결하며, 쉼표는 속성을 구분합니다. 필요한 함수 인터페이스는 쿼리 객체 하나를 반환하는 buildSelector(request)입니다. 셸 명령이나 모듈 내보내기가 아닙니다.

셸에서 cat >는 대상 파일을 덮어씁니다. <<'JS'는 달러 기호와 따옴표를 보존하며 내용을 그대로 받는 here-document를 시작합니다. 마지막 JS를 한 줄에 단독으로 붙여 넣으세요. 전체 블록을 셸 프롬프트에서 입력하세요:

cat > selector.js <<'JS'
function buildSelector(request) {
  return {
    variants: { $elemMatch: {
      color: request.color,
      size: request.size,
      stock: { $gt: 0 },
      price_cents: { $lte: request.max_cents }
    } }
  };
}
JS

./select는 저장된 함수를 읽고 반환된 선택 조건을 변경 없이 MongoDB find로 실행합니다. 세 인수는 색상, 사이즈, 최대 센트 가격입니다. 클라이언트는 예산을 숫자로 변환합니다. green과 M 같은 문자열은 대소문자를 구분하는 정확한 표기로 유지됩니다:

./select green M 5000

실제로 일치하는 문서는 CAMP-3와 RIDGE-2입니다. 일치하지 않는 옵션까지 포함한 원래 배열 전체가 여전히 표시됩니다. find에서는 정상입니다. 선택은 문서를 고를 뿐 표시할 요소를 고르지 않습니다. 선택 함수를 유용한 결과물로 보관하고 다음 단계에서 표시 문제를 해결하세요.

파일이 없으면 ls로 이 디렉터리를 나열하고 파일 이름과 절대 작업 디렉터리를 확인하세요. JavaScript 구문 오류가 나면 저장된 전체 블록을 수정한 형태로 덮어쓰고 클라이언트를 다시 실행하세요. 셸이 계속 기다리면 빠진 마지막 JS 줄을 입력하세요. 쿼리가 맞는 것처럼 보이게 하려고 원본 재고를 바꾸거나, 옵션을 삭제하거나, 데이터베이스를 초기화하지 마세요.

해당하는 모든 SKU 반환하기

이 단계에서는 첫 일치 항목만 반환하거나 전체 배열을 노출하지 않으면서 상품 선택을 완전한 옵션 결과로 발전시킵니다. 같은 Terminal의 /home/labex/project/variant-desk에서 시작하세요.

실제로 일치하는 두 상품을 읽어 보세요:

./catalog RIDGE-2
./catalog CAMP-3

green/M, 최대 5000센트 조건에서 Ridge Jacket에 해당하는 SKU는 두 개입니다. R-GM1은 4500이고 R-GM2는 5000입니다. R-GM3는 너무 비쌉니다. 다른 사이즈/색상과 재고가 0인 옵션도 조건에 맞지 않습니다. Camp Vest에는 C-GM1과 C-GM2가 있으며 둘 다 4000입니다. 가격이 같다고 서로 다른 SKU가 중복이 되는 것은 아닙니다.

위치 기반 투영 "variants.$": 1은 첫 번째로 일치하는 배열 요소만 반환합니다. 이 요청의 전체 집합을 제공하지 못하는 이유를 확인하세요:

./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1,"variants.$":1}).sort({product_id:1}).toArray()'

이 출력에는 상품마다 옵션이 하나뿐입니다. 두 번째 적법한 SKU는 재고가 있어도 빠집니다. 원래 배열 전체를 반환하면 반대로 원하지 않는 옵션까지 포함하는 문제가 생깁니다.

집계 파이프라인은 순서대로 처리되는 단계들의 배열입니다. 각 단계는 앞 단계의 문서를 받습니다. $project는 출력 필드를 선택하고 새 필드를 계산할 수 있습니다. $filter는 조건이 참인 모든 요소를 포함하는 배열을 계산합니다. 여기서는 개수 제한을 적용하지 않습니다.

$filter 안에서 input은 원본 배열이고 as: "v"는 현재 옵션의 이름을 지정합니다. "$variants"는 문서 필드를 참조하고, "$$v.color"는 현재 배열 요소의 색상을 참조합니다. 이 달러 기호들은 JavaScript 문자열 안의 MongoDB 표현식입니다. request.color는 클라이언트가 제공한 JavaScript 요청 값입니다.

집계 비교는 피연산자 배열을 사용합니다. { $eq: [left, right] }는 같음을 검사하고, $gt는 더 큼을, $lte는 작거나 같음을 검사합니다. $and는 같은 v에 대해 모든 비교가 참일 것을 요구합니다. 업무 조건은 같지만 $elemMatch의 쿼리 객체 표기와는 다릅니다.

$ifNull: ["$variants", []]는 null 또는 누락된 원본 필드를 빈 배열로 바꿉니다. []는 빈 JavaScript 배열입니다. 문자열처럼 배열이 아닌 값은 제공된 입력 계약 밖입니다. 이 파이프라인이 임의로 잘못된 모든 카탈로그를 정리한다고 주장하는 것은 아닙니다.

필터링 후 $match는 해당하는 옵션이 없는 상품을 제거합니다. "variants.0"은 인덱스 0, 즉 첫 번째 배열 요소를 가리킵니다. $exists: true는 그 요소가 존재하도록 요구합니다. 이는 match 단계의 쿼리 조건이며 filter 조건 안의 표현식이 아닙니다. 마지막 $sort는 실제 출력을 읽기 편하게 합니다.

전체 템플릿을 finder.js에 저장하세요. 필요한 인터페이스는 단계 배열을 반환하는 buildPipeline(request)입니다. 클라이언트는 반환된 파이프라인을 변경 없이 실행하며 나중에 답을 다시 필터링하지 않습니다:

cat > finder.js <<'JS'
function buildPipeline(request) {
  return [
    { $project: {
        _id: 0, product_id: 1, title: 1,
        variants: { $filter: {
          input: { $ifNull: ["$variants", []] },
          as: "v",
          cond: { $and: [
            { $eq: ["$$v.color", request.color] },
            { $eq: ["$$v.size", request.size] },
            { $gt: ["$$v.stock", 0] },
            { $lte: ["$$v.price_cents", request.max_cents] }
          ] }
        } }
    } },
    { $match: { "variants.0": { $exists: true } } },
    { $sort: { product_id: 1 } }
  ];
}
JS
./finder green M 5000

정확히 두 상품이 나와야 합니다. CAMP-3에는 C-GM1과 C-GM2가, RIDGE-2에는 R-GM1과 R-GM2가 포함됩니다. 반환된 각 옵션에는 정확한 SKU, 색상, 사이즈, 재고, 센트 가격이 유지됩니다. 상품의 정확한 product_id와 title도 유지됩니다. 아직 최저가 필드는 없습니다. 다음 단계에서 추가합니다.

이제 파이프라인은 같은 가격의 Camp Vest 옵션 두 개와 정확히 예산 경계에 있는 Ridge SKU를 포함해 해당하는 SKU 네 개를 모두 반환합니다. TRAIL-1, 빈 배열, null/누락 배열은 결과 상품을 만들지 않습니다. 이 결과를 만들려고 원본 문서를 삭제하거나 편집하지 않았습니다.

클라이언트가 buildPipeline이 파이프라인 배열을 반환해야 한다고 알리면 저장된 파일에 함수 전체와 return [ ... ];가 있는지 확인하세요. 구문 오류를 보고하면 전체 블록을 덮어쓰고 다시 시도하세요. 쿼리 JavaScript는 저장된 파일에 두고, 셸 명령, 대화형 use 명령, 모듈 내보내기를 추가하지 마세요.

일치 집합의 가격을 계산하고 검색 함수 재사용하기

이 단계에서는 해당하는 최저가를 추가하고, 조건에 맞는 결과가 없는 경우까지 포함해 같은 저장 쿼리를 다른 고객 요청에 사용합니다. Terminal의 /home/labex/project/variant-desk에서 시작하세요.

Ridge Jacket의 원래 최저가 옵션은 900센트이지만 재고가 0입니다. 다른 저렴한 옵션은 색상이 다릅니다. 상품 자체를 올바르게 선택했더라도 어느 쪽이든 green/M의 시작 가격으로 광고하면 고객을 오도하게 됩니다.

$set은 집계 단계에서 출력 필드를 추가하거나 교체하며 저장된 상품을 갱신하지 않습니다. 여기서는 min_price_cents를 추가합니다. 배열 피연산자 하나를 받는 $min 표현식은 그 안의 가장 작은 숫자 값을 반환합니다. "$variants.price_cents"는 현재 문서의 옵션 배열 안 가격에 접근합니다. 이 단계를 필터링 뒤에 배치하세요. 원래 배열이 아니라 $project가 만든 일치 배열을 읽어야 합니다. 뒤 단계에서 처리하면 변경된 필드를 모호함 없이 사용할 수도 있습니다.

finder.js를 아래 전체 파이프라인으로 덮어쓰세요. 새로 추가되는 유일한 단계는 빈 배열을 제외한 뒤 최솟값을 계산합니다:

cat > finder.js <<'JS'
function buildPipeline(request) {
  return [
    { $project: {
        _id: 0, product_id: 1, title: 1,
        variants: { $filter: {
          input: { $ifNull: ["$variants", []] },
          as: "v",
          cond: { $and: [
            { $eq: ["$$v.color", request.color] },
            { $eq: ["$$v.size", request.size] },
            { $gt: ["$$v.stock", 0] },
            { $lte: ["$$v.price_cents", request.max_cents] }
          ] }
        } }
    } },
    { $match: { "variants.0": { $exists: true } } },
    { $set: { min_price_cents: { $min: "$variants.price_cents" } } },
    { $sort: { product_id: 1 } }
  ];
}
JS
./finder green M 5000

결과는 해당하는 SKU 네 개를 모두 유지하면서 CAMP-3의 최저가 4000과 RIDGE-2의 최저가 4500을 추가해야 합니다. min_price_cents는 반환된 각 옵션 집합에서 도출됩니다. 동일 가격을 중복 제거하거나, 상품 전체의 최솟값을 사용하거나, 이 두 숫자를 직접 작성하지 마세요.

이제 같은 함수를 사용해 예산을 낮추세요. 요청 값은 바뀌지만 저장된 쿼리는 바뀌지 않습니다:

./finder green M 4499

CAMP-3만 남으며 여전히 4000센트 SKU 두 개와 최저가 4000을 가집니다. Ridge Jacket에는 새 한도보다 낮은 가격의 해당 green/M 옵션이 없습니다.

색상과 한도를 바꾸세요:

./finder blue M 2500

이제 RIDGE-2에는 1000인 R-BM만, TRAIL-1에는 2200인 T-BM만 포함됩니다. 한 요청에서 올바르게 제외된 상품도 다른 요청에서는 포함될 수 있습니다. 가용 여부는 영구적인 상품 표시가 아니라 특정 옵션과 요청의 관계에 속합니다.

마지막으로 일치 항목이 없는 예산을 시도하세요:

./finder green M 500

실제 결과는 []입니다. 클라이언트는 화면을 채우려고 데모 결과로 대체하거나 이용할 수 없는 항목을 표시하지 않습니다. 누락된 쿼리나 데이터베이스 오류는 성공한 빈 배열로 위장하지 않고 오류로 보고됩니다.

최종 파이프라인 결과에는 정확히 product_id, title, variants, min_price_cents가 들어 있습니다. 일치하는 각 상품을 한 번씩, 해당하는 각 SKU를 원래 옵션 필드 다섯 개와 함께 한 번씩 반환하세요. 상품/옵션 순서는 자유롭습니다. 식별 정보와 값이 업무 계약입니다. 쿼리는 같은 스키마에서 ID, 가격, 재고가 다른 카탈로그도 처리해야 합니다. 준비된 클라이언트는 실제 MongoDB 결과를 읽으며 선택, 배열, 가격을 바로잡지 않습니다.

저장한 JavaScript 파일 두 개와 원래 상품/도구를 모두 보관하세요. 만든 것은 재사용 가능한 카탈로그 쿼리이며 예약 시스템은 아닙니다. 동시 재고 변경이나 나중의 구매 완료 과정에 따라 가용 여부는 달라질 수 있습니다. 다음 챌린지에는 독립적인 대여 재고가 있으며, 다른 속성 이름과 요청 값에 같은 쿼리 메커니즘을 사용합니다.

요약

서로 다른 옵션의 조건을 조합한 일치를 거부하고, 첫 번째 일치 항목뿐 아니라 해당하는 모든 SKU를 유지했으며, 필터링한 배열에서 표시 가격을 계산했습니다. 저장한 함수는 원래 카탈로그를 보존하면서 변경된 요청, 동일 가격, 빈 결과를 처리합니다.