Files
Gnuboard7/docs/frontend/actions-handlers.md
T
HeuJung 50007d5cc6 fix(auth): 2단계 인증을 켠 사이트의 로그인 흐름 구현
2단계 인증은 7.0.6 에서 서버측이 갖춰졌지만 인증번호를 입력할 화면이 어느 버전에도
없었다. 그래서 그 설정을 켠 사이트는 관리자를 포함한 전원이 로그인할 수 없었다.

원인은 `POST /api/auth/login` 이 조건에 따라 **다른 형태의 200** 을 돌려준다는 것이다.
평소에는 `{token, user}` 지만 2단계 인증이 켜져 있으면 `{two_factor_required,
challenge_id, ...}` 를 돌려준다. 프론트는 앞의 형태만 선언하고 `response.data.user.language`
를 바로 읽었으므로 그 자리에서 TypeError 가 났고, 영문 원문이 로그인 화면에 그대로 노출됐다.
서버는 정상 응답했으므로 서버 로그에는 아무 흔적도 남지 않는다.

이어서 `setToken(undefined)` 가 `localStorage` 에 문자열 `"undefined"` 를 남겼다.
이 값은 truthy 라 이후 모든 요청이 `Bearer undefined` 로 나가 401 이 되고, 사용자에게는
「세션이 만료되었습니다」로 보인다. 관리자 로그인은 한발 더 나가 `null->isAdmin` 으로
500 이 되어, 설정을 되돌릴 수단까지 함께 사라졌다.

## 구현

- 로그인 응답을 판별 유니온(`LoginResult`)으로 표현하고, 형태를 판별한 뒤에 읽는다.
 `ApiClient.setToken` 은 비어 있지 않은 문자열만 저장한다.
- 사용자·관리자 로그인 화면에 인증번호 입력 단계를 추가했다. 같은 카드 안에서 넘어가며
 「인증번호 다시 받기」와 「처음부터」를 제공한다. 관리자 판정은 코드 확인에 성공한 뒤에
 수행하고, 거부할 때는 그 직전에 발급된 토큰을 회수한다.
- 재발송(`login/two-factor/resend`)은 기존 challenge 를 취소하고 새로 발행한다. 유효한
 코드를 여러 개 살려 두면 대입 시도의 표적이 넓어진다.
- 인증번호를 보내지 못하면 401 이 아니라 503 으로 답한다. 자격 증명은 올바른데 401 로
 뭉개면 사용자는 비밀번호를 의심하며 같은 시도를 반복하고, 운영자는 메일 설정이 깨진
 사실을 알 방법이 없다.
- 공개 본인인증 경로(`identity/verify`·`cancel`)가 로그인 목적의 challenge 를 소진하지
 못하도록 403 게이트를 세웠다. 소진되면 그 challenge 로 영영 로그인할 수 없다.
- 로그인 시도 제한 429 응답이 다국어 문구를 싣도록 했다(종전에는 프레임워크 기본 영문).
- 다국어 파라미터에서 파이프 표현식이 평가되지 않아 「유효시간 까지」처럼 값이 빠지던
 문제를 함께 고쳤다. 같은 결함이 문의 목록 화면에도 있었다.

## 이번 점검에서 함께 고친 것

- 계정 잠금(423)·발송 실패(503) 응답이 사용자·관리자 컨트롤러에 동일하게 복제돼 있었고
 그 주석 자신은 "단일 지점에서 만든다" 고 적혀 있었다. 페이로드에 필드가 하나 추가되면
 한쪽만 따라가 같은 실패를 두 화면이 다르게 안내하게 된다 — 트레이트로 통합했다.
- 테스트가 개발자 자신의 사이트 설정을 읽고 있었다. 2단계 인증을 켜 둔 환경에서는 로그인
 성공을 전제한 테스트가 503 으로 깨지는데 실패 메시지가 원인을 가리키지도 않는다.
 같은 결함군을 위해 이미 존재하던 단일 지점에 그 축을 추가했다.

## 버전

코어 7.0.11 · sirsoft-basic 1.1.4 · sirsoft-admin_basic 1.0.9 ·
번들 일본어팩 3종 · 템플릿 엔진 engine-v1.65.0.
2026-09-07 17:08:14 +09:00

10 KiB

액션 핸들러 - 핸들러별 상세 사용법

메인 문서: actions.md 관련 문서: actions-g7core-api.md | layout-json.md | state-management.md


TL;DR (5초 요약)

1. navigate: 페이지 이동 (path, query, mergeQuery 옵션)
2. apiCall: API 호출 (method, endpoint, body, onSuccess/onError)
3. setState: 상태 변경 (_global/_local/_isolated 경로, target 옵션)
4. openModal/closeModal: 모달 열기/닫기
5. sequence/parallel: 여러 액션 순차/병렬 실행

하위 문서 안내

이 문서는 규모가 커서 카테고리별로 분할되었습니다. 아래 링크를 통해 각 핸들러에 대한 상세 내용을 확인하세요.

하위 문서 주요 핸들러 설명
actions-handlers-navigation.md navigate, navigateBack, openWindow, replaceUrl, reloadExtensions, reloadRoutes, refresh 페이지 이동 및 라우트 관리
actions-handlers-state.md apiCall, setState, setError, refetchDataSource, remount API 호출 및 상태 관리
actions-handlers-ui.md login/logout, openModal/closeModal, toast, switch, sequence/parallel, loadScript, callExternal UI 인터랙션 및 외부 스크립트

목차

네비게이션 핸들러 → 상세 문서

  1. navigate - 페이지 이동
  2. navigateBack - 뒤로 가기
  3. openWindow - 새 창/탭에서 열기
  4. replaceUrl - URL만 변경 (refetch 없음)
  5. reloadExtensions ⭐ NEW (engine-v1.38.0+) - 확장 상태 원자 재동기화
  6. reloadRoutes (deprecated) - 라우트 재로드
  7. refresh - 페이지 새로고침

상태 관리 핸들러 → 상세 문서

  1. apiCall - API 호출
  2. setState - 상태 변경
  3. setError - 에러 상태 설정
  4. refetchDataSource - 데이터 소스 재조회
  5. appendDataSource - 데이터 소스 병합 (무한스크롤)
  6. remount - 컴포넌트 리마운트
  7. onSuccess/onError 후속 액션
  8. API 데이터 바인딩 규칙
  9. 에러 핸들링 시스템

UI 인터랙션 핸들러 → 상세 문서

  1. login / logout - 인증
  2. openModal / closeModal - 모달
  3. showAlert / toast - 알림
  4. confirm (액션 속성) - 실행 전 확인 대화상자
  5. switch - 조건부 액션
  6. sequence / parallel - 액션 조합
  7. reloadTranslations (deprecated) - 다국어 재로드 (extension 라이프사이클은 reloadExtensions 사용)
  8. showErrorPage - 에러 페이지
  9. loadScript ⭐ NEW - 외부 스크립트 로드 (same-origin 경로 또는 선언된 신뢰 호스트만)
  10. callExternal ⭐ NEW - 외부 라이브러리 호출
  11. 실전 예시

핸들러 빠른 참조

자주 사용하는 핸들러

핸들러 용도 상세 문서
navigate 페이지 이동 navigation
openWindow 새 창/탭에서 열기 navigation
replaceUrl URL만 변경 (refetch 없음) navigation
apiCall API 호출 state
setState 상태 변경 state
openModal 모달 열기 ui
closeModal 모달 닫기 ui
toast 토스트 알림 ui
sequence 순차 실행 ui
suppress 에러 전파 방지 (no-op) error

핸들러별 필수 속성

핸들러 필수 속성 선택 속성
navigate params.path params.query, params.mergeQuery, params.replace, params.fallback (engine-v1.40.0+, 미등록 경로 fallback, 기본 openWindow)
openWindow params.path -
replaceUrl - params.path, params.query, params.mergeQuery
apiCall target params.method, params.body, params.contentType, auth_required, onSuccess, onError
setState params.* params.target (global/local/isolated)
openModal target (모달 ID) -
toast params.message params.type, params.duration
switch cases params.value
sequence actions -
parallel actions -
suppress - -

모듈 커스텀 핸들러 다국어 처리

모듈에서 커스텀 핸들러를 개발할 때, 사용자에게 표시되는 모든 문자열은 반드시 다국어 처리해야 합니다.

핵심 원칙

필수: 모든 사용자 표시 문자열은 G7Core.t() 사용
필수: 다국어 키는 moduleIdentifier로 시작
✅ 필수: 영어 폴백 메시지 제공
✅ 필수: 파라미터는 {param} 형식 사용
❌ 금지: 한글 문자열 하드코딩 (toast, 알림 메시지 등)
예외: 로케일별 콘텐츠 생성용 상수맵은 허용 (예: DETAIL_REF_TRANSLATIONS)

다국어 키 네이밍 규칙

[moduleId].admin.[section].handler.[message_key]

예시:

  • sirsoft-ecommerce.admin.product.handler.category_max_5
  • sirsoft-ecommerce.admin.product.handler.options_generated

키 접미사 규칙:

접미사 용도 예시
_success 성공 메시지 copy_success
_error, _failed 오류 메시지 copy_error, upload_failed
_required 필수 입력 안내 name_required
_max_N 최대 개수 제한 category_max_5

구현 패턴

기본 패턴:

// ❌ DON'T: 하드코딩
G7Core.toast?.warning?.('최대 5개까지 선택 가능합니다.');

// ✅ DO: G7Core.t() + 영어 폴백
G7Core.toast?.warning?.(
    G7Core.t?.('sirsoft-ecommerce.admin.product.handler.category_max_5')
    ?? 'You can select up to 5 categories.'
);

파라미터 패턴:

// ❌ DON'T: 템플릿 리터럴만 사용
G7Core.toast?.success?.(`${count}개의 옵션이 생성되었습니다.`);

// ✅ DO: 파라미터 전달 + 폴백
G7Core.toast?.success?.(
    G7Core.t?.('sirsoft-ecommerce.admin.product.handler.options_generated', { count })
    ?? `${count} options have been generated.`
);

다국어 파일 등록 (resources/lang/ko.json, en.json):

// ko.json
{
  "admin": {
    "product": {
      "handler": {
        "category_max_5": "최대 5개까지 선택 가능합니다.",
        "options_generated": "{count}개의 옵션이 생성되었습니다."
      }
    }
  }
}

// en.json
{
  "admin": {
    "product": {
      "handler": {
        "category_max_5": "You can select up to 5 categories.",
        "options_generated": "{count} options have been generated."
      }
    }
  }
}

예외: 로케일별 콘텐츠 생성용 상수맵

로케일별로 다른 콘텐츠를 생성해야 하는 경우, 상수맵은 허용됩니다:

// ✅ 허용: 로케일별 콘텐츠 생성용 상수맵
const DETAIL_REF_TRANSLATIONS: Record<string, Record<string, string>> = {
    ko: { goods_name: '상품명', model_name: '모델명' },
    en: { goods_name: 'Product Name', model_name: 'Model Name' },
};

// 사용: 특정 로케일의 콘텐츠 생성
const text = DETAIL_REF_TRANSLATIONS[locale]?.[key] ?? key;

핸들러 개발 체크리스트

□ 모든 toast 메시지에 G7Core.t() 적용
□ 모든 확인/알림 모달 텍스트에 G7Core.t() 적용
□ 영어 폴백 메시지 제공 (G7Core.t?.() ?? 'fallback')
□ 다국어 파일(ko.json, en.json)에 키 추가
□ 파라미터는 {param} 형식으로 정의
□ 로그 메시지는 다국어 처리 불필요 (logger.log, logger.warn 등)

증상별 문서 찾기

증상 관련 문서 핵심 키워드
페이지 이동 안 됨 navigation navigate, path, replace
API 호출 실패 state apiCall, auth_required, onError
상태 변경 안 됨 state setState, target: global/local/isolated
모달 안 열림/안 닫힘 ui openModal, closeModal, modalStack
토스트 안 나옴 ui toast, params.type
외부 스크립트 로드 ui loadScript, onLoad, src 출처 게이트
조건부 액션 분기 ui switch, cases, default

관련 문서