Files
Gnuboard7/docs/frontend/g7core-api-advanced.md
T
HeuJung 3945b6f1b3 feat(core,extensions): 구동 에셋 자체 제공 · 자산 실패 폴백 · 운영자 추가 에셋(custom/)
공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다.

브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도
남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데
자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기
하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발
대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다.
런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다.

자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그
실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML
에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다.
편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다.

두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의
custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에
의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다.
확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을
고치면 그 변경을 감지해 재게시까지 예약된다.

FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접
넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로
나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린
스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과
분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠
화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를
함께 뒀다.

동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에
써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
2026-08-27 16:47:14 +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;
        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>;
      };

      // 자산 실패 안내 (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;
    };
  }
}

관련 문서