MAKITTDocs

HcsAsset

docs/domain/hcsAsset.md

HcsAsset

개요

HcsAsset(Headless Component System Asset)은 Builder에서 사전 정의된 컴포넌트 구조를 재사용 가능한 에셋으로 관리하는 도메인. 사용자는 직접 만든 컴포넌트 트리를 HcsAsset으로 저장하고, 다른 페이지나 프로젝트에서 쉽게 재사용할 수 있다.

서버 패키지: com.makitt.core.domain.hcsasset

핵심 기능:

  • Builder에서 만든 커스텀 컴포넌트 구조 저장 및 재사용
  • 비즈니스 의미를 담는 shopComponentType 라벨 시스템
  • OpenSearch 기반 전문 검색 (name, description, tags)
  • 필터링 (componentType, shopComponentType, createdBy, tags)
  • 사용 횟수 추적 (usageCount)
  • 생성자(createdBy) 기반 접근 제어

DynamoDB Entity

HcsAsset

Single Table Design 기반의 HcsAsset 엔티티. 사용자(User)별로 관리되며, OpenSearch와 이벤트 기반 동기화된다.

키 구조:

패턴예시
PKHCSASSET#{hcsAssetId}HCSASSET#550e8400-e29b-41d4-a716-446655440000
SKMETADATAMETADATA

필드:

필드DynamoDB Attribute타입필수설명
pkPKStringOPartition Key
skSKStringOSort Key (METADATA 고정)
entityTypeentity_typeStringO"HCS_ASSET" 고정
hcsAssetIdhcs_asset_idStringOHcsAsset 고유 ID (UUID)
namenameStringO에셋 이름
descriptiondescriptionStringX에셋 설명
treeNodeJsontree_node_jsonStringO컴포넌트 트리 JSON
thumbnailUrlthumbnail_urlStringX썸네일 이미지 URL
componentTypecomponent_typeStringO컴포넌트 타입 (예: "SECTION", "COMPONENT")
shopComponentTypeshop_component_typeStringX비즈니스 의미 라벨 (예: ProductCard, Header)
createdBycreated_byStringO생성자 User ID
usageCountusage_countLongO사용 횟수 (기본값 0)
tagstagsList<String>X태그 목록 (검색/필터용)
gsi1Pkgsi1_pkStringOGSI1 Partition Key
gsi1Skgsi1_skStringOGSI1 Sort Key
createdAtcreated_atInstantO생성 시각
updatedAtupdated_atInstantO수정 시각

Factory Methods:

  • HcsAsset.create(name, description, treeNodeJson, thumbnailUrl, componentType, shopComponentType, createdBy, tags) — 새로운 HcsAsset 생성 (UUID 및 타임스탬프 자동 생성)

Business Methods:

  • incrementUsageCount() — 사용 횟수 증가 + updatedAt 갱신
  • update(name, description, treeNodeJson, thumbnailUrl, componentType, shopComponentType, tags) — 에셋 메타데이터 업데이트 (nullable 필드)

GSI 키 구조

GSI용도PK 패턴SK 패턴
GSI1 (EntityLookupIndex)User별 생성 시간순 정렬 조회USER#{userId}HCSASSET#{createdAtEpochMs}#{hcsAssetId}

GSI1 설명:

  • 사용자가 만든 모든 HcsAsset을 생성 시간순으로 조회
  • KEYS_ONLY Projection → 2단계 조회 (GSI 쿼리 → 메인 테이블 Fetch)
  • Reverse Scan으로 최신순 정렬

Key Builder (com.makitt.core.common.dynamodb.key.HcsAssetKey):

HcsAssetKey.pk(hcsAssetId)"HCSASSET#{hcsAssetId}" HcsAssetKey.sk()"METADATA" HcsAssetKey.gsi1Pk(userId)"USER#{userId}" HcsAssetKey.gsi1Sk(createdAtEpochMs, hcsAssetId)"HCSASSET#{createdAtEpochMs}#{hcsAssetId}" HcsAssetKey.gsi1SkPrefix()"HCSASSET#" HcsAssetKey.extractHcsAssetId(pk)"550e8400..." (from "HCSASSET#550e8400...")

OpenSearch Document

HcsAssetDocument

HcsAsset의 검색 및 필터링을 위한 OpenSearch 인덱스.

Index 이름: hcs-assets

필드 매핑:

필드JSON Property타입설명
hcsAssetIdhcs_asset_idKeywordHcsAsset ID (Document ID)
namenameText에셋 이름 (전문 검색, 가중치 2x)
descriptiondescriptionText에셋 설명 (전문 검색)
treeNodeJsontree_node_jsonString컴포넌트 트리 JSON (저장만, 인덱싱 안 함)
thumbnailUrlthumbnail_urlKeyword썸네일 URL
componentTypecomponent_typeKeyword컴포넌트 타입 (필터용)
shopComponentTypeshop_component_typeKeyword비즈니스 라벨 (필터용)
createdBycreated_byKeyword생성자 User ID (필터용)
usageCountusage_countLong사용 횟수 (정렬용)
tagstagsKeyword[]태그 목록 (필터용)
createdAtcreated_atLong생성 시각 (epoch ms, 정렬용)
updatedAtupdated_atLong수정 시각 (epoch ms)

Factory Method:

  • HcsAssetDocument.fromEntity(hcsAsset) — DynamoDB Entity → OpenSearch Document 변환 (Instant → epoch ms)

검색 전략:

  • name, description: 전문 검색 (name에 2x 가중치)
  • tags: Keyword로 필터 (AND 조건 / OR 조건 양쪽 사용)
  • componentType, shopComponentType, createdBy: Keyword 필터
  • 기본 정렬: created_at DESC (최신순)
  • 태그 검색: usage_count DESC (인기순)
  • 페이지네이션: from = page * size

Value Objects

CreateHcsAssetRequestVo

필드타입설명
nameString에셋 이름
descriptionString설명
treeNodeJsonString트리 JSON
thumbnailUrlString썸네일 URL
componentTypeString컴포넌트 타입
shopComponentTypeString비즈니스 라벨
tagsList<String>태그
createdByString생성자 userId

UpdateHcsAssetRequestVo

필드타입설명
nameString(nullable)
descriptionString(nullable)
treeNodeJsonString(nullable)
thumbnailUrlString(nullable)
componentTypeString(nullable)
shopComponentTypeString(nullable)
tagsList<String>(nullable)

SearchHcsAssetRequestVo

필드타입기본값설명
keywordStringnull검색어 (name, description)
componentTypeStringnull필터
shopComponentTypeStringnull필터
createdByStringnull필터 (userId)
tagsList<String>null필터 (AND 조건)
pageInteger00-indexed 페이지
sizeInteger20페이지 크기

HcsAssetResponseVo

필드타입설명
hcsAssetIdString고유 ID
nameString이름
descriptionString설명
treeNodeJsonString트리 JSON
thumbnailUrlString썸네일
componentTypeString컴포넌트 타입
shopComponentTypeString비즈니스 라벨
createdByString생성자
usageCountLong사용 횟수
tagsList<String>태그
createdAtInstant생성 시각
updatedAtInstant수정 시각

Conversion: HcsAssetResponseVo.fromEntity(HcsAsset) — 정적 팩토리 메서드


Repository

HcsAssetRepository (DynamoDB)

public interface HcsAssetRepository { void save(HcsAsset hcsAsset); Optional<HcsAsset> findById(String hcsAssetId); List<HcsAsset> findByUserId(String userId, int limit); List<HcsAsset> scanAll(); void delete(HcsAsset hcsAsset); }

구현 상세:

  • save(): PutItem (생성/수정 동일)
  • findById(): GetItem (PK=HCSASSET#{hcsAssetId}, SK=METADATA)
  • findByUserId(): GSI1 Query (KEYS_ONLY) → Main Table BatchGetItem. Reverse scan (최신순)
  • scanAll(): Scan + entityType 필터 (개발/관리용, 운영 주의)
  • delete(): DeleteItem

HcsAssetSearchRepository (OpenSearch)

public interface HcsAssetSearchRepository { String index(HcsAssetDocument document); Optional<HcsAssetDocument> findById(String hcsAssetId); void update(HcsAssetDocument document); void delete(String hcsAssetId); List<HcsAssetDocument> search(String userId, String keyword, String componentType, String shopComponentType, List<String> tags, int page, int size); List<HcsAssetDocument> findByUserId(String userId, int size); List<HcsAssetDocument> findByTagsAny(List<String> tags, int size); void bulkIndex(List<HcsAssetDocument> documents); long deleteAllDocuments(); }

Search 로직:

  • search(): BoolQuery — filter(createdBy, componentType, shopComponentType, tags AND 조건) + must(keyword → name^2, description)
  • findByTagsAny(): BoolQuery — should(tags OR 조건), minimum_should_match=1, 정렬: usage_count DESC

Service

HcsAssetService

public interface HcsAssetService { void createHcsAsset(HcsAsset hcsAsset); void updateHcsAsset(HcsAsset hcsAsset); Optional<HcsAsset> findHcsAsset(String hcsAssetId); HcsAsset getHcsAsset(String hcsAssetId); // throws HCS_ASSET_NOT_FOUND List<HcsAsset> findByUserId(String userId, int limit); List<HcsAsset> getAllHcsAssets(); void deleteHcsAsset(HcsAsset hcsAsset); void incrementUsageCount(String hcsAssetId); }

이벤트 발행: 생성/수정/삭제 시 EntityChangedEvent 발행 → OpenSearch 자동 동기화

HcsAssetSearchService (OpenSearch)

public interface HcsAssetSearchService { String indexHcsAsset(HcsAsset hcsAsset); String indexHcsAssetDocument(HcsAssetDocument document); Optional<HcsAssetDocument> getHcsAssetById(String hcsAssetId); void updateHcsAsset(HcsAsset hcsAsset); void updateHcsAssetDocument(HcsAssetDocument document); void deleteHcsAsset(String hcsAssetId); List<HcsAssetDocument> searchHcsAssets(String userId, String keyword, String componentType, String shopComponentType, List<String> tags, int page, int size); List<HcsAssetDocument> findByUserId(String userId, int size); List<HcsAssetDocument> findByTagsAny(List<String> tags, int size); void bulkIndexHcsAssets(List<HcsAsset> hcsAssets); void bulkIndexHcsAssetDocuments(List<HcsAssetDocument> documents); }

Application

HcsAssetApplication

@Service public class HcsAssetApplication { HcsAssetResponseVo createHcsAsset(CreateHcsAssetRequestVo requestVo); HcsAssetResponseVo getHcsAsset(String hcsAssetId, String userId); List<HcsAssetResponseVo> listHcsAssets(String userId, int limit); List<HcsAssetResponseVo> searchHcsAssets(SearchHcsAssetRequestVo requestVo); HcsAssetResponseVo updateHcsAsset(String hcsAssetId, UpdateHcsAssetRequestVo requestVo, String userId); void deleteHcsAsset(String hcsAssetId, String userId); void incrementUsageCount(String hcsAssetId, String userId); }

접근 제어: 모든 수정/삭제/조회 메서드에서 createdBy == userId 확인. 불일치 시 MakittBizException(HCS_ASSET_ACCESS_DENIED) 발생.


API

HcsAssetController

Base Path: /api/v1/hcs-assets

인증: 모든 엔드포인트에 SecurityContext 필요 (SecurityContextUtil.getCurrentUserId())

메서드엔드포인트상태 코드설명
POST/api/v1/hcs-assets201 CreatedHcsAsset 생성
GET/api/v1/hcs-assets200 OK내 HcsAsset 목록 조회 (limit 파라미터)
GET/api/v1/hcs-assets/{hcsAssetId}200 OKHcsAsset 상세 조회
POST/api/v1/hcs-assets/search200 OKOpenSearch 검색/필터
PUT/api/v1/hcs-assets/{hcsAssetId}200 OKHcsAsset 수정
DELETE/api/v1/hcs-assets/{hcsAssetId}204 No ContentHcsAsset 삭제
POST/api/v1/hcs-assets/{hcsAssetId}/increment-usage204 No Content사용 횟수 증가

Request/Response DTOs

CreateHcsAssetRequest:

필드타입검증예시
nameString@NotBlank"Product Card with Rating"
descriptionString선택
treeNodeJsonString@NotBlank"{\"id\":\"...\",\"type\":\"container\",...}"
thumbnailUrlString선택"https://example.com/thumbnail.png"
componentTypeString@NotBlank"COMPONENT"
shopComponentTypeString선택"ProductCard"
tagsList<String>선택["product", "card", "rating"]

Conversion: CreateHcsAssetRequest.toVo(request, createdBy) — userId를 createdBy로 설정

UpdateHcsAssetRequest:

필드타입설명
nameString선택
descriptionString선택
treeNodeJsonString선택
thumbnailUrlString선택
componentTypeString선택
shopComponentTypeString선택
tagsList<String>선택

SearchHcsAssetRequest:

필드타입기본값예시
keywordStringnull"product"
componentTypeStringnull"COMPONENT"
shopComponentTypeStringnull"ProductCard"
pageInteger0
sizeInteger20

Conversion: SearchHcsAssetRequest.toVo(request, createdBy) — userId를 createdBy 필터로 설정

HcsAssetResponse:

필드타입예시
hcsAssetIdString"550e8400-e29b-41d4-a716-446655440000"
nameString"Product Card with Rating"
descriptionString
treeNodeJsonString
thumbnailUrlString
componentTypeString"COMPONENT"
shopComponentTypeString"ProductCard"
createdByString"user123"
usageCountLong42
tagsList<String>
createdAtInstant
updatedAtInstant

에러 응답:

  • 401 Unauthorized: 인증 실패
  • 403 Forbidden: 접근 권한 없음 (createdBy != userId)
  • 404 Not Found: HcsAsset 없음

OpenSearch 동기화

HcsAssetSearchSyncListener

AbstractSearchSyncListener<HcsAsset, HcsAssetDocument> 상속. EntityChangedEvent 리스닝으로 DynamoDB → OpenSearch 자동 동기화.

조건부 활성화: @ConditionalOnProperty(name = "search.sync.enabled", havingValue = "true", matchIfMissing = true)

메서드:

  • toDocument(HcsAsset)HcsAssetDocument
  • indexDocument(HcsAssetDocument) → 인덱싱
  • updateDocument(HcsAssetDocument) → 업데이트
  • deleteDocument(String id) → 삭제

동기화 흐름:

HcsAssetService.createHcsAsset(entity)
  → repository.save(entity)
  → eventPublisher.publishEvent(EntityChangedEvent.created(entity))
    → HcsAssetSearchSyncListener
      → HcsAssetSearchService.indexHcsAsset(entity)
        → HcsAssetSearchRepository.index(document)

Error Codes

코드설명
HCS_ASSET_NOT_FOUNDerror.hcs.asset.not.foundHCS 에셋을 찾을 수 없습니다
HCS_ASSET_ACCESS_DENIEDerror.hcs.asset.access.denied접근할 권한이 없습니다

서버 아키텍처

HcsAssetController (makitt-api)
    |
    v  (Request DTO → VO)
HcsAssetApplication (makitt-application)
    |
    v  (VO → Entity)
HcsAssetService (makitt-core/service)
    |
    +---> HcsAssetRepository (DynamoDB)
    |
    +---> EntityChangedEvent → HcsAssetSearchSyncListener
    |                              |
    |                              v
    +---> HcsAssetSearchService → HcsAssetSearchRepository (OpenSearch)
    |
    v
HcsAsset (makitt-core/entity)

패키지 구조:

makitt-core/
  com.makitt.core.domain.hcsasset/
    entity/
      HcsAsset.java                    # DynamoDB Entity
    vo/
      CreateHcsAssetRequestVo.java
      UpdateHcsAssetRequestVo.java
      SearchHcsAssetRequestVo.java
      HcsAssetResponseVo.java
    service/
      HcsAssetService.java             # 인터페이스
      HcsAssetServiceImpl.java         # 비즈니스 로직
    repository/
      HcsAssetRepository.java          # DynamoDB Repository
      HcsAssetRepositoryImpl.java

  com.makitt.core.common.dynamodb.key/
    HcsAssetKey.java                   # Key Builder
    KeyPrefix.java                     # HCS_ASSET = "HCSASSET#"

  com.makitt.core.opensearch/
    document/
      HcsAssetDocument.java            # OpenSearch Document
    repository/
      HcsAssetSearchRepository.java
      HcsAssetSearchRepositoryImpl.java
    service/
      HcsAssetSearchService.java
      HcsAssetSearchServiceImpl.java
    listener/
      HcsAssetSearchSyncListener.java  # 이벤트 기반 동기화

makitt-application/
  com.makitt.application.hcsasset/
    HcsAssetApplication.java

makitt-api/
  com.makitt.api.controller.hcsasset/
    HcsAssetController.java
    dto/
      CreateHcsAssetRequest.java
      UpdateHcsAssetRequest.java
      SearchHcsAssetRequest.java
      HcsAssetResponse.java

접근 제어

작업권한
생성인증된 사용자
조회생성자 본인만
목록 조회본인이 생성한 에셋만
검색본인이 생성한 에셋만 (createdBy 필터 자동 적용)
수정생성자 본인만
삭제생성자 본인만
사용 횟수 증가생성자 본인만

검증 패턴:

if (!entity.getCreatedBy().equals(currentUserId)) { throw new MakittBizException(ErrorCode.HCS_ASSET_ACCESS_DENIED, ...); }

관련 설정

HcsConfig

@Configuration @ConfigurationProperties(prefix = "hcs") public class HcsConfig { private SystemDefaults systemDefaults; public static class SystemDefaults { private String tokensKey; // S3 object key for default tokens private String presetsKey; // S3 object key for default presets private String nodeDefaultsKey; // S3 object key for default node-defaults } }

참조