첫 커밋: 로컬 프로젝트 업로드

This commit is contained in:
2026-06-10 15:51:34 +09:00
commit 6a8dbeb2e9
1211 changed files with 312864 additions and 0 deletions

View File

@@ -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`

View 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 오류는 남아 있으나, 목적(연동/동의 저장)은 달성됨

View 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) 정확성 확인
- 코드/링크 만료/재사용 시나리오 점검

View File

@@ -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=...&notice=...` -> locale 보정 후 query 100% 유지
- 비로그인 보호 경로 -> `/[locale]/signin` 이동 시 기존 query 유지
- 인코딩된 nested `redirect_uri`, 중복 query key, fragment 보존

View 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&notice=qr_login_required`
- 기대: `/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2&notice=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`

View File

@@ -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 동시 수집

View File

@@ -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` 통과

View File

@@ -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] 링크 클릭/코드 입력 로그인 실패 재현 및 수정`

View File

@@ -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 문구를 유지합니다.

View 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 흐름을 확인할 수 있습니다.

View File

@@ -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를 정해 마일스톤에 반영하는 것이 좋다.

View File

@@ -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)" 배지가 정상 노출됨을 확인했습니다.

View 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`