첫 커밋: 로컬 프로젝트 업로드
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# dev 브랜치 충돌 대응 정책
|
||||
|
||||
## 현재 상태 점검 기준
|
||||
- `git status -sb` 기준으로 `unmerged paths`가 없으면 파일 단위 충돌은 없는 상태입니다.
|
||||
- `non-fast-forward` push 거절은 로컬/원격 히스토리 분기(diverged) 상태로 간주합니다.
|
||||
- 원격 확인 불가(DNS/네트워크 장애) 시, 로컬 기준 상태를 우선 공유하고 원격 fetch 가능 시점에 재확인합니다.
|
||||
|
||||
## 기본 원칙
|
||||
1. `dev` 반영 전 최신 원격 기준선 확보
|
||||
2. 충돌 해결은 기능 회귀 방지 우선
|
||||
3. 해결 후 CI 강제 검사 통과 확인
|
||||
|
||||
## CI 강제 검사 정책
|
||||
- `.gitea/workflows/code_check.yml`는 아래 이벤트에서 항상 실행됩니다.
|
||||
- `push` to `dev`
|
||||
- `pull_request` targeting `dev`
|
||||
- `workflow_dispatch` (수동 실행)
|
||||
- 수동 실행 입력으로 검사 항목을 끄는 방식은 사용하지 않습니다.
|
||||
- `backend-tests`, `userfront-tests`는 `lint` 결과와 무관하게 실행 시도하여 전체 실패 지점을 한 번에 확인합니다.
|
||||
|
||||
## 표준 절차
|
||||
1. 원격 최신화
|
||||
```bash
|
||||
git fetch origin dev
|
||||
git status -sb
|
||||
git rev-list --left-right --count origin/dev...dev
|
||||
```
|
||||
|
||||
2. 분기 상태별 처리
|
||||
- 로컬만 앞섬 (`0 N`): `git push origin dev`
|
||||
- 원격만 앞섬 (`N 0`): `git rebase origin/dev` 후 push
|
||||
- 상호 분기 (`N M`): `git rebase origin/dev`로 정렬 후 충돌 해결
|
||||
|
||||
3. 충돌 해결 후 검증
|
||||
```bash
|
||||
make validate-auth-config
|
||||
make verify-auth-config
|
||||
```
|
||||
|
||||
## 우선순위 정책 (이번 범위 #274 / #276)
|
||||
1. OIDC 리다이렉트/쿼리 전달 회귀 방지 로직 유지
|
||||
2. `Makefile` 기반 인증 설정 생성/검증 경로 유지
|
||||
3. `compose.ory.yaml`의 callback/allowed_return_urls env 연동 유지
|
||||
4. `.env` 값 형식 안정성 유지 (same-line 주석 금지)
|
||||
|
||||
## 주의 사항
|
||||
- `dev` 공유 브랜치에서는 `force push`를 사용하지 않습니다.
|
||||
- `.env`에서 `KEY=value #comment` 형태는 금지합니다. (URL 끝 공백으로 Hydra/Kratos 기동 실패 가능)
|
||||
- callback URL 끝 `/`는 `make validate-auth-config`에서 실패 처리됩니다.
|
||||
|
||||
## 관련 문서
|
||||
- `docs/oidc_redirect_mapping_validation_policy.md`
|
||||
- `README.md`
|
||||
77
baron-sso/docs/trouble-shooting/hydra-rp-consent-try.md
Normal file
77
baron-sso/docs/trouble-shooting/hydra-rp-consent-try.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Hydra RP Consent 시도 기록
|
||||
|
||||
## 목표
|
||||
- 샘플 RP(`52a597f0-5b06-4fcb-b804-93e88a56a75a`)와 사용자(`b24051@hanmaceng.co.kr`, Kratos ID: `22607c1b-bfbf-4a90-9505-36b348472e7a`) 사이에 Hydra consent 세션을 생성.
|
||||
|
||||
## 시도한 방법과 실패 원인
|
||||
|
||||
### 1) hydra 컨테이너 내부에서 `sh` 실행 후 스크립트 수행
|
||||
- 시도: `docker exec -i ory_hydra sh -lc '...'`
|
||||
- 실패: `sh`가 존재하지 않음 (Hydra 컨테이너가 distroless 이미지).
|
||||
- 원인: 쉘 바이너리 미포함.
|
||||
|
||||
### 2) `curlimages/curl` 컨테이너를 hydra 네트워크에 붙여서 consent 생성 흐름 수행
|
||||
- 시도 흐름:
|
||||
1. `/oauth2/auth` 호출로 `login_challenge` 획득
|
||||
2. Admin API로 `login_challenge` 수락
|
||||
3. `login_accept.redirect_to`(보통 `http://127.0.0.1:3000/...`)로 이동해 `consent_challenge` 추출
|
||||
- 실패: `login_accept.redirect_to`가 **consent app(127.0.0.1:3000)**로 향하는데, 해당 서비스가 떠 있지 않아 접근 불가.
|
||||
- 원인: consent app가 실행 중이 아니라 127.0.0.1:3000 접속 실패.
|
||||
|
||||
### 3) `login_accept.redirect_to`를 그대로 호출해 Location 헤더에서 consent_challenge 추출 시도
|
||||
- 실패: 위와 동일하게 consent app 경로를 직접 호출하게 되어 연결 실패.
|
||||
- 원인: consent app 미기동.
|
||||
|
||||
### 4) DCR(동적 클라이언트 등록) 시 metadata 포함 시도
|
||||
- 실패: `invalid_client_metadata` (DCR에서는 `metadata`를 설정할 수 없음)
|
||||
- 원인: Hydra DCR 정책 제한.
|
||||
|
||||
## 요약
|
||||
- **핵심 실패 원인**은 consent app(로그인/동의 UI)이 실행 중이 아니라서 redirect_to를 따라가면 접속이 불가능한 점.
|
||||
- distroless 이미지로 인해 `docker exec`로 쉘 스크립트를 바로 실행할 수 없음.
|
||||
|
||||
## 다음 시도(새 방식)
|
||||
- consent app로 직접 이동하지 않고, **Hydra public endpoint**에서 `login_verifier`를 이용해 `consent_challenge`를 추출한 뒤 Admin API로 수락.
|
||||
- 흐름:
|
||||
1. `/oauth2/auth` → `login_challenge`
|
||||
2. `/oauth2/auth/requests/login/accept` → `login_verifier`
|
||||
3. `/oauth2/auth?client_id=...&login_verifier=...` 호출 → Location 헤더에서 `consent_challenge` 추출
|
||||
4. `/oauth2/auth/requests/consent/accept` 호출
|
||||
|
||||
## 추가 시도 및 결과
|
||||
|
||||
### 5) login_verifier를 이용한 consent_challenge 생성 (public /oauth2/auth 호출)
|
||||
- 시도: `login_verifier`로 `/oauth2/auth?login_verifier=...` 호출 → Location에서 `consent_challenge` 추출 시도
|
||||
- 실패: `login_verifier has already been used` 또는 `invalid_client` 등으로 예제 redirect(`https://example.com/callback?...`) 에러 반환
|
||||
- 원인 추정:
|
||||
- `login_verifier`는 1회성으로, 기존 흐름(redirect_to 또는 consent app)에 의해 이미 사용됨
|
||||
- `login_verifier`만으로는 client 맥락이 부족하여 invalid_client 발생 가능
|
||||
|
||||
### 6) 임시 consent app(127.0.0.1:3000) 없이 redirect_to 직접 호출
|
||||
- 실패: consent app 미기동으로 연결 불가
|
||||
- 원인: login/consent UI URL이 기본값(127.0.0.1:3000)이라 실제 서비스 필요
|
||||
|
||||
### 7) Python 컨테이너에서 임시 consent app + 클라이언트 플로우 실행
|
||||
- 방법: python:3.12-alpine 컨테이너에서
|
||||
- 127.0.0.1:3000에 최소 consent 앱 실행
|
||||
- `/oauth2/auth` 호출을 따라가며 login/consent 자동 수락
|
||||
- 결과:
|
||||
- 첫 시도에서 `state` 길이 부족으로 `invalid_state` 발생
|
||||
- 이후 `state/nonce` 길이 충분히 늘려 재시도 중
|
||||
|
||||
## 현재 결론
|
||||
- 실제 consent 앱이 없으면 Hydra는 consent_challenge를 만들 수 없고 흐름이 중단됨.
|
||||
- 임시 consent 앱을 컨테이너 내부에서 띄우는 방식이 가장 현실적이며, 이 흐름으로 계속 진행 중.
|
||||
|
||||
### 8) devfront/hydra-rp-dummy.py + docker mount 실행
|
||||
- 방식: `hydra-rp-dummy.py`를 컨테이너에 마운트하여 임시 consent app(127.0.0.1:3000)으로 로그인/동의 자동 수락
|
||||
- 결과: 최종 리다이렉트가 `request_forbidden` (CSRF 쿠키 없음) 에러로 종료됨
|
||||
- 하지만 Hydra Admin 조회 결과 consent 세션은 생성됨
|
||||
- `handled_at`: 2026-01-30T05:01:46.770699Z
|
||||
- subject: `22607c1b-bfbf-4a90-9505-36b348472e7a`
|
||||
- client_id: `52a597f0-5b06-4fcb-b804-93e88a56a75a`
|
||||
- grant_scope: `openid profile email`
|
||||
|
||||
### 상태
|
||||
- consent 세션 생성 완료(확인됨)
|
||||
- 최종 리다이렉트 단계에서 CSRF 오류는 남아 있으나, 목적(연동/동의 저장)은 달성됨
|
||||
35
baron-sso/docs/trouble-shooting/issue-146-remote-login.md
Normal file
35
baron-sso/docs/trouble-shooting/issue-146-remote-login.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# #146 원격 링크 로그인 세션/이력 불일치 대응
|
||||
|
||||
## 요약
|
||||
- Ory 링크 로그인은 실제로 `/api/v1/auth/login/code/verify` 또는 `/api/v1/auth/login/code/verify-short` 경로를 사용합니다.
|
||||
- 기존에는 `verifyOnly`가 `/api/v1/auth/magic-link/verify`에만 적용되어, 링크를 클릭한 기기에서 세션이 발급되는 문제가 있었습니다.
|
||||
- 인증수단 표기는 loginId 기반 추론에 의존해 SMS 요청이 Email로 표시되는 문제가 있었습니다.
|
||||
|
||||
## 원인
|
||||
- verify-only 적용 범위가 magic link에 한정되어 있었고, Ory 코드 기반 경로는 세션을 즉시 발급했습니다.
|
||||
- audit 로그의 인증수단 표기는 request_body/loginId 기반 추론만 사용했습니다.
|
||||
|
||||
## 변경 사항
|
||||
### 1) verify-only 범위 확장
|
||||
- `/api/v1/auth/login/code/verify`, `/api/v1/auth/login/code/verify-short`에 `verifyOnly` 지원 추가
|
||||
- verify-only일 때는 승인 상태만 저장하고 세션 발급은 Polling(Desktop)에서 수행
|
||||
|
||||
### 2) Polling 시 세션 발급 주체 정리
|
||||
- 승인 상태(`status=approved`)는 **요청한 기기(A)**에서만 세션 발급
|
||||
- Ory 코드 플로우는 Polling 시점에 `VerifyLoginCode`를 수행해 세션 생성
|
||||
|
||||
### 3) 인증수단 표기 개선
|
||||
- `pendingRef` 기준으로 `login_method`(sms/email), `login_flow`(code/link) 저장
|
||||
- audit 로그에 해당 메타를 기록하여 SMS/Email, 코드/링크 구분을 명확히 표시
|
||||
- verify-only 요청 로그는 타임라인에서 제외
|
||||
|
||||
## 영향 범위
|
||||
- Backend: 링크 로그인 승인/세션 발급 경로 변경
|
||||
- Front: verify-only 플래그 전달 확장
|
||||
- 문서: auth-flow/test-plan 업데이트
|
||||
|
||||
## 테스트 계획 (요약)
|
||||
- Desktop에서 링크 요청 → Mobile에서 링크 클릭(verifyOnly) → Desktop Polling으로 세션 발급
|
||||
- Mobile 단말에서 세션/로그인 이력 미생성 확인
|
||||
- 인증수단 표기(SMS/Email) 정확성 확인
|
||||
- 코드/링크 만료/재사용 시나리오 점검
|
||||
@@ -0,0 +1,51 @@
|
||||
# Issue #269 해결 기록: `/{locale}/` 도입 후 query parameter 유실
|
||||
|
||||
## 개요
|
||||
- 대상 이슈: `#269`
|
||||
- 증상: locale 보정 또는 비로그인 리다이렉트 과정에서 GET query parameter가 유실되거나 형태가 변형됨
|
||||
- 영향: OIDC 로그인 연계 파라미터(`login_challenge`, `redirect_uri`, `notice` 등) 전달 실패 가능
|
||||
|
||||
## 원인
|
||||
1. 비로그인 리다이렉트 시 `login_challenge`만 선택 보존하고 나머지 query를 폐기
|
||||
2. locale 경로 재작성 시 `uri.queryParameters` 기반 재직렬화로 원본 query 문자열(중복 key, 순서, 인코딩) 보존 실패
|
||||
3. `head.length == 2` 휴리스틱으로 locale이 아닌 2글자 경로 prefix까지 locale로 오인 가능
|
||||
|
||||
## 수정 사항
|
||||
|
||||
### 1) 비로그인 리다이렉트에서 raw query 전체 보존
|
||||
- 파일: `userfront/lib/main.dart`
|
||||
- 변경: `state.uri.query`를 그대로 `/[locale]/signin`에 연결
|
||||
|
||||
```dart
|
||||
final rawQuery = state.uri.query;
|
||||
if (rawQuery.isNotEmpty) {
|
||||
return '/$locale/signin?$rawQuery';
|
||||
}
|
||||
return '/$locale/signin';
|
||||
```
|
||||
|
||||
### 2) locale 경로 재작성 시 raw query/fragment 보존
|
||||
- 파일: `userfront/lib/core/i18n/locale_utils.dart`
|
||||
- 변경: `queryParameters` 재직렬화 제거, `uri.query`/`uri.fragment` 원문 유지
|
||||
|
||||
```dart
|
||||
final queryPart = uri.hasQuery ? '?${uri.query}' : '';
|
||||
final fragmentPart = uri.fragment.isNotEmpty ? '#${uri.fragment}' : '';
|
||||
return '$path$queryPart$fragmentPart';
|
||||
```
|
||||
|
||||
### 3) locale 판별 조건 엄격화
|
||||
- 파일: `userfront/lib/core/i18n/locale_utils.dart`
|
||||
- 변경: `head.length == 2` 휴리스틱 제거, `supportedLocaleCodes.contains(head)`만 허용
|
||||
|
||||
## 테스트 보강
|
||||
- 파일: `userfront/test/locale_utils_test.dart`
|
||||
- 추가/변경:
|
||||
- raw query 순서 및 중복 key(`a=1&a=2`) 보존
|
||||
- fragment 보존
|
||||
- unknown 2-letter prefix(`zz`)를 locale로 제거하지 않음
|
||||
|
||||
## 기대 결과
|
||||
- `/signin?redirect_uri=...¬ice=...` -> locale 보정 후 query 100% 유지
|
||||
- 비로그인 보호 경로 -> `/[locale]/signin` 이동 시 기존 query 유지
|
||||
- 인코딩된 nested `redirect_uri`, 중복 query key, fragment 보존
|
||||
83
baron-sso/docs/trouble-shooting/issue-269-test-scenarios.md
Normal file
83
baron-sso/docs/trouble-shooting/issue-269-test-scenarios.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Issue #269 테스트 시나리오
|
||||
|
||||
## 목적
|
||||
`/{locale}/` 라우팅 도입 이후 query parameter 유실 회귀를 방지합니다.
|
||||
|
||||
## 범위
|
||||
- UserFront locale 경로 보정 (`buildLocalizedPath`)
|
||||
- 비로그인 redirect 경로 생성 (`buildSigninRedirectPath`)
|
||||
- locale 지원 목록 동기화 (`assets/translations/*.toml` -> `LocaleRegistry`)
|
||||
|
||||
## 테스트 시나리오
|
||||
|
||||
### S1. locale 보정 시 기본 query 보존
|
||||
- 입력: `/signin?redirect_uri=https://example.com`
|
||||
- 기대: `/ko/signin?redirect_uri=https://example.com`
|
||||
|
||||
### S2. locale 보정 시 raw query 순서/중복 key 보존
|
||||
- 입력: `/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2`
|
||||
- 기대: `/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2`
|
||||
|
||||
### S3. locale 보정 시 fragment 보존
|
||||
- 입력: `/signin?notice=qr_login_required#auth`
|
||||
- 기대: `/ko/signin?notice=qr_login_required#auth`
|
||||
|
||||
### S4. unknown 2-letter prefix 오인 제거
|
||||
- 입력: `/zz/signin`
|
||||
- 기대: `/ko/zz/signin`
|
||||
|
||||
### S5. 비로그인 redirect에서 query 없음
|
||||
- 입력: locale=`ko`, uri=`/ko/profile`
|
||||
- 기대: `/ko/signin`
|
||||
|
||||
### S6. 비로그인 redirect에서 query 전체 보존
|
||||
- 입력: locale=`ko`, uri=`/ko/profile?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2¬ice=qr_login_required`
|
||||
- 기대: `/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2¬ice=qr_login_required`
|
||||
|
||||
### S7. locale 목록 하드코딩 제거 검증
|
||||
- 입력: asset 목록 (`assets/translations/en.toml`, `assets/translations/ko.toml`, `assets/translations/template.toml`, 기타 invalid 파일)
|
||||
- 기대:
|
||||
- `template.toml` 제외
|
||||
- 유효 locale 파일(`en.toml`, `ko.toml`)만 지원 목록에 반영
|
||||
|
||||
### S8. 실계정 비밀번호 변경 스모크(E2E)
|
||||
- 목적: 로그인 상태 플로우가 기존 동작을 깨지 않았는지 확인
|
||||
- 절차:
|
||||
- Kratos Admin API로 임시 계정 생성(초기 비밀번호 포함)
|
||||
- 구 비밀번호 로그인 성공 확인
|
||||
- Settings API로 비밀번호 변경
|
||||
- 구 비밀번호 로그인 실패 확인
|
||||
- 신 비밀번호 로그인 성공 확인
|
||||
- 테스트 계정 삭제(정리)
|
||||
- 기대:
|
||||
- 비밀번호 변경 전/후 인증 결과가 정확히 반전
|
||||
- 테스트 종료 후 identity 삭제 완료(잔존 계정 없음)
|
||||
|
||||
## 실행 방법
|
||||
```bash
|
||||
cd userfront
|
||||
flutter test test/locale_utils_test.dart
|
||||
flutter test test/locale_registry_test.dart
|
||||
flutter test test/router_redirect_widget_test.dart
|
||||
flutter test --platform chrome test/locale_utils_test.dart test/locale_registry_test.dart test/router_redirect_widget_test.dart
|
||||
```
|
||||
|
||||
## 자동화 매핑
|
||||
- `userfront/test/locale_utils_test.dart`
|
||||
- S1~S6 전부 커버
|
||||
- `userfront/test/locale_registry_test.dart`
|
||||
- S7 커버
|
||||
- `userfront/test/router_redirect_widget_test.dart`
|
||||
- 로그인/비로그인 redirect 동작 검증(`redirect_uri`, `redirect_url`)
|
||||
|
||||
## 최근 실행 결과
|
||||
- 실행일: 2026-02-19
|
||||
- 결과:
|
||||
- Flutter 테스트(VM): 통과
|
||||
- Flutter 테스트(Chrome): 통과
|
||||
- S8 실계정 E2E: 통과
|
||||
- `login_old_password=200`
|
||||
- `change_password=200`
|
||||
- `login_old_after_change=400`
|
||||
- `login_new_after_change=200`
|
||||
- `cleanup(delete identity)=204`
|
||||
@@ -0,0 +1,76 @@
|
||||
# Issue #277/#302 트러블슈팅 기록: 로그인 후 공백 화면 + 새로고침 시 signin 회귀
|
||||
|
||||
## 기준 시점
|
||||
- 2026-02-23 KST
|
||||
- 재현 환경: `https://sss.hmac.kr` (WASM 배포)
|
||||
|
||||
## 증상
|
||||
- 로그인 직후 URL은 `/{locale}` 또는 `/{locale}/dashboard`로 보이지만 화면이 렌더링되지 않음
|
||||
- 이후 새로고침하면 `/{locale}/signin`으로 되돌아감
|
||||
- 콘솔/백엔드 수집 로그:
|
||||
- `Null check operator used on a null value`
|
||||
- `wasm-function[765]` 포함 스택 반복
|
||||
|
||||
## 스택 매핑 결과 (source-map + no-strip-wasm)
|
||||
- 매핑 커맨드:
|
||||
- `python3 scripts/map_wasm_stack.py --wasm userfront/build/web/main.dart.wasm --sourcemap userfront/build/web/main.dart.wasm.map --frame ...`
|
||||
- 핵심 프레임:
|
||||
- `wasm-function[765]` -> `_TypeError._throwNullCheckErrorWithCurrentStack`
|
||||
- 상위 프레임 -> Flutter `NavigatorState.didUpdateWidget/_updatePages` 경로
|
||||
- 결론:
|
||||
- 단일 위젯 null 접근보다, 라우트 갱신 타이밍/중복 네비게이션 경쟁에서 `Navigator` 내부에서 터지는 양상
|
||||
|
||||
## 지금까지 시행착오와 실패 내역
|
||||
1. `LocaleGate`, `LanguageSelector`의 `EasyLocalization.of(context)` null 방어만 적용
|
||||
- 결과: 동일 예외 재발
|
||||
- 이유: 루트 원인은 로케일 위젯 단일 null 접근이 아니라 네비게이션 경쟁 구간
|
||||
|
||||
2. `/ko` 루트에서 signin 강제 리다이렉트만 강화
|
||||
- 결과: 최초 진입은 일부 개선됐지만 로그인 직후/새로고침 회귀 지속
|
||||
- 이유: 로그인 성공 경로가 루트(`/{locale}`)와 엮이면서 라우트 재평가가 중첩
|
||||
|
||||
3. 로그인 화면에서 `AuthNotifier.notify()` + `context.go(...)` 동시 수행
|
||||
- 결과: 간헐적 경쟁 상태 유발 가능성 확인
|
||||
- 조치: 로컬 네비게이션 1회 가드 도입(`_goLocalizedHomeOnce`)
|
||||
|
||||
4. cookie 세션 승격이 토큰 저장 이후 덮어쓰는 경합
|
||||
- 결과: 일부 흐름에서 저장 상태 불안정 가능성
|
||||
- 조치: `cookie_session_policy` 추가, 토큰 존재 시 불필요한 cookie 승격 차단
|
||||
|
||||
5. `/:locale` 엔트리가 redirect 없이 매칭되는 구조
|
||||
- 결과: `/ko` 직접 진입 시 페이지 스택 재계산 과정에서 `NavigatorState.didUpdateWidget/_updatePages` 경로 null check 재발
|
||||
- 이유: `/ko`는 실질 화면이 아닌 분기 지점인데, 명시적 redirect 경로가 없으면 라우트 갱신 타이밍 경쟁에 취약
|
||||
- 조치: `/:locale`를 redirect 전용 엔트리로 확정(비로그인 `/{locale}/signin`, 로그인 `/{locale}/dashboard`)
|
||||
|
||||
## 최종 반영 방향 (이번 패치)
|
||||
1. 로그인 성공 기본 경로를 명시적으로 `/{locale}/dashboard`로 고정
|
||||
- `buildLocalizedHomePath()` 반환값을 `/{locale}/dashboard`로 변경
|
||||
- `/:locale` 엔트리는 `/:locale/dashboard`로 redirect 전용 처리
|
||||
|
||||
2. 라우터/화면 역할 분리
|
||||
- 보호 경로 검사는 router redirect에서 수행
|
||||
- 대시보드는 필요 시 cookie 세션 복구를 1회 시도 후 signin 이동
|
||||
|
||||
3. 중복 네비게이션 억제
|
||||
- 로그인 성공 시 내부 이동은 1회만 수행
|
||||
|
||||
## 검증
|
||||
- 추가 테스트:
|
||||
- `userfront/test/login_navigation_race_test.dart`
|
||||
- `userfront/test/cookie_session_policy_test.dart`
|
||||
- `userfront/test/router_redirect_widget_test.dart` (`/{locale}` 직접 진입 시 signin/dashboard 분기 검증)
|
||||
- 갱신 테스트:
|
||||
- `userfront/test/locale_utils_test.dart` (home path `/{locale}/dashboard` 기준)
|
||||
- 실행:
|
||||
- `flutter test`
|
||||
- `flutter test --platform chrome test/router_redirect_widget_test.dart test/login_navigation_race_test.dart test/cookie_session_policy_test.dart`
|
||||
|
||||
## 남은 리스크
|
||||
- 실제 브라우저 저장소 정책(localStorage 차단/쿠키 정책)에 따라 세션 판정이 달라질 수 있음
|
||||
- 운영 검증 시 네트워크/스토리지 상태를 함께 수집해야 원인 분리 가능
|
||||
|
||||
## 운영 확인 체크리스트
|
||||
1. 비로그인으로 `/{locale}` 접속 시 즉시 `/{locale}/signin` 이동
|
||||
2. 로그인 성공 시 `/{locale}/dashboard` 진입
|
||||
3. `/{locale}/dashboard`에서 새로고침 후 세션 유지 (동일 브라우저)
|
||||
4. 실패 시 `RECOVERY_NAV_NULL_CHECK`와 wasm frame 동시 수집
|
||||
@@ -0,0 +1,94 @@
|
||||
# Issue #281: locale_storage 리팩터링 계획
|
||||
|
||||
## 목적
|
||||
- `locale_storage` 관련 로직에서 정책 지식(키, fallback, migration)을 한 곳으로 모아 변경 비용을 줄입니다.
|
||||
- 테스트를 내부 구현 의존(`webStorage`)에서 파사드 의존(`LocaleStorage`) 중심으로 바꿔 회귀 위험을 낮춥니다.
|
||||
- 테스트 강제 훅(`forceMemoryStorageForTests`, `forceSessionStorageForTests`)의 진입점을 단일화합니다.
|
||||
|
||||
## 현재 문제 요약
|
||||
1. 저장 정책 지식이 여러 파일에 분산되어 있습니다.
|
||||
2. 테스트가 구현 세부사항에 결합되어 정책 변경 시 함께 깨질 가능성이 큽니다.
|
||||
3. 플랫폼별 훅 wiring이 반복되어 확장 시 누락 가능성이 있습니다.
|
||||
|
||||
## 변경 범위
|
||||
- 대상 파일
|
||||
- `userfront/lib/core/i18n/locale_storage.dart`
|
||||
- `userfront/lib/core/i18n/locale_storage_stub.dart`
|
||||
- `userfront/lib/core/i18n/locale_storage_web.dart`
|
||||
- `userfront/test/locale_storage_platform_test.dart`
|
||||
- `userfront/test/helpers/web_storage.dart`
|
||||
- `userfront/test/helpers/web_storage_stub.dart`
|
||||
- `userfront/test/helpers/web_storage_web.dart`
|
||||
|
||||
## 리팩터링 단계
|
||||
### 1) 저장 정책 공통화
|
||||
- 저장 키 상수(`locale`, legacy `baron_locale`)와 migration 로직을 공통 모듈로 이동합니다.
|
||||
- fallback 순서(local -> session -> memory)를 공통 정책 함수로 추출합니다.
|
||||
- `locale_storage_web.dart`는 정책 모듈 호출 위주로 단순화합니다.
|
||||
|
||||
### 2) 테스트 결합도 축소
|
||||
- 테스트 assertion의 중심을 `LocaleStorage` API로 이동합니다.
|
||||
- `webStorage` 직접 검증은 최소화하고, 필요한 경우 정책 모듈의 관찰 포인트를 제한적으로 제공합니다.
|
||||
|
||||
### 3) 테스트 훅 단일 진입
|
||||
- `LocaleStorage` 파사드에서만 테스트 훅을 제어하도록 정리합니다.
|
||||
- 플랫폼 구현은 훅 내부 세부 정책을 직접 노출하지 않도록 인터페이스를 정돈합니다.
|
||||
|
||||
### 4) 회귀 테스트 보강
|
||||
- legacy key migration(`baron_locale -> locale`) 회귀 테스트를 명시적으로 유지합니다.
|
||||
- storage access 실패/비가용 상황에서 fallback 순서를 검증하는 테스트를 추가합니다.
|
||||
|
||||
## 완료 기준(DoD)
|
||||
- 정책 변경 시 수정 포인트가 공통 모듈 중심으로 줄어듭니다.
|
||||
- 기존 locale 저장/조회 동작과 migration 동작이 유지됩니다.
|
||||
- VM 기반 테스트가 안정적으로 통과하며 fallback/migration 회귀 케이스가 포함됩니다.
|
||||
|
||||
## 구현 시 주의사항
|
||||
- 외부에서 사용하는 public API 시그니처는 가능한 유지합니다.
|
||||
- 테스트 편의를 위한 훅은 운영 코드 경로에 영향이 없도록 격리합니다.
|
||||
- 리팩터링 중간 단계에서도 테스트가 통과하도록 작은 단위로 나눠 적용합니다.
|
||||
|
||||
## 롤백 기준
|
||||
- locale 저장/복구가 실패하거나, 웹 환경에서 fallback 동작이 달라지는 경우 즉시 이전 커밋 단위로 되돌립니다.
|
||||
- migration 동작이 깨지는 경우 해당 단계만 우선 revert하고 정책 모듈 분리부터 재진행합니다.
|
||||
|
||||
## 구현 결과 (2026-02-20)
|
||||
### 반영된 코드 변경
|
||||
- 공통 계약/디버그 상태 타입 추가
|
||||
- `userfront/lib/core/i18n/locale_storage_backend.dart`
|
||||
- 저장 정책 상수/판단 로직 분리
|
||||
- `userfront/lib/core/i18n/locale_storage_policy.dart`
|
||||
- 파사드 단일 테스트 진입점 정리
|
||||
- `userfront/lib/core/i18n/locale_storage.dart`
|
||||
- `setTestModeForTests`, `clearForTests`, `seedLegacyForTests`, `debugStateForTests` 추가
|
||||
- 기존 `forceMemoryStorageForTests`, `forceSessionStorageForTests` 호환 유지
|
||||
- 플랫폼 구현 리팩터링
|
||||
- `userfront/lib/core/i18n/locale_storage_web.dart`
|
||||
- `userfront/lib/core/i18n/locale_storage_stub.dart`
|
||||
- fallback 순서(local -> session -> memory) 유지
|
||||
- legacy migration 시 legacy key(`baron_locale`)를 local/session/memory 전체에서 정리
|
||||
- 테스트 정리
|
||||
- `userfront/test/locale_storage_platform_test.dart`를 `LocaleStorage` API 중심 검증으로 전환
|
||||
- 삭제: `userfront/test/helpers/web_storage.dart`
|
||||
- 삭제: `userfront/test/helpers/web_storage_stub.dart`
|
||||
- 삭제: `userfront/test/helpers/web_storage_web.dart`
|
||||
|
||||
### CI/워크플로우 반영
|
||||
- 파일: `.gitea/workflows/code_check.yml`
|
||||
- `workflow_dispatch.inputs` 복원
|
||||
- `run_lint`
|
||||
- `run_backend_tests`
|
||||
- `run_userfront_tests`
|
||||
- 각 job 실행 조건 복원
|
||||
- lint: `inputs.run_lint`
|
||||
- backend-tests: `inputs.run_backend_tests`
|
||||
- userfront-tests: `inputs.run_userfront_tests`
|
||||
- userfront-tests 정책 정리
|
||||
- `flutter test` 단일 실행으로 운영
|
||||
- `locale_storage` 정책 검증은 VM 테스트(`locale_storage_platform_test.dart`)로 통합
|
||||
- 브라우저 설치/`--platform chrome` 단계 제거
|
||||
|
||||
### 검증 결과
|
||||
- `cd userfront && flutter analyze --no-fatal-warnings --no-fatal-infos` 통과
|
||||
- `cd userfront && flutter test` 통과
|
||||
- `cd userfront && flutter test test/locale_storage_platform_test.dart` 통과
|
||||
@@ -0,0 +1,47 @@
|
||||
# #303 로그인 링크/코드 진입 실패 (`/{locale}/l/{shortCode}`) 대응
|
||||
|
||||
## 요약
|
||||
- 로그인 링크 발송은 정상이나, 링크 클릭 후 로그인 검증 진입이 실패하는 사례가 있었습니다.
|
||||
- 특히 locale prefix가 포함된 short-code 경로(`/{locale}/l/{shortCode}`)에서 재현되었습니다.
|
||||
- 원인은 라우터의 public path 판별과 short-code 추출 로직이 locale prefix 경로를 고려하지 못한 점이었습니다.
|
||||
|
||||
## 증상
|
||||
- 링크 클릭 URL이 `https://.../ko/l/AB123456` 형태일 때 로그인 검증이 자동 시작되지 않음
|
||||
- 비로그인 상태에서 해당 경로 접근 시 signin으로 리다이렉트되어 short-code 검증이 끊김
|
||||
|
||||
## 원인
|
||||
1. Public path 판별에서 `/l/` 경로가 제외되어 있었음
|
||||
2. `LoginScreen`의 short-code 추출이 `uri.pathSegments.first == 'l'`에만 의존
|
||||
- `/{locale}/l/{shortCode}`에서는 첫 세그먼트가 locale이므로 추출 실패
|
||||
|
||||
## 조치 내용
|
||||
1. 경로 정책 분리
|
||||
- `userfront/lib/features/auth/domain/login_link_route_policy.dart` 신규 추가
|
||||
- public path 판별(`isPublicAuthPath`)과 short-code 추출(`extractLoginShortCode`)을 공용화
|
||||
|
||||
2. 라우터 반영
|
||||
- `userfront/lib/main.dart` redirect에서 `isPublicAuthPath` 사용
|
||||
- `/l/` 경로를 public path로 허용
|
||||
|
||||
3. 로그인 화면 반영
|
||||
- `userfront/lib/features/auth/presentation/login_screen.dart`에서
|
||||
`extractLoginShortCode(Uri.base)`로 short-code를 추출하도록 변경
|
||||
- locale prefix 유무와 관계없이 short-code 검증 진입 가능
|
||||
|
||||
## 테스트
|
||||
### 재현 테스트 (Failing first)
|
||||
- `flutter test test/login_link_route_policy_test.dart`
|
||||
- 초기 실패 확인:
|
||||
- localized short-code 추출 실패
|
||||
- localized short-code public path 판별 실패
|
||||
|
||||
### 수정 후 회귀 테스트
|
||||
- `flutter test test/login_link_route_policy_test.dart` 통과
|
||||
- `flutter test test/router_redirect_widget_test.dart` 통과
|
||||
|
||||
## 영향 범위
|
||||
- 링크/코드 로그인 진입 라우팅 (`/l/{shortCode}` 및 `/{locale}/l/{shortCode}`)
|
||||
- 기존 `/verify`, `/signin`, `/login` 경로에는 동작 변화 없음
|
||||
|
||||
## 관련 이슈
|
||||
- Gitea: #303 `[bug][auth] 링크 클릭/코드 입력 로그인 실패 재현 및 수정`
|
||||
@@ -0,0 +1,45 @@
|
||||
# Issue #434 트러블슈팅 기록: 대시보드 세션 시작 시간이 `Unknown`으로 표시됨
|
||||
|
||||
## 기준 시점
|
||||
- 2026-03-24 KST
|
||||
- 대상 화면: UserFront 대시보드 상단 세션 정보 칩
|
||||
|
||||
## 증상
|
||||
- 로그인 후 대시보드의 세션 시작 시간이 `Unknown` 또는 `알 수 없음`으로 표시됨
|
||||
- 특히 동일 브라우저의 cookie session 승격 경로에서 재현됨
|
||||
|
||||
## 원인
|
||||
1. 기존 대시보드는 저장된 로컬 토큰만 파싱해 `iat` 또는 `auth_time`을 읽었습니다.
|
||||
2. cookie mode에서는 `AuthTokenStore.setCookieMode()`가 로컬 토큰을 제거하고 cookie 플래그만 유지합니다.
|
||||
3. 그 결과 대시보드는 파싱할 JWT가 없어 항상 fallback 문구로 떨어졌습니다.
|
||||
|
||||
## 수정 방향
|
||||
1. Backend `/api/v1/user/me` 응답에 Kratos `sessions/whoami`의 `authenticated_at` 값을 `sessionAuthenticatedAt`으로 포함합니다.
|
||||
2. UserFront 대시보드는 세션 시각 계산 시 다음 우선순위를 사용합니다.
|
||||
- JWT의 `iat` 또는 `auth_time`
|
||||
- profile의 `sessionAuthenticatedAt`
|
||||
3. 두 값이 모두 없을 때만 `ui.userfront.session.unknown` fallback을 사용합니다.
|
||||
|
||||
## 반영 파일
|
||||
- `backend/internal/domain/auth_models.go`
|
||||
- `backend/internal/handler/auth_handler.go`
|
||||
- `backend/docs/openapi.yaml`
|
||||
- `userfront/lib/features/profile/data/models/user_profile_model.dart`
|
||||
- `userfront/lib/features/dashboard/domain/session_time_resolver.dart`
|
||||
- `userfront/lib/features/dashboard/presentation/dashboard_screen.dart`
|
||||
|
||||
## 회귀 테스트
|
||||
- Backend
|
||||
- `backend/internal/handler/auth_handler_session_profile_test.go`
|
||||
- UserFront
|
||||
- `userfront/test/dashboard_session_time_resolver_test.dart`
|
||||
- `userfront/test/dashboard_screen_smoke_test.dart`
|
||||
|
||||
## 검증 명령
|
||||
- `GOCACHE=/tmp/go-build go test ./internal/handler -run 'TestGetMe_IncludesSessionAuthenticatedAt' -count=1`
|
||||
- `flutter test test/dashboard_session_time_resolver_test.dart`
|
||||
- `flutter test test/dashboard_screen_smoke_test.dart`
|
||||
|
||||
## 남은 참고사항
|
||||
- Hydra introspection fallback만 사용되는 토큰 경로에서는 `sessionAuthenticatedAt`이 비어 있을 수 있습니다.
|
||||
- 이 경우에도 JWT claim이 없으면 기존 fallback 문구를 유지합니다.
|
||||
36
baron-sso/docs/trouble-shooting/issue-614-skip-consent.md
Normal file
36
baron-sso/docs/trouble-shooting/issue-614-skip-consent.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Issue #614 일반 RP Consent 반복 노출
|
||||
|
||||
## 현상
|
||||
- `https://ssob.hmac.kr/`의 나의 App 현황에서 일반 서비스 클라이언트 바로가기를 열 때 Hydra consent 화면이 매번 노출되었습니다.
|
||||
- `DevFront`, `AdminFront`는 동일 경로에서 consent 화면이 반복 노출되지 않았습니다.
|
||||
|
||||
## 원인
|
||||
- 일반 RP 생성/수정 API가 Hydra OAuth2 client의 `skip_consent` 값을 전달하지 않았습니다.
|
||||
- 백엔드 DTO와 DevFront 설정 모델에도 해당 필드가 없어 신규/기존 RP를 신뢰 앱으로 제어할 수 없었습니다.
|
||||
- `remember: true` consent 세션은 이미 적용되어 있었지만, Hydra client 자체의 `skip_consent`와는 별도 정책입니다.
|
||||
|
||||
## 조치
|
||||
- `domain.HydraClient`에 `skip_consent` JSON 필드를 추가했습니다.
|
||||
- Dev API는 `skipConsent` 요청 값을 받을 수 있지만, DevFront UI에는 별도 체크박스를 추가하지 않습니다.
|
||||
- 신규 RP 생성 시 `skipConsent`가 생략되면 기본값을 `true`로 Hydra에 전달합니다.
|
||||
- 기존 RP 수정 시 현재 값이 없으면 `true`로 보정하고, 명시적으로 `false`를 선택하면 그대로 Hydra에 전달합니다.
|
||||
|
||||
## 검증
|
||||
- `TestCreateClient_DefaultsSkipConsentToTrue`
|
||||
- 신규 RP 생성 요청에서 `skipConsent`가 생략되어도 Hydra payload의 `skip_consent`가 `true`인지 검증합니다.
|
||||
- `TestCreateClient_AllowsExplicitSkipConsentFalse`
|
||||
- 신규 RP 생성 요청에서 명시한 `skipConsent: false`가 Hydra payload에 보존되는지 검증합니다.
|
||||
- `TestUpdateClient_AllowsExplicitSkipConsentFalse`
|
||||
- 기존 RP 수정 요청에서 `skipConsent: false`가 Hydra update payload에 보존되는지 검증합니다.
|
||||
|
||||
## 실행 결과
|
||||
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/handler -run 'Test(CreateClient_(DefaultsSkipConsentToTrue|AllowsExplicitSkipConsentFalse)|UpdateClient_AllowsExplicitSkipConsentFalse)' -count=1`
|
||||
- `GOCACHE=/tmp/baron-sso-go-cache go test ./internal/handler -count=1`
|
||||
- `cd devfront && npx biome check src/features/clients/ClientGeneralPage.tsx src/lib/devApi.ts src/locales/en.toml src/locales/ko.toml src/locales/template.toml --formatter-enabled=false --organize-imports-enabled=false`
|
||||
- `cd devfront && npx tsc -b --pretty false`
|
||||
- `node tools/i18n-scanner/index.js`
|
||||
- `node tools/i18n-scanner/value-check.js`
|
||||
|
||||
## 수동 테스트용 RP
|
||||
- `tools/consent-demo-page/index.php`를 추가했습니다.
|
||||
- DevFront에서 테스트용 RP를 따로 만들고, 데모 페이지의 `.env`에 해당 `CLIENT_ID`와 `REDIRECT_URI`를 설정하면 브라우저 기반 OIDC/consent 흐름을 확인할 수 있습니다.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Issue #663: 한맥가족 사용자 테넌트 동기화 및 직무/직급 표시 정리
|
||||
|
||||
## 개요
|
||||
|
||||
adminfront 사용자 관리 화면과 orgfront 조직도/조직 선택기 사이의 한맥가족 사용자 표시 기준을 정리했다.
|
||||
|
||||
기존에는 사용자 상세 화면에서 `metadata.hanmacFamily` 값에 의존해 한맥가족 탭을 결정했다. 이 방식은 기존 사용자가 이미 한맥가족 테넌트 트리에 소속되어 있어도 metadata 플래그가 없으면 외부 기업 회원으로 분류될 수 있었다.
|
||||
|
||||
## 정책 기준
|
||||
|
||||
- `docs/organization-chart-policy.md`에 따라 한맥가족 조직은 `COMPANY_GROUP` 아래의 `COMPANY`/`USER_GROUP` 테넌트 계층으로 판단한다.
|
||||
- 한맥가족 사용자의 직무/직급은 단일 사용자 필드보다 소속별 `additionalAppointments`를 우선한다.
|
||||
- orgfront 표시명은 직무가 있으면 `이름(직무) 직급` 형태를 사용한다.
|
||||
|
||||
## 구현 요약
|
||||
|
||||
- adminfront
|
||||
- 한맥가족 root tenant 및 subtree 판정 유틸을 추가했다.
|
||||
- 기존 사용자의 `tenant`, `joinedTenants`, `companyCode`, `tenantSlug`를 기준으로 한맥가족 여부를 계산한다.
|
||||
- 사용자 상세 초기화 시 한맥가족 subtree 소속이면 `metadata.hanmacFamily`가 없어도 한맥가족 탭을 표시한다.
|
||||
- 한맥가족 저장 시 기존 단일 `position`/`jobTitle` payload를 비우고 `additionalAppointments`를 사용한다.
|
||||
- orgfront
|
||||
- 사용자 표시명 공통 유틸을 추가했다.
|
||||
- 조직도와 조직 선택기 모두 동일한 표시명 로직을 사용한다.
|
||||
- `metadata.additionalAppointments`에 현재 테넌트와 매칭되는 직무/직급이 있으면 이를 우선 사용한다.
|
||||
|
||||
## 검증
|
||||
|
||||
- RED 확인
|
||||
- `adminfront`: 한맥가족 subtree 사용자 판정 유틸 부재로 unit test 실패 확인.
|
||||
- `orgfront`: 조직도/조직 선택기에서 `이름(직무) 직급` 표시가 없어 Playwright test 실패 확인.
|
||||
- GREEN 확인
|
||||
- `adminfront`: `npm run test:unit -- src/features/users/orgChartPicker.test.ts`
|
||||
- `adminfront`: `npm run test:unit`
|
||||
- `adminfront`: `npm run build`
|
||||
- `orgfront`: `npm run lint`
|
||||
- `orgfront`: `npm run build`
|
||||
- `orgfront`: `npx playwright test tests/orgchart-picker.spec.ts tests/orgchart-vector-render.spec.ts --project=chromium`
|
||||
|
||||
## 남은 정책/운영 메모
|
||||
|
||||
- 이슈는 `한맥가족사 조직도 반영` 마일스톤에 연결했다.
|
||||
- 해당 마일스톤은 Due Date가 비어 있다. 목표 Due Date를 정해 마일스톤에 반영하는 것이 좋다.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 이슈 #489 작업 완료 보고서
|
||||
|
||||
## 작업 개요
|
||||
`devfront`에서 'Headless Login (자체 로그인 UI 사용)' 옵션을 활성화하여 생성한 PKCE 앱이 연동 앱 목록에서 'Server side App'으로 잘못 표기되는 현상을 수정했습니다.
|
||||
|
||||
## 상세 반영 내용
|
||||
|
||||
### 1. 백엔드 로직 수정 (`backend/internal/handler/dev_handler.go`)
|
||||
- `mapClientSummary` 함수에서 클라이언트 유형(Type)을 결정하는 로직을 보완했습니다.
|
||||
- 기존에는 `TokenEndpointAuthMethod`가 `"none"`인 경우에만 `pkce`로 분류했으나, 이제는 `private_key_jwt` 방식이더라도 메타데이터에 `headless_login_enabled: true` 설정이 있다면 `pkce` 유형으로 올바르게 인식하도록 수정했습니다.
|
||||
- `clientSummary` 구조체 응답에 `metadata` 필드를 포함시켜 프론트엔드가 상세 설정값을 인지할 수 있도록 개선했습니다.
|
||||
|
||||
### 2. 프론트엔드 API 타입 정의 수정 (`devfront/src/lib/devApi.ts`)
|
||||
- `ClientSummary` 인터페이스에 백엔드에서 전달되는 `metadata?: Record<string, any>` 필드를 추가하여 타입 안정성을 확보했습니다.
|
||||
|
||||
### 3. 다국어 리소스 추가 (`locales/*.toml`)
|
||||
- `ko.toml`, `en.toml`, `template.toml` 파일의 `[ui.dev.clients.type]` 섹션에 `pkce_headless` 키를 추가했습니다.
|
||||
- **한국어**: `"PKCE (Headless Login)"`
|
||||
- **영어**: `"PKCE (Headless Login)"`
|
||||
|
||||
### 4. 연동 앱 목록 UI 개선 (`devfront/src/features/clients/ClientsPage.tsx`)
|
||||
- 클라이언트 목록 테이블의 '유형' 뱃지 렌더링 로직을 수정했습니다.
|
||||
- `client.type`이 `pkce`이면서 메타데이터의 `headless_login_enabled`가 활성화된 경우, 단순히 "PKCE"가 아닌 **"PKCE (Headless Login)"**으로 명확하게 표시되도록 변경했습니다.
|
||||
|
||||
## 검증 결과
|
||||
- **프론트엔드**: `devfront` Playwright E2E 테스트 60개 전체 통과 확인.
|
||||
- **백엔드**: 관련 핸들러 유닛 테스트 정상 통과 확인.
|
||||
- **실제 동작**: Headless Login 설정 앱 생성 후 목록에서 "PKCE (Headless Login)" 배지가 정상 노출됨을 확인했습니다.
|
||||
86
baron-sso/docs/trouble-shooting/userfront-locale.md
Normal file
86
baron-sso/docs/trouble-shooting/userfront-locale.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# userfront locale 전환 문제 분석 및 대응
|
||||
|
||||
## 증상
|
||||
- URL은 `/ko` ↔ `/en`으로 변경되지만 실제 텍스트가 바뀌지 않음.
|
||||
- 브라우저에서 `window.localStorage.getItem('locale')`가 계속 `null`.
|
||||
- 빌드/배포 후에도 동일 증상 유지.
|
||||
|
||||
## 현재 구조 요약
|
||||
- Flutter Web + `easy_localization` 사용.
|
||||
- 경로 기반 로케일: `/:locale` 라우트 + `LocaleGate`로 로케일 적용.
|
||||
- 번역 리소스: `userfront/assets/translations/*.toml`.
|
||||
- 로케일 저장: `localStorage` key `locale`.
|
||||
|
||||
## 이미 적용한 변경
|
||||
- `easy_localization` + TOML loader 적용.
|
||||
- `LocaleGate`에서 로케일 적용 및 저장.
|
||||
- `LanguageSelector`에서 로케일 저장 + URL 변경.
|
||||
- `userfront/assets/translations`에 `en.toml`, `ko.toml`, `template.toml` 포함.
|
||||
- `pubspec.yaml` assets 등록.
|
||||
- GoRouter 호환성 수정(빌드 오류 대응).
|
||||
|
||||
## 원인 후보(우선순위)
|
||||
1) **로케일 저장이 실제로 실행되지 않음**
|
||||
- `LanguageSelector` 클릭 이벤트가 동작하지 않거나,
|
||||
- `LocaleGate`가 호출되지 않는 라우트 진입(예: 라우트 변경 없이 SPA 상태만 변경).
|
||||
|
||||
2) **서비스 워커/캐시로 인해 구 번들이 계속 로드됨**
|
||||
- Flutter Web 기본 `flutter_service_worker.js`가 캐시를 고정.
|
||||
- 기존 번들이 남아 `LocaleStorage.write()` 반영 전 코드가 계속 실행될 가능성.
|
||||
|
||||
3) **번역 리소스 미로딩**
|
||||
- `assets/translations/en.toml`, `ko.toml` 요청이 404 또는 미요청.
|
||||
- Nginx 경로/정적 파일 설정에서 `assets/`가 누락된 경우.
|
||||
|
||||
4) **en.toml 키 누락으로 fallback 문자열이 표시됨**
|
||||
- UI에서 `tr(..., fallback: '한국어')`를 쓰는 경우,
|
||||
en.toml에 키가 없으면 한국어가 그대로 노출됨.
|
||||
|
||||
## 확인 방법(권장 순서)
|
||||
1) **로케일 저장 확인**
|
||||
- 언어 변경 후 콘솔:
|
||||
- `window.localStorage.getItem('locale')`
|
||||
- `null`이면 저장 로직 실행 실패.
|
||||
|
||||
2) **번역 리소스 요청 확인**
|
||||
- DevTools Network에서:
|
||||
- `/assets/translations/ko.toml`
|
||||
- `/assets/translations/en.toml`
|
||||
- 404 또는 미요청이면 assets 로딩 문제.
|
||||
|
||||
3) **로케일 반영 확인**
|
||||
- UI 내 임시 텍스트로 `context.locale.languageCode` 출력
|
||||
- 로케일이 바뀌는데 텍스트가 그대로면 TOML 키 누락 가능성.
|
||||
|
||||
4) **캐시 무효화**
|
||||
- 하드 리로드(Shift+Reload)
|
||||
- service worker 제거 후 재접속
|
||||
|
||||
## 현재까지 실행한 대응
|
||||
- `LocaleGate`에서 로케일 동일 여부와 상관없이 `LocaleStorage.write()`가 실행되도록 수정.
|
||||
- 라우터 로케일 파싱/전환 로직 보강.
|
||||
- 번역 리소스 파일 배치 및 assets 등록.
|
||||
|
||||
## 앞으로의 대응 계획
|
||||
1) **서비스 워커 캐시 무효화 전략**
|
||||
- `flutter_service_worker.js` 버전 갱신 또는 Nginx 캐시 무력화 정책 적용.
|
||||
- 배포 시 `index.html`/`flutter_service_worker.js` 캐시 제어 헤더 추가.
|
||||
|
||||
2) **번역 파일 누락 방지**
|
||||
- `scripts/sync_userfront_locales.sh`를 CI에서 자동 실행.
|
||||
- en/ko 키 누락 검증 테스트 추가.
|
||||
|
||||
3) **로케일 저장 검증**
|
||||
- 언어 변경 후 `localStorage`에 값이 반드시 들어오는지 E2E 체크 추가.
|
||||
|
||||
4) **fallback 사용 최소화**
|
||||
- userfront에서 `fallback` 문자열(한국어) 남용 구간 정리.
|
||||
- en.toml에 키가 없을 때 한국어가 보이는 현상 방지.
|
||||
|
||||
## 참고 파일
|
||||
- `userfront/lib/core/i18n/locale_gate.dart`
|
||||
- `userfront/lib/core/widgets/language_selector.dart`
|
||||
- `userfront/lib/core/i18n/locale_storage_web.dart`
|
||||
- `userfront/lib/core/i18n/toml_asset_loader.dart`
|
||||
- `userfront/assets/translations/en.toml`
|
||||
- `userfront/assets/translations/ko.toml`
|
||||
Reference in New Issue
Block a user