TypeScript 클라이언트
./gradlew elcore3ts 는 서버와 같은 모델에서 화면용 TypeScript 클라이언트를 src/main/ts 에 씁니다: DTO, 라벨이
있는 enum, 노출된 루트마다 타입 있는 API 클라이언트, 서버의 400 과 같은 문구의 검증 함수, 생명주기 버튼용 canX(dto).
./gradlew elcore3ts
build·compileJava 는 이 태스크를 부르지 않습니다. 빌드가 클라이언트도 쓰게 하려면:
elasticore {
architecture = 'hexagonal'
typescriptClient = true // 기본 false. elcore3 다음, 컴파일 전에 elcore3ts 를 실행
}
이 설정이 없는데 src/main/ts 에 예전 모델로 만든 생성 클라이언트가 있으면, elcore3 가 끝에 한 번 경고합니다:
"... holds a generated TypeScript client made from an older model - run the elcore3ts task after a model change, or set
typescriptClient = true ...".
생성되는 것 (Pet Store)
src/main/ts/
ItemDTO.ts, ItemSrchDTO.ts, ItemKeyDTO.ts 인터페이스, 필드 제한, validateItem()
ItemApi.ts create, findById, search, update, ... 엔드포인트마다
PurchaseOrderApi.ts ship, cancel, canShip, canCancel, canDelete
CustomerApi.ts signIn, signOut, me (@signin 에서)
OrderStatus.ts, UserRole.ts, CreditCardType.ts enum 과 라벨
Address.ts, CreditCard.ts 값 객체
PurchaseOrderInvariants.ts violations(), isValid() - 보내기 전 확인
apiSupport.ts request(), Page<T>, ProblemDetail, ApiError
unitCost: money(10.2)! @min(0.01, 'price must be positive') 에서 나온 DTO 와 검증:
export interface ItemDTO {
productName?: string;
itemId?: number;
name: string;
description: string;
imagePath?: string;
unitCost: number;
productId?: number;
}
/** Each field's label and limits, as the DSL declares them. */
export const ItemFields = {
name: { label: "name", required: true, maxLength: 30 },
unitCost: { label: "unitCost", required: true, min: 0.01 },
// ...
} as const;
export function validateItem(value: Partial<ItemDTO> | null | undefined, partial = false): Record<string, string> {
// ...
if (!partial && value.unitCost == null) errors.unitCost = "unitCost is required";
else if (value.unitCost != null && Number(value.unitCost) < 0.01) errors.unitCost = "price must be positive";
return errors;
}
화면이 보여 줄 라벨까지 가진 enum:
export const OrderStatus = {
PLACED: "PLC",
SHIPPED: "SHP",
CANCELLED: "CAN"
} as const;
export const OrderStatusLabel: Record<OrderStatus, string> = {
[OrderStatus.PLACED]: "Placed",
[OrderStatus.SHIPPED]: "Shipped",
[OrderStatus.CANCELLED]: "Cancelled",
};
생명주기의 버튼:
export function canCancel(value: PurchaseOrderDTO | null | undefined): boolean {
return value != null && (value.status === OrderStatus.PLACED);
}
오류
2xx 가 아닌 응답은 서버의 problem detail 을 담은 ApiError 가 됩니다: 분기용 code(not-found, invalid,
state.PurchaseOrder.status.cancel ...)와 폼용 fieldErrors(필드 -> 메시지).
try {
await Orders.cancel(id);
} catch (e) {
if (e instanceof ApiError) showError(e.problem?.detail, e.fieldErrors);
}
로그인은 쿠키 세션이라 화면과 API 가 같은 출처(origin)일 때 동작합니다.
브라우저에 닿는 것
TypeScript 모델은 서버가 고른 규칙을 따릅니다: Java·Kotlin 규칙이 ID 참조(customerId)와 값 객체 인라인
(deliveryAddress)을 쓰면 TS 도 그렇습니다. XxxApi.ts 는 컨트롤러에 없는 엔드포인트(@readonly, @restExclude,
명령만 받는 루트의 update)를 만들지 않고, @writeonly 필드는 응답에 없으니 optional 입니다. 명세와 불변식은 함수로
따라옵니다 - 강제가 아니라 보내기 전 확인이고, 거절은 여전히 서버가 합니다. 금액은 number 로 선언되고, 생성된 비교는
Number(...) 를 써서 서버가 소수를 JSON 숫자로 보내든 문자열로 보내든 맞습니다.
Pet Store 의 화면(web/src)은 생성된 라벨(OrderStatusLabel), 검증(validateAddress), 서버 오류
(ApiError.fieldErrors), 버튼 판단(canCancel)을 쓰고, webBuild 태스크가 클라이언트에 대해 타입 검사를 합니다.
서버와 화면이 같은 DSL 에서 나오므로, 필드를 바꾸면 화면 코드가 컴파일 오류로 알려 줍니다.