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.
250 lines
10 KiB
Markdown
250 lines
10 KiB
Markdown
# 액션 핸들러 - 핸들러별 상세 사용법
|
|
|
|
> **메인 문서**: [actions.md](actions.md)
|
|
> **관련 문서**: [actions-g7core-api.md](actions-g7core-api.md) | [layout-json.md](layout-json.md) | [state-management.md](state-management.md)
|
|
|
|
---
|
|
|
|
## TL;DR (5초 요약)
|
|
|
|
```text
|
|
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](actions-handlers-navigation.md) | navigate, navigateBack, openWindow, replaceUrl, **reloadExtensions**, reloadRoutes, refresh | 페이지 이동 및 라우트 관리 |
|
|
| [actions-handlers-state.md](actions-handlers-state.md) | apiCall, setState, setError, refetchDataSource, remount | API 호출 및 상태 관리 |
|
|
| [actions-handlers-ui.md](actions-handlers-ui.md) | login/logout, openModal/closeModal, toast, switch, sequence/parallel, loadScript, callExternal | UI 인터랙션 및 외부 스크립트 |
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
### 네비게이션 핸들러 → [상세 문서](actions-handlers-navigation.md)
|
|
|
|
1. [navigate](actions-handlers-navigation.md#navigate) - 페이지 이동
|
|
2. [navigateBack](actions-handlers-navigation.md#navigateback) - 뒤로 가기
|
|
3. [openWindow](actions-handlers-navigation.md#openwindow) - 새 창/탭에서 열기
|
|
4. [replaceUrl](actions-handlers-navigation.md#replaceurl) - URL만 변경 (refetch 없음)
|
|
5. [reloadExtensions](actions-handlers-navigation.md#reloadextensions) ⭐ NEW (engine-v1.38.0+) - 확장 상태 원자 재동기화
|
|
6. [reloadRoutes](actions-handlers-navigation.md#reloadroutes) (deprecated) - 라우트 재로드
|
|
7. [refresh](actions-handlers-navigation.md#refresh) - 페이지 새로고침
|
|
|
|
### 상태 관리 핸들러 → [상세 문서](actions-handlers-state.md)
|
|
|
|
5. [apiCall](actions-handlers-state.md#apicall) - API 호출
|
|
6. [setState](actions-handlers-state.md#setstate) - 상태 변경
|
|
7. [setError](actions-handlers-state.md#seterror) - 에러 상태 설정
|
|
8. [refetchDataSource](actions-handlers-state.md#refetchdatasource) - 데이터 소스 재조회
|
|
9. [appendDataSource](actions-handlers-state.md#appenddatasource) - 데이터 소스 병합 (무한스크롤)
|
|
10. [remount](actions-handlers-state.md#remount) - 컴포넌트 리마운트
|
|
11. [onSuccess/onError 후속 액션](actions-handlers-state.md#onsuccessonerror-후속-액션)
|
|
12. [API 데이터 바인딩 규칙](actions-handlers-state.md#api-데이터-바인딩-규칙)
|
|
13. [에러 핸들링 시스템](actions-handlers-state.md#에러-핸들링-시스템-errorhandling)
|
|
|
|
### UI 인터랙션 핸들러 → [상세 문서](actions-handlers-ui.md)
|
|
|
|
14. [login / logout](actions-handlers-ui.md#login--logout) - 인증
|
|
- [login 의 반환값 — 2단계 인증이 켜진 사이트](actions-handlers-ui.md#login-의-반환값--2단계-인증이-켜진-사이트)
|
|
- [loginTwoFactor](actions-handlers-ui.md#logintwofactor) - 인증번호 확인
|
|
- [loginTwoFactorResend](actions-handlers-ui.md#logintwofactorresend) - 인증번호 다시 받기
|
|
15. [openModal / closeModal](actions-handlers-ui.md#openmodal--closemodal) - 모달
|
|
16. [showAlert / toast](actions-handlers-ui.md#showalert--toast) - 알림
|
|
17. [confirm (액션 속성)](actions-handlers-ui.md#confirm-액션-속성) - 실행 전 확인 대화상자
|
|
18. [switch](actions-handlers-ui.md#switch) - 조건부 액션
|
|
19. [sequence / parallel](actions-handlers-ui.md#sequence--parallel) - 액션 조합
|
|
20. [reloadTranslations](actions-handlers-ui.md#reloadtranslations) (deprecated) - 다국어 재로드 (extension 라이프사이클은 `reloadExtensions` 사용)
|
|
21. [showErrorPage](actions-handlers-ui.md#showerrorpage) - 에러 페이지
|
|
22. [loadScript](actions-handlers-ui.md#loadscript) ⭐ NEW - 외부 스크립트 로드 (same-origin 경로 또는 선언된 신뢰 호스트만)
|
|
23. [callExternal](actions-handlers-ui.md#callexternal) ⭐ NEW - 외부 라이브러리 호출
|
|
24. [실전 예시](actions-handlers-ui.md#실전-예시)
|
|
|
|
---
|
|
|
|
## 핸들러 빠른 참조
|
|
|
|
### 자주 사용하는 핸들러
|
|
|
|
| 핸들러 | 용도 | 상세 문서 |
|
|
|--------|------|----------|
|
|
| `navigate` | 페이지 이동 | [navigation](actions-handlers-navigation.md#navigate) |
|
|
| `openWindow` | 새 창/탭에서 열기 | [navigation](actions-handlers-navigation.md#openwindow) |
|
|
| `replaceUrl` | URL만 변경 (refetch 없음) | [navigation](actions-handlers-navigation.md#replaceurl) |
|
|
| `apiCall` | API 호출 | [state](actions-handlers-state.md#apicall) |
|
|
| `setState` | 상태 변경 | [state](actions-handlers-state.md#setstate) |
|
|
| `openModal` | 모달 열기 | [ui](actions-handlers-ui.md#openmodal--closemodal) |
|
|
| `closeModal` | 모달 닫기 | [ui](actions-handlers-ui.md#openmodal--closemodal) |
|
|
| `toast` | 토스트 알림 | [ui](actions-handlers-ui.md#showalert--toast) |
|
|
| `sequence` | 순차 실행 | [ui](actions-handlers-ui.md#sequence--parallel) |
|
|
| `suppress` | 에러 전파 방지 (no-op) | [error](layout-json-features-error.md#에러-전파-방지-suppress-핸들러) |
|
|
|
|
### 핸들러별 필수 속성
|
|
|
|
| 핸들러 | 필수 속성 | 선택 속성 |
|
|
|--------|----------|----------|
|
|
| `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` | - | - |
|
|
|
|
---
|
|
|
|
## 모듈 커스텀 핸들러 다국어 처리
|
|
|
|
모듈에서 커스텀 핸들러를 개발할 때, 사용자에게 표시되는 모든 문자열은 반드시 다국어 처리해야 합니다.
|
|
|
|
### 핵심 원칙
|
|
|
|
```text
|
|
필수: 모든 사용자 표시 문자열은 G7Core.t() 사용
|
|
필수: 다국어 키는 moduleIdentifier로 시작
|
|
✅ 필수: 영어 폴백 메시지 제공
|
|
✅ 필수: 파라미터는 {param} 형식 사용
|
|
❌ 금지: 한글 문자열 하드코딩 (toast, 알림 메시지 등)
|
|
예외: 로케일별 콘텐츠 생성용 상수맵은 허용 (예: DETAIL_REF_TRANSLATIONS)
|
|
```
|
|
|
|
### 다국어 키 네이밍 규칙
|
|
|
|
```text
|
|
[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` |
|
|
|
|
### 구현 패턴
|
|
|
|
**기본 패턴**:
|
|
|
|
```typescript
|
|
// ❌ 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.'
|
|
);
|
|
```
|
|
|
|
**파라미터 패턴**:
|
|
|
|
```typescript
|
|
// ❌ 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`):
|
|
|
|
```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."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 예외: 로케일별 콘텐츠 생성용 상수맵
|
|
|
|
로케일별로 다른 콘텐츠를 **생성**해야 하는 경우, 상수맵은 허용됩니다:
|
|
|
|
```typescript
|
|
// ✅ 허용: 로케일별 콘텐츠 생성용 상수맵
|
|
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;
|
|
```
|
|
|
|
### 핸들러 개발 체크리스트
|
|
|
|
```text
|
|
□ 모든 toast 메시지에 G7Core.t() 적용
|
|
□ 모든 확인/알림 모달 텍스트에 G7Core.t() 적용
|
|
□ 영어 폴백 메시지 제공 (G7Core.t?.() ?? 'fallback')
|
|
□ 다국어 파일(ko.json, en.json)에 키 추가
|
|
□ 파라미터는 {param} 형식으로 정의
|
|
□ 로그 메시지는 다국어 처리 불필요 (logger.log, logger.warn 등)
|
|
```
|
|
|
|
---
|
|
|
|
## 증상별 문서 찾기
|
|
|
|
| 증상 | 관련 문서 | 핵심 키워드 |
|
|
|------|----------|------------|
|
|
| 페이지 이동 안 됨 | [navigation](actions-handlers-navigation.md) | navigate, path, replace |
|
|
| API 호출 실패 | [state](actions-handlers-state.md#apicall) | apiCall, auth_required, onError |
|
|
| 상태 변경 안 됨 | [state](actions-handlers-state.md#setstate) | setState, target: global/local/isolated |
|
|
| 모달 안 열림/안 닫힘 | [ui](actions-handlers-ui.md#openmodal--closemodal) | openModal, closeModal, modalStack |
|
|
| 토스트 안 나옴 | [ui](actions-handlers-ui.md#showalert--toast) | toast, params.type |
|
|
| 외부 스크립트 로드 | [ui](actions-handlers-ui.md#loadscript) | loadScript, onLoad, src 출처 게이트 |
|
|
| 조건부 액션 분기 | [ui](actions-handlers-ui.md#switch) | switch, cases, default |
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
- [액션 시스템 기초](actions.md)
|
|
- [G7Core API](actions-g7core-api.md)
|
|
- [상태 관리](state-management.md)
|
|
- [데이터 소스](data-sources.md)
|
|
- [레이아웃 JSON](layout-json.md)
|