Content
최종 수정: 2026년 7월 3일
Content는 Content Type(틀)으로 찍어낸 실제 데이터 한 건입니다. 옷가게 쇼핑몰을 예로 들면, Content Type "상품"이 상품명·가격·상세 설명 같은 항목 구성을 규정하고, "스테인리스 텀블러 500ml" 한 개가 그 틀을 따르는 Content 한 건입니다.
Content는 두 부분으로 이루어집니다. fields에는 각 필드의 실제 값이 들어가고, sys에는 발행·버전·보관 같은 상태가 담깁니다. CMA에서 Content는 Space 하위 리소스이며, 조회·삭제 경로는 /spaces/{spaceId}/contents를, 생성·수정 경로는 Content Type 하위인 /spaces/{spaceId}/content-types/{contentTypeId}/contents를 기준으로 합니다. 관리 작업은 CMA에서 수행하고, 발행된 스냅샷은 CDA로 전달됩니다.
리소스 구조
다음은 발행된 Content "스테인리스 텀블러 500ml"의 단일 조회 응답입니다. sys(시스템 속성)와 함께 fields(필드 값)와 metadata(태그 등 부가 정보)를 가집니다.
{
"sys": {
"id": "3trmXRM3RqbgSnifyg7PUl8DzDgDzP",
"type": "Content",
"space": { "sys": { "id": "HnQ32YiH", "type": "Refer", "targetType": "Space" } },
"contentType": { "sys": { "id": "3trmXRLdJF4GBlAjtcuoZ7Pnxj8dlA", "type": "Refer", "targetType": "ContentType" } },
"publish": {
"version": 1,
"at": "2026-06-18T09:51:44.128Z",
"firstAt": "2026-06-18T09:51:44.128Z",
"counter": 1,
"by": { "sys": { "id": "3p4tcFbQRwz503VXdtHXNI5dZH5TVB", "type": "Refer", "targetType": "User" } }
},
"createdBy": { "sys": { "id": "3p4tcFbQRwz503VXdtHXNI5dZH5TVB", "type": "Refer", "targetType": "User" } },
"createdAt": "2026-06-18T09:51:14.597Z",
"updatedBy": { "sys": { "id": "3p4tcFbQRwz503VXdtHXNI5dZH5TVB", "type": "Refer", "targetType": "User" } },
"updatedAt": "2026-06-18T09:51:44.128Z",
"version": 2,
"status": "Published"
},
"fields": {
"price": { "en-US": 18000 },
"productName": { "en-US": "Stainless Tumbler 500ml", "ko-KR": "스테인리스 텀블러 500ml" }
},
"metadata": { "tags": [] }
}주요 키:
sys.id: Content의 고유 식별자입니다. 단일 조회·수정·삭제·발행 경로의{contentId}에 들어갑니다.sys.contentType: 이 Content가 따르는 Content Type을 가리키는Refer입니다.fields: 각 필드의 값입니다. 키는 필드의apiName이며, 값은 로케일별 맵입니다. 아래 필드 키는 apiName이다에서 설명합니다.metadata.tags: 이 Content에 단 Tag 목록입니다. 각 항목은Refer<Tag>모양이며, 단 태그가 없으면 빈 배열[]입니다.
필드 키는 apiName이다
fields 객체의 키는 각 필드의 apiName입니다. Content Type의 내부 id나 콘텐츠 스튜디오에 보이는 필드 이름(예: 상품명)이 아닙니다. 따라서 Content를 만들거나 읽을 때는 Content Type에 정의된 apiName을 키로 써야 합니다.
데모 "상품" Content Type의 필드와 그 apiName 매핑은 다음과 같습니다.
| 콘텐츠 스튜디오 이름 | apiName | type | localized | required |
|---|---|---|---|---|
| 상품명 | productName | ShortText | true | true |
| 가격 | price | Long | false | false |
| 상세 설명 | description | RichText | true | false |
| 대표 사진 | photo | Refer<Media> | false | false |
| 브랜드 | brand | Refer | false | false |
각 필드 값은 로케일별 맵입니다. 값의 모양은 localized에 따라 다릅니다.
localized: true필드는{ "<locale>": 값 }형태로 여러 로케일 값을 가질 수 있습니다. 예:"productName": { "en-US": "Stainless Tumbler 500ml", "ko-KR": "스테인리스 텀블러 500ml" }.localized: false필드(비종속 필드)는 Space 기본 Locale 키 하나에만 값을 넣습니다. 다른 로케일 키는 넣을 수 없습니다. 데모 Space의 기본 Locale이en-US이므로, 가격은"price": { "en-US": 18000 }처럼en-US키 하나만 가집니다.
기본 Locale이 무엇인지, required 필드를 어느 로케일까지 채워야 하는지, 값이 없을 때의 fallback 규칙은 다국어 (개념)에서 다룹니다.
타입별 값 모양
로케일 키 안에 들어가는 값의 모양은 필드의 type을 따릅니다. 대부분의 타입은 그 타입의 값을 그대로 넣습니다(텍스트 타입은 문자열, Number·Long은 숫자, Boolean은 true/false). 모양이 객체나 배열이라 헷갈리기 쉬운 타입은 다음과 같습니다.
Location:{ "latitude": <숫자>, "longitude": <숫자> }객체입니다.latitude는 -90 이상 90 이하,longitude는 -180 이상 180 이하이며, 두 키 모두 필수입니다. 정의된 키 외에는 받지 않으므로lat·lng·lon같은 줄임 키를 쓰면 거부됩니다.Refer: 대상을 가리키는Refer객체이며, id 문자열만 넣지 않고sys객체를 그대로 넣습니다. 다른 Content를 가리키면targetType이Content, Media를 가리키면Media입니다.Refer모양 자체는 시스템 속성 (sys)의 Refer 모양에서 정의합니다.Array: 원소 타입의 값을 담은 JSON 배열입니다. 원소가 텍스트면 문자열 배열이고, 원소가Refer(Content 또는 Media)면Refer객체의 배열입니다. id 문자열의 배열이 아닙니다.
다음은 fields 안에 들어가는 값의 예입니다(Space 기본 Locale이 en-US인 경우).
{
"location": { "en-US": { "latitude": 37.5662, "longitude": 126.9910 } },
"tags": { "en-US": ["신상", "한정판"] },
"photo": { "en-US": { "sys": { "type": "Refer", "id": "3trmXRM3RqbgSnifyg7PUl8DzDgDzP", "targetType": "Media" } } },
"photos": { "en-US": [ { "sys": { "type": "Refer", "id": "3trmXRM3RqbgSnifyg7PUl8DzDgDzP", "targetType": "Media" } } ] }
}시스템 속성 (sys)
모든 Content는 공통 시스템 속성을 sys 객체에 담습니다. space, contentType, createdBy, updatedBy는 Refer 모양({ "sys": { "id", "type": "Refer", "targetType" } })으로 들어갑니다.
| 속성 | 타입 | 설명 |
|---|---|---|
id | string | 리소스 고유 식별자. |
type | string | 리소스 종류. Content는 항상 "Content". |
space | Refer<Space> | 이 Content가 속한 Space. |
contentType | Refer<ContentType> | 이 Content가 따르는 Content Type. |
publish | object | 발행 상태 포인터. 아래 키 참조. |
archive | object | 보관 정보. 보관 중일 때만 존재하며, 아니면 키가 없습니다. 아래 키 참조. |
createdBy | Refer<User> | 생성한 사용자. |
createdAt | string (date-time) | 생성 시각. |
updatedBy | Refer<User> | 마지막으로 수정한 사용자. |
updatedAt | string (date-time) | 마지막 수정 시각. |
version | integer (≥1) | 리소스 버전. 생성·수정·발행·발행취소·보관 등 모든 변경마다 1씩 올라갑니다. |
status | string (enum) | 발행 상태. 아래 4가지 중 하나. |
status는 다음 4가지 중 하나입니다.
status | 의미 |
|---|---|
Draft | 작성 중이며 아직 발행되지 않은 상태. |
Changed | 발행된 적이 있으나 그 뒤 수정되어 아직 발행되지 않은 변경분이 있는 상태. |
Published | 발행되어 있고 미발행 변경분이 없는 상태. |
Archived | 보관된 상태. |
publish 객체는 발행 상태를 가리키는 포인터입니다. 발행 중일 때는 다음 키를 모두 가집니다.
| 키 | 타입 | 설명 |
|---|---|---|
version | integer | 발행된 시점의 sys.version. |
at | string (date-time) | 마지막 발행 시각. |
firstAt | string (date-time) | 최초 발행 시각. 발행취소해도 보존됩니다. |
counter | integer | 누적 발행 횟수. |
by | Refer<User> | 마지막으로 발행한 사용자. |
발행취소하면 publish에서 version·at·by가 빠지고 firstAt·counter만 남습니다. 한 번도 발행한 적이 없으면 publish는 { "counter": 0 }입니다.
archive 객체는 보관 중일 때만 존재합니다. 보관 중이면 version(보관 시점의 sys.version)·at(보관 시각)·by(보관한 사용자)를 가지며, 보관 상태가 아니면 archive 키 자체가 없습니다.
아래 예시의 sys.version과 모든 시각 값은 실제 호출 시점의 값이며, 호출마다 달라집니다.
발행과 버전, 동시성
Content의 라이프사이클은 다음과 같습니다.
- 생성하면
status는Draft입니다.Content는 생성 시 자동으로 발행되지 않습니다. 이 점이 Content Type과 다릅니다. Content Type은 생성과 동시에 자동 발행되지만, Content는 별도 발행 호출이 있어야 전달 경로에 오릅니다. - 발행하면
status가Published가 됩니다. - 발행한 뒤 수정하면
status가Changed가 됩니다. 미발행 변경분이 있다는 뜻입니다. - 발행취소하면
status가 다시Draft가 됩니다. - 보관하려면 먼저 발행취소해야 합니다. 발행 상태인 Content는 바로 보관할 수 없습니다.
sys.version은 변경마다 1씩 올라갑니다.
수정·부분 수정·발행·발행취소·보관·보관해제 요청에는 x-weegloo-version 헤더에 현재 sys.version을 실어야 합니다. 이 값이 빠지거나 현재 버전과 어긋나면 동시 수정 충돌로 간주되어 요청이 거부됩니다. 생성과 삭제 요청에는 이 헤더가 없습니다. 발행·발행취소·보관·보관해제 같은 상태 전환에는 별도 요청 본문이 없습니다.
API
아래 모든 엔드포인트의 기준 URL은 https://cma.weegloo.com/v1이며, Authorization 헤더에 CMA를 인증하는 Bearer 토큰이 필요합니다. 수정·부분 수정·발행·발행취소·보관·보관해제는 낙관적 동시성 제어를 위해 X-Weegloo-Version 헤더(현재 리소스의 sys.version)를 함께 보내야 합니다.
/contents 목록(GET /spaces/{spaceId}/contents)을 fields.*로 필터하거나 정렬할 때는 sys.contentType.sys.id={contentTypeId}로 Content Type을 함께 지정해야 합니다. contentType={contentTypeId} 형태로는 대신할 수 없습니다. /content-types/{contentTypeId}/contents는 Content Type이 경로에 명시되어 있어 따로 지정할 필요가 없습니다.
관련 문서
- Content Type: 이 Content의 틀(필드 정의·apiName).
- CDA Content: 발행된 Content를 방문자에게 전달(읽기).
- Tag: metadata.tags에 다는 분류 라벨.
- 다국어 (개념): 기본 Locale·필수 채움·fallback.
