Files
Gnuboard7/docs/frontend/g7core-api-advanced.md
T
HeuJung 1e89335258 fix(core,extensions): 동적 스크립트 주입 경로 전부에 출처 게이트 적용
레이아웃 scripts[] 에만 있던 원격 스크립트 차단 게이트가 다른 주입 경로에는
없어, 저장측(SafeLayoutExpressions·NoExternalUrls)이 외부 URL 저장을 422 로
막아도 런타임 디스패치 한 번으로 임의 원격 코드가 로드됐다. 저장 검증이
통째로 무의미해지는 상태였고 오류도 경고도 남지 않았다.

판정식을 support/scriptSrcPolicy.ts 로 분리해 런타임 SSoT 를 하나로 두고,
loadScript 액션 · 확장 핸들러 재로드 · 편집기 프리뷰 · 편집기 자산 매니페스트 ·
G7Core.asset.loadScript · PG 플러그인 SDK 주입이 모두 그것을 경유하게 했다.
사본을 두지 않은 이유는 차집합이 그대로 우회로가 되기 때문이다.

- 게이트는 캐시 검사보다 앞에 둔다 (뒤에 두면 이미 로드된 미신뢰 스크립트가
 캐시 히트로 통과한다)
- NoExternalUrls 순회를 9키로 확대 — 번들 레이아웃 601개 사전 스캔으로
 정당한 외부 URL 0건을 확인한 뒤 넓혔다 (저장 회귀 없음)
- callExternal 은 생성자를 참조 동일성으로 거부하고(별칭 전역 포함),
 프로토타입 경로 세그먼트를 읽기·매핑 키 양쪽에서 차단한다
- PG SDK 는 서비스 SDK 라 자체 호스팅이 불가능하므로 manifest 선언 +
 주입 직전 호스트 확인이 게이트다. tosspayments SDK URL 은 확장자가 없어
 정적 검사에 걸리지 않아 그 런타임 확인이 유일한 게이트다
- 함께 드러난 결함: 동시 loadScript 가 로드 전에 완료 처리되던 문제,
 확장 재로드에서 script 기존재가 CSS 까지 건너뛰던 문제, loadCSS 의
 in-flight 미공유
2026-09-02 21:43:13 +09:00

30 KiB

G7Core 전역 API 레퍼런스 - 고급

버전: engine-v1.4.0+ 관련 문서: g7core-api.md | state-management.md | components.md


목차

  1. 다국어 (G7Core.locale, G7Core.t)
  2. 액션 실행 (G7Core.dispatch)
  3. 컴포넌트 이벤트 (G7Core.componentEvent)
  4. 이벤트 생성 헬퍼
  5. 컴포넌트 렌더링 헬퍼
  6. 인증 및 API
  7. 반응형 및 전환 상태
  8. 타입 정의

다국어 (G7Core.locale, G7Core.t)

로케일 관리 및 번역 API입니다.

G7Core.locale API

메서드 설명 반환값
current() 현재 로케일 반환 string (예: 'ko', 'en')
supported() 지원 로케일 목록 string[]
change(locale) 로케일 변경 Promise<void>

G7Core.t() 번역 함수

G7Core.t(key: string, params?: Record<string, string | number>): string
파라미터 타입 설명
key string 번역 키 (예: 'common.confirm')
params object 번역 파라미터 (선택)

사용 예시

// 로케일 조회
const currentLocale = G7Core.locale.current();  // 'ko'
const supportedLocales = G7Core.locale.supported();  // ['ko', 'en']

// 로케일 변경
await G7Core.locale.change('en');

// 번역 함수 사용
const text = G7Core.t('common.confirm');  // '확인'

// 파라미터 치환
const message = G7Core.t('admin.users.pagination_info', {
  from: 1,
  to: 10,
  total: 100
});
// "1-10 / 총 100건"

컴포넌트에서 t 함수 선언 패턴

상세 내용: components.md

// 컴포넌트 파일 상단에 선언
const t = (key: string, params?: Record<string, string | number>) =>
  (window as any).G7Core?.t?.(key, params) ?? key;

// 사용
<Button>{t('common.confirm')}</Button>
<Span>{t('admin.users.count', { count: 10 })}</Span>

액션 실행 (G7Core.dispatch)

액션을 프로그래밍 방식으로 실행하는 API입니다.

함수 시그니처

G7Core.dispatch(action: ActionConfig): Promise<ActionResult>

ActionConfig 구조

interface ActionConfig {
  handler: string;           // 핸들러 이름 (예: 'api', 'navigate', 'toast')
  params?: Record<string, any>;  // 핸들러 파라미터
  target?: string;           // 대상 ID (모달 등)
}

ActionResult 구조

interface ActionResult {
  success: boolean;
  data?: any;
  error?: Error;
}

사용 예시

// 네비게이션
await G7Core.dispatch({
  handler: 'navigate',
  params: { path: '/users' }
});

// API 호출
const result = await G7Core.dispatch({
  handler: 'api',
  params: {
    method: 'POST',
    endpoint: '/api/users',
    body: { name: 'John' }
  }
});

// 모달 열기
await G7Core.dispatch({
  handler: 'openModal',
  target: 'confirm_modal'
});

// 토스트 표시
await G7Core.dispatch({
  handler: 'toast',
  params: {
    message: '저장되었습니다',
    type: 'success'
  }
});

// 상태 설정
await G7Core.dispatch({
  handler: 'setState',
  params: {
    target: 'global',
    sidebarOpen: true
  }
});

컴포넌트 이벤트 (G7Core.componentEvent)

컴포넌트 간 통신을 위한 이벤트 시스템입니다.

API 목록

메서드 설명 반환값
on(eventName, callback) 이벤트 구독 구독 해제 함수
emit(eventName, data?) 이벤트 발생 Promise<any[]>
off(eventName) 특정 이벤트 리스너 제거 void
clear() 모든 리스너 제거 void

사용 예시

// 이벤트 구독
const unsubscribe = G7Core.componentEvent.on('fileSelected', (data) => {
  console.log('파일 선택됨:', data.file);
});

// 이벤트 발생
await G7Core.componentEvent.emit('fileSelected', { file: selectedFile });

// 이벤트 발생 + 결과 수집
const results = await G7Core.componentEvent.emit('triggerUpload:logo_uploader');
// results: 모든 리스너의 반환값 배열

// 구독 해제
unsubscribe();

// 특정 이벤트의 모든 리스너 제거
G7Core.componentEvent.off('fileSelected');

// 모든 리스너 제거
G7Core.componentEvent.clear();

이벤트 네이밍 규칙

패턴 설명 예시
{action} 단순 액션 fileSelected, dataLoaded
{action}:{target} 특정 대상 지정 triggerUpload:logo_uploader
{component}.{action} 컴포넌트별 이벤트 sidebar.toggle, modal.close

사용 사례

FileUploader 트리거

// 파일 업로더 컴포넌트에서 이벤트 구독
useEffect(() => {
  const unsubscribe = G7Core.componentEvent.on(
    `triggerUpload:${uploaderId}`,
    async () => {
      inputRef.current?.click();
    }
  );
  return unsubscribe;
}, [uploaderId]);

// 다른 컴포넌트에서 업로드 트리거
G7Core.componentEvent.emit('triggerUpload:logo_uploader');

데이터 동기화

// 데이터 변경 알림
G7Core.componentEvent.emit('dataChanged', {
  source: 'userForm',
  data: updatedUser
});

// 다른 컴포넌트에서 수신
G7Core.componentEvent.on('dataChanged', ({ source, data }) => {
  if (source === 'userForm') {
    refreshUserList();
  }
});

이벤트 생성 헬퍼

ActionDispatcher와 호환되는 이벤트 객체를 생성하는 헬퍼 함수들입니다.

API 목록

함수 설명 반환값
createChangeEvent(value, name?) change 이벤트 생성 ChangeEvent
createClickEvent(data?) click 이벤트 생성 ClickEvent
createSubmitEvent(formData?) submit 이벤트 생성 SubmitEvent
createKeyboardEvent(key, modifiers?) keyboard 이벤트 생성 KeyboardEvent

사용 예시

// Change 이벤트 생성
const changeEvent = G7Core.createChangeEvent('newValue', 'fieldName');
// { target: { value: 'newValue', name: 'fieldName' } }

// Click 이벤트 생성
const clickEvent = G7Core.createClickEvent({ itemId: 123 });
// { data: { itemId: 123 } }

// Submit 이벤트 생성
const submitEvent = G7Core.createSubmitEvent({
  name: 'John',
  email: 'john@example.com'
});
// { formData: { name: 'John', email: 'john@example.com' } }

// Keyboard 이벤트 생성
const keyEvent = G7Core.createKeyboardEvent('Enter', { ctrlKey: true });
// { key: 'Enter', ctrlKey: true, shiftKey: false, altKey: false }

사용 사례

컴포넌트에서 ActionDispatcher 핸들러를 직접 호출할 때 사용합니다:

// 컴포넌트에서 프로그래밍 방식으로 change 이벤트 발생
const handleCustomChange = (newValue: string) => {
  const event = G7Core.createChangeEvent(newValue, 'customField');
  onChange?.(event);  // props로 전달된 onChange 호출
};

// Submit 이벤트와 함께 액션 실행
const handleFormSubmit = async () => {
  const submitEvent = G7Core.createSubmitEvent(formData);
  await G7Core.dispatch({
    handler: 'api',
    params: {
      method: 'POST',
      endpoint: '/api/submit',
      body: submitEvent.formData
    }
  });
};

컴포넌트 렌더링 헬퍼

G7Core.renderItemChildren

반복 렌더링 컴포넌트(CardGrid, DataGrid 등)에서 각 아이템의 자식 컴포넌트를 렌더링하는 헬퍼입니다.

G7Core.renderItemChildren(
  children: ComponentDef[],
  itemContext: Record<string, any>,
  componentMap: Record<string, React.ComponentType>,
  keyPrefix?: string,
  options?: RenderOptions
): React.ReactNode[]

파라미터

파라미터 타입 설명
children array 렌더링할 자식 컴포넌트 정의 배열
itemContext object 현재 아이템 컨텍스트 (row, item 등)
componentMap object 컴포넌트 이름 → 컴포넌트 매핑
keyPrefix string React key 접두사 (선택)
options object 추가 옵션 (선택)

사용 예시

// CardGrid 컴포넌트에서 사용
const CardGrid: React.FC<Props> = ({ items, cellChildren }) => {
  const componentMap = G7Core.getComponentMap();

  return (
    <Div className="grid">
      {items.map((item, index) => (
        <Div key={item.id || index}>
          {G7Core.renderItemChildren(
            cellChildren,
            { row: item, index },
            componentMap,
            `card_${index}`
          )}
        </Div>
      ))}
    </Div>
  );
};

G7Core.getComponentMap

전체 컴포넌트 맵을 반환합니다:

const componentMap = G7Core.getComponentMap();
// { Button: ButtonComponent, Input: InputComponent, ... }

G7Core.renderComponentLayout

engine-v1.12.0+ 추가

component_layout 정의를 받아서 React 요소로 렌더링합니다. RichSelect 등에서 커스텀 아이템 렌더링 시 사용합니다.

G7Core.renderComponentLayout(
  layoutDefs: ComponentDef[] | undefined,
  itemContext: Record<string, any>,
  keyPrefix?: string
): React.ReactNode

파라미터

파라미터 타입 설명
layoutDefs array component_layout 정의 배열
itemContext object 항목별 컨텍스트 (item, index, isSelected 등)
keyPrefix string React key prefix (선택)

사용 예시

// RichSelect 컴포넌트에서 사용
const RichSelect: React.FC<Props> = ({ options, __componentLayoutDefs }) => {
  return (
    <Div>
      {options.map((option, index) => (
        <Div key={option.value}>
          {G7Core.renderComponentLayout(
            __componentLayoutDefs?.item,
            { item: option, index, isSelected: selectedValue === option.value },
            `item-${index}`
          )}
        </Div>
      ))}
    </Div>
  );
};

G7Core.renderExpandContent

engine-v1.12.0+ 추가

DataGrid, CardGrid 등에서 확장 행(expandChildren) 콘텐츠를 렌더링하는 헬퍼입니다. 60줄 이상의 복잡한 로직을 10줄 이하로 간소화합니다.

G7Core.renderExpandContent(config: {
  children: ComponentDef[];
  row: any;
  expandContext?: Record<string, any>;
  componentContext?: { state?: Record<string, any>; setState?: Function };
  componentMap?: Record<string, React.ComponentType>;
  keyPrefix: string;
}): React.ReactNode

파라미터

파라미터 타입 설명
children array 렌더링할 expandChildren 컴포넌트 정의
row object 현재 행 데이터
expandContext object 확장 컨텍스트 (바인딩 표현식 자동 평가)
componentContext object 부모 컴포넌트 컨텍스트 (state, setState)
componentMap object 컴포넌트 맵 (없으면 자동 조회)
keyPrefix string React key prefix

동작 원리

  1. 전역 상태 가져오기
  2. componentContext.state를 _local에 자동 병합
  3. expandContext 바인딩 표현식 자동 평가
  4. 최종 컨텍스트 구성 (row, item, $item, _local, _global, _computed)
  5. 렌더링

사용 예시

// DataGrid에서 확장 행 렌더링 (Before: 60줄+ → After: 10줄)
const renderExpandedContent = useCallback((row: any) => {
  if (expandedRowRender) return expandedRowRender(row);
  if (expandChildren && expandChildren.length > 0) {
    return G7Core.renderExpandContent({
      children: expandChildren,
      row,
      expandContext,
      componentContext: __componentContext,
      keyPrefix: `expand-${row[idField]}`,
    });
  }
  return null;
}, [expandChildren, expandContext, __componentContext, expandedRowRender, idField]);

인증 및 API

G7Core.AuthManager

인증 상태를 관리하는 클래스입니다.

상세 내용: auth-system.md

// 현재 사용자 정보
const user = G7Core.AuthManager.getUser();

// 로그인 상태 확인
const isLoggedIn = G7Core.AuthManager.isAuthenticated();

// 토큰 조회
const token = G7Core.AuthManager.getToken();

G7Core.api

API 클라이언트 인스턴스입니다.

// GET 요청
const users = await G7Core.api.get('/api/users');

// POST 요청
const result = await G7Core.api.post('/api/users', {
  name: 'John',
  email: 'john@example.com'
});

// PUT 요청
await G7Core.api.put('/api/users/1', { name: 'Jane' });

// DELETE 요청
await G7Core.api.delete('/api/users/1');

WebSocket (G7Core.websocket)

실시간 데이터 통신을 위한 WebSocket 관리 API입니다.

API 목록

메서드 설명 반환값
subscribe(channel, event, callback, options?) 채널/이벤트 구독 구독 키 (string)
unsubscribe(subscriptionKey) 구독 해제 void
leaveChannel(channel) 채널 떠나기 void
disconnect() 연결 종료 void
isInitialized() 초기화 상태 확인 boolean
getSubscriptionCount() 현재 구독 수 number

사용 예시

// 채널 구독
const subscriptionKey = G7Core.websocket.subscribe(
  'admin.dashboard',      // 채널명
  'stats.updated',        // 이벤트명
  (data) => {             // 콜백
    console.log('통계 업데이트:', data);
  }
);

// 옵션과 함께 구독
const key = G7Core.websocket.subscribe(
  'private-user.123',
  'notification',
  handleNotification,
  { presence: true }
);

// 구독 해제
G7Core.websocket.unsubscribe(subscriptionKey);

// 채널 떠나기 (해당 채널의 모든 구독 해제)
G7Core.websocket.leaveChannel('admin.dashboard');

// 연결 종료
G7Core.websocket.disconnect();

// 상태 확인
if (G7Core.websocket.isInitialized()) {
  console.log('구독 수:', G7Core.websocket.getSubscriptionCount());
}

컴포넌트에서 사용 패턴

useEffect(() => {
  const key = G7Core.websocket.subscribe(
    'admin.orders',
    'order.created',
    (order) => {
      // 새 주문 처리
      refreshOrderList();
    }
  );

  return () => {
    G7Core.websocket.unsubscribe(key);
  };
}, []);

반응형 및 전환 상태

G7Core.ResponsiveManager

반응형 상태를 관리합니다.

상세 내용: responsive-layout.md

// 현재 브레이크포인트 조회
const breakpoint = G7Core.ResponsiveManager.getCurrentBreakpoint();
// 'sm', 'md', 'lg', 'xl', '2xl'

// 모바일 여부
const isMobile = G7Core.ResponsiveManager.isMobile();

G7Core.useResponsive (React Hook)

const { breakpoint, isMobile, isTablet, isDesktop } = G7Core.useResponsive();

G7Core.TransitionManager

페이지 전환 상태를 관리합니다.

// 전환 중 여부
const isPending = G7Core.TransitionManager.getIsPending();

// 상태 변경 구독
const unsubscribe = G7Core.TransitionManager.subscribe((isPending) => {
  console.log('전환 상태:', isPending);
});

G7Core.useTransitionState (React Hook)

const { isPending } = G7Core.useTransitionState();

G7Core.updateQueryParams

같은 페이지 내에서 URL 쿼리 파라미터만 변경하면서 컴포넌트 리마운트 없이 데이터소스를 refetch 한다. 일반적으로 navigate 핸들러의 replace: true 옵션 사용 시 엔진이 자동 호출하므로 직접 호출은 드물다.

G7Core.updateQueryParams(
  newPath: string,
  options?: { transitionOverlayTarget?: string }
): Promise<void>
파라미터 타입 설명
newPath string 새 경로 (쿼리스트링 포함). 예: /admin/products?status=active&page=2
options.transitionOverlayTarget string transition_overlay.target 동적 override (engine-v1.36.0+). 탭 속 서브 탭 등 부분 영역만 spinner 표시

동작:

  1. window.history.replaceState 로 URL 갱신 (페이지 리로드 없음)
  2. auto_fetch: true 인 데이터소스 refetch (websocket 제외)
  3. transition_overlay 가 정의되어 있고 blocking 또는 wait_for 일치 progressive 데이터소스가 1개 이상이면 오버레이 자동 표시 (engine-v1.35.0+)
  4. fetch 완료 시 updateTemplateData 호출 + hideTransitionOverlay
  5. computed 재계산

사용 예시:

// 일반: 쿼리 파라미터 변경 + 데이터 refetch
await G7Core.updateQueryParams('/admin/products?status=active&page=2');

// 서브 탭 spinner target 동적 override
await G7Core.updateQueryParams(
  '/admin/settings?tab=notification&channel=email',
  { transitionOverlayTarget: 'notif_channel_content' }
);

관련:


React Hooks

G7Core.useControllableState

engine-v1.12.0+ 추가

Controlled/Uncontrolled 컴포넌트 패턴을 단순화하는 훅입니다. 외부 value prop과 내부 상태를 자동으로 동기화합니다.

const [value, setValue] = G7Core.useControllableState<T>(
  controlledValue,  // T | undefined - 외부 controlled value (선택)
  defaultValue,     // T - 초기값 (uncontrolled일 때 사용)
  onChange?,        // (value: T) => void - 값 변경 시 콜백 (선택)
  options?          // { isEqual?: (a: T, b: T) => boolean } - 옵션 (선택)
);

파라미터

파라미터 타입 설명
controlledValue T | undefined 외부 controlled value (있으면 controlled 모드)
defaultValue T 초기값 (uncontrolled 모드에서 사용)
onChange (value: T) => void 값 변경 시 호출될 콜백 (선택)
options object 추가 옵션 (선택)
options.isEqual (a: T, b: T) => boolean 커스텀 비교 함수 (기본: ===)

반환값

인덱스 타입 설명
[0] T 현재 값 (controlled 또는 uncontrolled)
[1] (value: T | ((prev: T) => T)) => void 값 설정 함수 (함수형 업데이트 지원)

동작 원리

  • Controlled 모드: controlledValue가 undefined가 아니면, 외부 값을 그대로 사용
  • Uncontrolled 모드: controlledValue가 undefined면, 내부 상태 사용
  • 자동 동기화: 외부 controlledValue가 변경되면 내부 상태도 동기화
  • onChange 호출: setValue 호출 시 onChange 콜백 자동 호출
  • 함수형 업데이트: setValue(prev => newValue) 형태 지원

사용 예시

// DataGrid에서 정렬 상태 관리
const DataGrid: React.FC<Props> = ({
  sortKey: externalSortKey,
  sortOrder: externalSortOrder,
  onSort,
  ...props
}) => {
  const [sortKey, setSortKey] = G7Core.useControllableState(
    externalSortKey,
    null,
    (key) => onSort?.(key, sortOrder)
  );

  const [sortOrder, setSortOrder] = G7Core.useControllableState(
    externalSortOrder,
    'asc',
    (order) => onSort?.(sortKey, order)
  );

  // 함수형 업데이트 예시
  const toggleSortOrder = () => {
    setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
  };

  // ...
};
// PermissionTree에서 선택된 권한 관리 (배열 비교 함수 사용)
const PermissionTree: React.FC<Props> = ({ value, onChange }) => {
  // 배열 비교 함수 정의
  const areArraysEqual = (a: number[], b: number[]): boolean => {
    if (a.length !== b.length) return false;
    const sortedA = [...a].sort((x, y) => x - y);
    const sortedB = [...b].sort((x, y) => x - y);
    return sortedA.every((val, idx) => val === sortedB[idx]);
  };

  const [selectedIds, setSelectedIds] = G7Core.useControllableState<number[]>(
    value,
    [],
    onChange,
    { isEqual: areArraysEqual }
  );

  // 함수형 업데이트로 선택 토글
  const toggleSelection = (id: number) => {
    setSelectedIds(prev =>
      prev.includes(id)
        ? prev.filter(x => x !== id)
        : [...prev, id]
    );
  };

  // ...
};

G7Core에서 훅 사용 패턴

컴포넌트에서 G7Core의 훅을 사용할 때 타입 안전성을 위한 패턴:

// G7Core 전역 객체에서 훅 가져오기
const G7Core = (window as any).G7Core;

// 타입 캐스팅으로 훅 정의
const useControllableState = G7Core?.useControllableState as
  | (<T>(
      controlledValue: T | undefined,
      defaultValue: T,
      onChange?: (value: T) => void,
      options?: { isEqual?: (a: T, b: T) => boolean }
    ) => [T, (value: T | ((prev: T) => T)) => void])
  | undefined;

// 폴백 로직과 함께 사용
const [state, setState] = useControllableState
  ? useControllableState<MyType>(externalValue, defaultValue, handleChange)
  : useState<MyType>(defaultValue);

G7Core.shallowArrayEqual

배열의 얕은 비교를 수행합니다. 배열 길이와 각 인덱스의 요소를 ===로 비교합니다.

G7Core.shallowArrayEqual<T>(a: T[], b: T[]): boolean

동작 원리

  1. 두 배열이 같은 참조면 true 반환
  2. 배열 길이가 다르면 false 반환
  3. 각 인덱스의 요소를 ===로 비교 (얕은 비교)

사용 예시

// 기본 타입 배열
G7Core.shallowArrayEqual([1, 2, 3], [1, 2, 3]);  // true
G7Core.shallowArrayEqual([1, 2], [1, 2, 3]);     // false

// 객체 배열 (참조 비교)
const obj = { id: 1 };
G7Core.shallowArrayEqual([obj], [obj]);          // true (같은 참조)
G7Core.shallowArrayEqual([{ id: 1 }], [{ id: 1 }]); // false (다른 참조)

// useControllableState와 함께 사용
const [selectedIds, setSelectedIds] = G7Core.useControllableState<number[]>(
  value,
  [],
  onChange,
  { isEqual: G7Core.shallowArrayEqual }
);

주의: 배열 내 객체는 참조로 비교됩니다. 객체 내용까지 비교하려면 커스텀 비교 함수를 작성하세요.

G7Core.shallowObjectEqual

객체의 얕은 비교를 수행합니다. 1단계 속성만 ===로 비교합니다.

G7Core.shallowObjectEqual<T extends Record<string, any>>(a: T, b: T): boolean

동작 원리

  1. 두 객체가 같은 참조면 true 반환
  2. 키 개수가 다르면 false 반환
  3. 각 키의 값을 ===로 비교 (얕은 비교)

사용 예시

// 기본 속성 비교
G7Core.shallowObjectEqual({ a: 1, b: 2 }, { a: 1, b: 2 });  // true
G7Core.shallowObjectEqual({ a: 1 }, { a: 1, b: 2 });        // false (키 개수 다름)
G7Core.shallowObjectEqual({ a: 1, b: 2 }, { a: 1, b: 3 });  // false (값 다름)

// 중첩 객체 (참조 비교)
const nested = { x: 1 };
G7Core.shallowObjectEqual({ data: nested }, { data: nested });  // true (같은 참조)
G7Core.shallowObjectEqual({ data: { x: 1 } }, { data: { x: 1 } }); // false (다른 참조)

// useControllableState와 함께 사용
const [formData, setFormData] = G7Core.useControllableState<FormData>(
  value,
  { name: '', email: '' },
  onChange,
  { isEqual: G7Core.shallowObjectEqual }
);

주의: 중첩 객체는 참조로 비교됩니다. 깊은 비교가 필요하면 커스텀 비교 함수를 작성하세요.


타입 정의

전역 타입 선언

declare global {
  interface Window {
    G7Core: {
      // 상태 관리
      state: {
        get: () => Record<string, any>;
        set: (updates: Record<string, any>) => void;
        update: (updater: (prev: Record<string, any>) => Record<string, any>) => void;
        subscribe: (listener: (state: Record<string, any>) => void) => () => void;
        getDataSource: (id: string) => DataSourceValue | undefined;
      };

      // 토스트
      toast: {
        show: (message: string, options?: ToastOptions) => void;
        success: (message: string, duration?: number) => void;
        error: (message: string, duration?: number) => void;
        warning: (message: string, duration?: number) => void;
        info: (message: string, duration?: number) => void;
      };

      // 모달
      modal: {
        open: (modalId: string) => void;
        close: (modalId?: string) => void;
        closeAll: () => void;
        isOpen: (modalId: string) => boolean;
        getStack: () => string[];
      };

      // 네비게이션
      navigation: {
        isPending: () => boolean;
        onComplete: (callback: () => void) => () => void;
      };

      // 다국어
      locale: {
        current: () => string;
        supported: () => string[];
        change: (locale: string) => Promise<void>;
      };
      t: (key: string, params?: Record<string, string | number>) => string;

      // 확장 자산 URL (engine-v1.62.0+)
      asset: {
        template: (identifier: string, path: string, version?: number | string | null) => string;
        templateDir: (identifier: string, path: string) => string;
        module: (identifier: string, path: string, version?: number | string | null) => string;
        plugin: (identifier: string, path: string, version?: number | string | null) => string;
        convertToCurrentMode: (url: string) => string;
        // 출처 게이트 적용 (engine-v1.64.0+) — 미신뢰 URL 은 reject
        loadScript: (url: string, attrs?: Record<string, string>, options?: Record<string, unknown>) => Promise<void>;
        loadStylesheet: (url: string, attrs?: Record<string, string>, options?: Record<string, unknown>) => Promise<void>;
        // 같은 판정을 직접 물어보는 seam (engine-v1.64.0+)
        isAllowedScriptSrc: (url: string) => boolean;
      };

      // 자산 실패 안내 (engine-v1.62.0+)
      assets: {
        notifyFailure: (failure: AssetFailure) => void;
        clearFailure: (id: string) => void;
        clearAll: () => void;
        getFailures: () => AssetFailure[];
        retryAll: () => Promise<void>;
      };
      // 액션
      dispatch: (action: ActionConfig) => Promise<ActionResult>;

      // 이벤트
      componentEvent: {
        on: (eventName: string, callback: (data?: any) => void | Promise<any>) => () => void;
        emit: (eventName: string, data?: any) => Promise<any[]>;
        off: (eventName: string) => void;
        clear: () => void;
      };

      // 이벤트 헬퍼
      createChangeEvent: (value: any, name?: string) => any;
      createClickEvent: (data?: any) => any;
      createSubmitEvent: (formData?: any) => any;
      createKeyboardEvent: (key: string, modifiers?: KeyModifiers) => any;

      // 렌더링 헬퍼
      renderItemChildren: (children: any[], itemContext: Record<string, any>, componentMap: Record<string, any>, keyPrefix?: string, options?: any) => React.ReactNode[];
      renderComponentLayout: (layoutDefs: any[] | undefined, itemContext: Record<string, any>, keyPrefix?: string) => React.ReactNode;
      renderExpandContent: (config: { children: any[]; row: any; expandContext?: Record<string, any>; componentContext?: any; componentMap?: Record<string, any>; keyPrefix: string }) => React.ReactNode;
      getComponentMap: () => Record<string, React.ComponentType>;

      // 스타일 헬퍼
      style: {
        mergeClasses: (base: string, override: string) => string;
        conditionalClass: (conditions: Record<string, boolean>) => string;
        joinClasses: (...classes: (string | false | undefined)[]) => string;
      };

      // 플러그인 설정
      plugin: {
        getSettings: (pluginId: string) => Record<string, any> | undefined;
        get: (pluginId: string, key: string, defaultValue?: any) => any;
        getAll: () => Record<string, Record<string, any>>;
      };

      // 모듈 설정
      module: {
        getSettings: (moduleId: string) => Record<string, any> | undefined;
        get: (moduleId: string, key: string, defaultValue?: any) => any;
        getAll: () => Record<string, Record<string, any>>;
      };

      // WebSocket
      websocket: {
        subscribe: (channel: string, event: string, callback: (data: any) => void, options?: any) => string;
        unsubscribe: (subscriptionKey: string) => void;
        leaveChannel: (channel: string) => void;
        disconnect: () => void;
        isInitialized: () => boolean;
        getSubscriptionCount: () => number;
      };

      // React Hooks
      useControllableState: <T>(
        controlledValue: T | undefined,
        defaultValue: T,
        onChange?: (value: T) => void,
        options?: { isEqual?: (a: T, b: T) => boolean }
      ) => [T, (value: T | ((prev: T) => T)) => void];
      shallowArrayEqual: <T>(a: T[], b: T[]) => boolean;
      shallowObjectEqual: <T extends Record<string, any>>(a: T, b: T) => boolean;
      useResponsive: () => ResponsiveState;
      useTransitionState: () => TransitionState;
      useTranslation: () => { t: TranslationFunction };

      // 위지윅 편집기
      wysiwyg: {
        isEditMode: () => boolean;
        setEditMode: (layoutName: string, templateId: string) => void;
        clearEditMode: () => void;
        getCurrentLayoutName: () => string | null;
        getCurrentTemplateId: () => string | null;
        isEditModeFromUrl: () => boolean;
        getEditModeUrl: (route: string, templateId: string) => string;
        enterEditMode: (route: string, templateId: string) => void;
        exitEditMode: () => void;
        getVersion: () => string;
        getPhase: () => number;
      };

      // 기타
      AuthManager: AuthManagerType;
      api: ApiClientType;
      ResponsiveManager: ResponsiveManagerType;
      TransitionManager: TransitionManagerType;
    };
  }
}

관련 문서