값 표현식 (Value Expressions)

Script에서 값이 필요한 모든 자리(URL, 요청 body, 필드 값, 조건, 필터 값, 대상 id 등)는 아래 세 형태 중 하나입니다. 예외는 둘뿐입니다. RegexpatternCachekey는 리터럴로만 쓰며 그 안의 { /pointer }는 값으로 바뀌지 않습니다. 이 문서는 그 세 형태와, 값이 어디서 오는지(컨텍스트 루트), 그리고 WEEGLOO 데이터 특유의 로케일 맵 규칙을 설명합니다. Statement 카탈로그의 모든 필드가 이 규칙을 따릅니다.

세 가지 형태

형태규칙
참조 (reference)문자열 안의 { /json-pointer }를 컨텍스트에서 resolve합니다."{ /payload/fields/title }"
리터럴 (literal){ /ptr }가 없는 값(문자열, 숫자, 불리언, 객체, 배열)입니다. 그대로 사용합니다."draft", 42, true, { "a": 1 }
연산과 조건 (JsonLogic)연산자 하나를 키로 갖는 객체입니다. 피연산자는 다시 값 표현식(참조, 리터럴, 중첩)입니다. 자리에 따라 연산자에 $가 필요합니다.{ "$+": [ "{ /vars/n }", 1 ] }

세 형태는 중첩됩니다. JsonLogic 피연산자에 참조를, 참조 결과를 다시 연산에 넣는 식으로 조합합니다.

데이터 자리와 식 자리: $를 언제 붙이나

같은 JSON이 자리에 따라 다르게 읽힙니다. 갈리는 기준은 그 자리의 키를 누가 소유하는가입니다. fields의 키는 Content Type의 필드 id이고 Http.body의 키는 상대 API의 스키마이므로, 그런 자리에서 cat이나 in은 연산자가 아니라 필드 이름이어야 합니다.

자리해당 필드읽는 방식
데이터 자리fields(ResourceCreate, ResourceUpdate, ResourcePatch), Http.body, Return.value, SetVar.value, Cache.value, Cache.defaultValue$가 없는 키는 언제나 필드 이름입니다. 연산을 쓰려면 $를 붙입니다.
식 자리If.condition, Loop.while, version값 전체가 식입니다. 연산자는 cat$cat도 됩니다.
템플릿 자리그 밖의 전부(url, method, headers[].value, locale, order, over, target.sys.id, EmailSend의 필드들, Signature·Hash·Regex의 값 필드)문자열이므로 { /pointer }만 들어갑니다.
리터럴 전용Regex.pattern, Cache.key값 표현식이 아닙니다. Regex.pattern에 적은 { /pointer }는 치환되지 않고 패턴의 일부가 됩니다.

규칙은 두 줄입니다.

  1. 데이터 자리에서 $가 없는 키는 언제나 필드 이름입니다. 연산을 쓰려면 연산자에 $를 붙입니다.
  2. 한번 $로 식에 들어가면 그 안쪽은 전부 식입니다. 중첩된 연산자에는 $가 필요 없습니다(붙여도 됩니다).

헷갈리면 모든 연산자에 $를 붙이세요. 어느 자리에서나 옳습니다.

// 데이터 자리: cat 은 Content Type 의 필드 이름입니다 (연결 연산이 아닙니다)
"fields": { "cat": { "en-US": "hello" } }
 
// 데이터 자리에서 계산: 경계에만 $, 그 안쪽은 그대로
"fields": { "tier": { "en-US": { "$if": [ { ">=": [ "{ /p/score }", 700 ] }, "gold", "silver" ] } } }
 
// 식 자리: 그대로 씁니다
"condition": { "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }

$로 시작하는 필드 이름이 필요할 때: $$

JSON Schema의 $ref, $schema처럼 키가 실제로 $로 시작해야 하면 $를 두 번 씁니다. "$$ref"는 데이터 키 $ref를 뜻합니다. 맨 앞의 $ 하나만 벗겨지고($$$ref$$ref), 키에만 적용됩니다(값 안의 $는 그대로입니다).

"body": { "$$ref": "#/components/schemas/Item", "topK": { "$min": [ "{ /payload/fields/k }", 50 ] } }

거부되는 두 가지

아래 두 경우는 조용히 다른 뜻으로 해석되지 않고 오류로 거부됩니다.

  • $ 키가 같은 객체의 다른 키와 함께 있으면 오류입니다. 연산은 그 객체의 유일한 키여야 하며, 형제 데이터는 한 단계 밖으로 빼면 됩니다.
  • 모르는 $는 오류입니다. $catt$catt라는 필드가 아닙니다. $ 네임스페이스는 연산자용으로 예약되어 있습니다.

식 자리에서는 연산자 이름이 형제 키와 함께 있는 것도 오류입니다({ "and": […], "or": […] }). 그 자리에는 데이터라는 해석이 없고 모든 객체는 참으로 판정되므로, 그대로 두면 조건이 조용히 항상 참이 됩니다.

참조: { /json-pointer }

중괄호 안에 RFC 6901 JSON Pointer(반드시 /로 시작)를 넣습니다. 중괄호 주변 공백은 허용됩니다({ /a/b }{/a/b}와 같습니다).

단일 포인터와 혼합 템플릿: 타입 규칙

  • 문자열 전체가 단일 포인터면 그 값의 원래 타입을 그대로 유지합니다(숫자면 숫자, 객체면 객체, 배열이면 배열).
  • 리터럴 텍스트와 섞이면 문자열로 연결(concatenation) 됩니다.
"{ /payload/fields/count }"                 // 숫자 값이면 숫자 그대로 (예: 42)
"{ /payload/fields/tags }"                  // 배열이면 배열 그대로
"page-{ /payload/fields/n }-of-10"          // 문자열 연결 → "page-42-of-10"
"Bearer { /payload/fields/token }"          // 문자열 연결 → "Bearer abc123"

없는 값

  • 경로가 없거나 값이 비면 단일 포인터는 null, 혼합 템플릿은 빈 문자열로 처리됩니다.

컨텍스트 루트: 값은 어디서 오나

{ /pointer }의 최상위 세그먼트는 아래 일곱 중 하나입니다.

루트내용
/payload호출 시 전달된 JSON payload(입력)입니다. 예: { /payload/fields/email }
/rawPayload같은 입력을 호출자가 보낸 본문 문자열 그대로 담습니다(파싱 전). 예: { /rawPayload }
/headers호출 시 전달된 요청 HTTP 헤더입니다. 키는 소문자이고 이름당 단일 값입니다. 예: { /headers/authorization }
/now실행이 시작된 시각입니다. { /now/seconds }·{ /now/millis }·{ /now/iso }
/<name>name이 붙은 앞선 statement의 결과입니다. 예: { /order/sys/id }
/vars/<name>SetVar로 선언한 script-scoped 가변 변수입니다. 예: { /vars/total }
/errorTrycatch 블록 안에서만 씁니다. 잡힌 에러 { message }입니다. 예: { /error/message }

/<name>을 뺀 여섯 이름(payload·rawPayload·headers·now·vars·error)은 예약되어 있어 statement의 name으로 쓸 수 없습니다. 같은 이름을 쓰면 그 루트를 덮어써 버리므로 저장 시점에 거부됩니다(공통 필드의 바인딩 이름 규칙).

/rawPayload: 보낸 그대로의 본문

/payload는 파싱된 값이고, /rawPayload같은 본문의 원문 문자열입니다. 둘은 같은 것을 가리키지만 같지 않습니다. 파싱된 값을 다시 문자열로 만들면 공백, 숫자 표기, 이스케이프, 중복 키가 모두 정규화되어 보낸 바이트로 돌아오지 않습니다.

그래서 보낸 바이트 위에서 계산되는 값은 /rawPayload로만 다룰 수 있습니다. 대표적인 경우가 결제 대행사 웹훅의 서명 검증입니다(Signature). 값을 꺼내 쓰는 평소의 참조는 /payload로 합니다.

호출 본문은 JSON 객체만 받습니다. 본문이 비면 없는 것으로 보고, JSON 객체가 아니면(깨진 JSON, 배열, 스칼라, 리터럴 null) 실행하지 않고 거부합니다(오류 참조).

/now: 실행이 시작된 시각

/now는 이 실행이 시작된 시각을 세 가지 형태로 담습니다.

포인터
{ /now/seconds }epoch 초(정수)
{ /now/millis }epoch 밀리초(정수)
{ /now/iso }sys.createdAt 같은 플랫폼의 시각 표기 문자열(UTC)
  • 한 실행에는 시각이 하나뿐입니다. 시계를 읽는 statement가 아니라 실행이 시작될 때 심어 두는 값이므로, 문 두 개가 서로 다른 값을 볼 일이 없습니다. Parallel의 각 브랜치도 같은 시각을 물려받습니다. 문이 아니라서 statement 개수에도 세지 않습니다.
  • 표준 시간대를 고르는 필드는 없습니다. epoch 값은 어디서나 같은 수이고, iso는 UTC 표기입니다.
  • 웹훅의 replay window(서명이 실린 타임스탬프가 지금으로부터 몇 초 안인지) 검증에 씁니다. 타임스탬프는 보통 문자열로 들어오지만 산술 연산이 숫자로 바꿔 주므로 그대로 비교합니다.
// 서명에 실린 타임스탬프가 5분(300초) 안인가
{ "<": [ { "-": [ "{ /now/seconds }", "{ /sig/1 }" ] }, 300 ] }

statement 결과의 모양

name을 붙인 statement의 결과 형태는 타입마다 다릅니다.

statement결과 모양참조 예
Http{ status, body }{ /resp/status }, { /resp/body/choices/0/message/content }
ResourceCreate, ResourceRead(단건), ResourceFind(단건)리소스 그 자체{ /post/sys/id }, { /post/fields/title/en-US }
ResourceForEach(순회 중) name현재 항목 = 리소스 그 자체. onEach 안에서만 참조{ /post/sys/id }, { /post/fields/title/en-US }
ParseJson파싱된 값 그 자체(객체·배열·스칼라){ /quote/items/0/price }
SignatureBoolean(검증 통과 여부){ /verified }
Hash문자열(선언한 표기의 다이제스트){ /expectedSign }
RegexMatchBoolean. Capture는 배열(0=매치 전체, 1부터 캡쳐 그룹) 또는 매치가 없으면 null{ /isOrderId }, { /sig/1 }
  • ResourceFind는 매치가 없으면 null을 바인딩합니다. { "==": [ "{ /found }", null ] }로 존재 여부를 분기합니다.
  • ResourceRead(단건)는 대상이 없으면 에러입니다(Try로 처리 가능). 자세한 내용은 Statement 카탈로그의 리소스 읽기에서 다룹니다.
  • ServiceUser를 읽으면 결과는 회원 리소스 그 자체입니다({ /member/sys/id }). Content·Media와 달리 필드가 로케일 맵이 아니라 값 그대로입니다. 규칙은 회원 디렉터리 읽기에서 다룹니다.

연산과 조건: JsonLogic

계산이나 조건이 필요하면 jsonlogic.com 스펙의 연산자 객체를 씁니다.

  • 데이터 접근은 vanilla var(dot-path)가 아니라 { /ptr } 참조로 통일합니다. 엔진이 피연산자의 포인터를 먼저 resolve한 뒤 연산자를 적용합니다.
  • 연산자는 그 객체의 유일한 키여야 합니다. 데이터 자리에서는 $를 붙인 키만 연산이고, 식 자리에서는 $ 유무에 관계없이 연산입니다(데이터 자리와 식 자리 참조).

연산자 표

표의 이름은 연산자 토큰입니다. 데이터 자리에 쓸 때는 앞에 $를 붙입니다(cat$cat). 식 자리에서는 둘 다 됩니다.

분류연산자의미와 예
조건if (별칭 ?:){ "if": [조건, 참값, 조건2, 참값2, …, 기본값] }. 첫 참 조건의 값, 없으면 마지막 기본값.
논리and, or단축 평가. and는 첫 falsy(또는 마지막), or는 첫 truthy(또는 마지막)를 값으로 돌려줍니다.
논리! (not), !! (to-bool){ "!": x }는 truthy 부정, { "!!": x }는 truthy 여부. 존재 검사에 !!를 자주 씁니다.
동등==, !=느슨한 비교(숫자 강제 변환 후 비교. "1"==1은 참).
동등===, !==엄격한 비교(타입까지).
비교<, <=, >, >=연쇄 가능: { "<": [1,2,3] }1<2 AND 2<3. 숫자화 불가(NaN)면 false.
산술+모든 피연산자의 합.
산술-피연산자 1개면 음수화, 2개면 뺄셈.
산술*, /, %곱, 나눗셈, 나머지.
집계min, max피연산자들의 최소, 최대.
문자열cat모든 피연산자를 문자열로 이어 붙임.
포함in{ "in": [needle, haystack] }. haystack이 문자열이면 부분문자열, 컬렉션이면 원소 포함.
배열merge여러 배열이나 값을 하나의 배열로 평탄화(누적 수집에 씀).

배열 순회 연산자(map, filter, reduce, all, some, none)는 지원하지 않습니다. Script는 배열을 Loop로 순회합니다(Statement 카탈로그의 Loop).

숫자 변환과 예시

숫자 변환 규칙은 다음과 같습니다. 숫자는 그대로 두고, true는 1, false는 0, 문자열은 파싱하며(파싱할 수 없으면 계산이 실패합니다), null은 0으로 변환합니다.

아래 스니펫은 식 자리 기준입니다. 데이터 자리(fields, Http.body, Return.value, SetVar.value)에 넣을 때는 최상위 연산자에 $를 붙이고 안쪽 피연산자는 그대로 둡니다.

{ "-":  [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] }   // 잔액 - 비용
{ "<":  [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] }   // 잔액 < 비용 → boolean
{ "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }
{ "cat": [ "id-", "{ /payload/sys/id }" ] }                                    // "id-<uuid>"
{ "!!": "{ /found/sys/id }" }                                                  // 존재하면 true
{ "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] }                       // 배열 누적: SetVar.value 는 데이터 자리라 $
{ "if": [ "{ /payload/fields/next }", "{ /payload/fields/next }", "END" ] }     // next가 있으면 next, 없으면 "END"

참과 거짓 판정 (Truthiness)

if, and, or, !, !!If.condition, Loop.while은 아래 규칙으로 참과 거짓을 가립니다.

  • falsy: null, false, 숫자 0, 빈 문자열 "", 빈 컬렉션(빈 배열).
  • truthy: 그 외 전부(0이 아닌 숫자, 비어 있지 않은 문자열과 배열, 모든 객체).

키도 참조할 수 있습니다

fields 같은 맵의 { /ptr } 참조를 지원합니다. 키가 런타임에 resolve됩니다.

"fields": { "{ /payload/fields/fieldName }": { "en-US": "{ /payload/fields/fieldValue }" } }

두 키가 같은 값으로 resolve되면 충돌이 나고 엔진 에러입니다.

로케일 맵: Content와 Media 특유 규칙

WEEGLOO ContentMedia의 각 필드는 값이 아니라 로케일별 맵입니다(예: balance{ "en-US": 1, "ko-KR": 10 }). 따라서 읽고 쓸 때 로케일을 함께 다뤄야 합니다. Mediatitledescription(스칼라), file(인제스트 지시)이 로케일 맵입니다. /payload나 HTTP 응답처럼 ContentMedia가 아닌 JSON은 이 규칙과 무관합니다(스키마가 정한 구조 그대로이고, 스칼라면 스칼라입니다).

읽기

  • 스칼라를 얻으려면 로케일까지 지정합니다: { /<name>/fields/<field>/<locale> }(예: { /post/fields/title/en-US }).
  • 로케일 없이 { /<name>/fields/<field> }면 로케일 맵 객체 전체가 나옵니다.
  • localized:false 필드는 기본 로케일 버킷에만 있으므로 그 기본 로케일 코드로 읽습니다.

쓰기 (ResourceCreate, ResourceUpdate, ResourcePatch의 fields)

값은 로케일 맵 { "<locale>": <스칼라 값표현식> }입니다. 읽기와 대칭입니다.

"fields": {
  "title":  { "en-US": "Hello", "ko-KR": "안녕" },   // 여러 로케일은 버킷 나열
  "status": { "en-US": "paid" }
}
  • ResourceCreate는 populate하는 모든 필드에 Space 기본 로케일 버킷을 반드시 포함해야 합니다(default-locale 규칙).
  • ResourceUpdate는 전체 교체입니다. fields에 없는 필드와 로케일은 제거됩니다(file 포함).
  • ResourcePatch는 지정한 필드와 버킷만 갱신합니다(나머지 필드와 로케일은 유지).
  • 리터럴 null로 삭제합니다: 값이 리터럴 null이면 그 (field, locale) 버킷을 삭제합니다(Patch에서 특정 로케일 비우기의 표준). ""(빈 문자열)은 삭제가 아니라 빈 값 설정입니다. 값표현식({ /ptr })이 런타임에 null로 평가되면 삭제가 아니라 에러입니다(payload 누락을 조용히 삼키지 않음). 삭제는 리터럴 null만 해당합니다.
  • Media file: 값이 스칼라가 아니라 인제스트 지시 { "source": …, "encoding": "url"|"base64" }입니다. 인제스트가 실제로 무엇을 하는지는 Statement 카탈로그의 ResourceCreate에서 다룹니다.
  • localized:false 필드는 기본 로케일 버킷에만 넣습니다.
  • 로케일 코드(맵 키)도 { /ptr } 참조가 가능합니다(위 키도 참조할 수 있습니다 참조). 동적 로케일을 만들 때 씁니다.

locale 편의 필드

ResourceCreate, ResourceUpdate, ResourcePatchlocale을 주면 엔진이 fields각 값을 자동으로 { <locale>: 값 } 버킷으로 감쌉니다. 즉 스칼라만 주면 됩니다.

// 아래 두 개는 동일하다
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "locale": "en-US", "fields": { "title": "Hello" } }
 
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "fields": { "title": { "en-US": "Hello" } } }

locale을 주면서 값에 이미 로케일 맵({ "en-US": … })을 중첩하면 { <locale>: { "en-US": … } }이중 중첩됩니다(작성자 실수). locale을 쓰면 스칼라만, 안 쓰면 명시 로케일 맵만 쓰도록 하나로 통일합니다.

where와 order의 로케일

  • whereorder에서 fields.X는 엔진이 Space 기본 로케일을 자동 적용합니다(CMA 조회와 동일).
  • 특정 로케일을 지정하려면 fields.X.<locale>로 명시합니다.
"where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }   // 기본 로케일 slug
"where": { "fields.title.ko-KR": { "prefix": "안" } }              // 특정 로케일

오류

값 표현식의 규칙을 어겼을 때 나오는 코드입니다. 저장 시 검사하며, 정의의 다른 정적 제약을 어긴 코드는 실행 시맨틱, 제약, 보안의 오류에, 호출 시 나오는 코드는 엔드포인트의 오류에 있습니다. 모든 리소스에 공통인 코드는 공통 오류를 참조하세요.

코드조건
WGL400056데이터 자리에서 $ 연산 키를 같은 객체의 다른 키와 함께 두었습니다.
WGL400055데이터 자리에 연산자로 정의되지 않은 $ 키를 적었습니다.