Add 로그인과정.md
This commit is contained in:
552
로그인과정.md
Normal file
552
로그인과정.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# localhost:5000 로그인 화면 호출 흐름
|
||||
|
||||
이 문서는 브라우저에서 `http://localhost:5000/`로 접근했을 때 UserFront 로그인 화면이 뜨기까지의 순차 흐름을 정리합니다.
|
||||
|
||||
작성 기준:
|
||||
|
||||
- 로컬 URL: `http://localhost:5000/`
|
||||
- gateway: `baron_gateway`
|
||||
- UserFront upstream: `baron_userfront:5000`
|
||||
- 로그인 화면 구현 파일: `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart`
|
||||
|
||||
## 1. 결론
|
||||
|
||||
로그인 화면의 대표 URL은 다음입니다.
|
||||
|
||||
- 한국어: `http://localhost:5000/ko/signin`
|
||||
- 영어: `http://localhost:5000/en/signin`
|
||||
|
||||
로그인 화면의 실제 소스 파일 절대경로는 다음입니다.
|
||||
|
||||
```text
|
||||
/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart
|
||||
```
|
||||
|
||||
`http://localhost:5000/`로 접근하면 gateway가 UserFront 정적 앱을 내려주고, Flutter 앱이 기동된 뒤 `GoRouter`가 locale과 세션 상태를 판단하여 미로그인 사용자를 `/{locale}/signin`으로 보냅니다.
|
||||
|
||||
## 2. 전체 순차 흐름
|
||||
|
||||
### 2.1 브라우저 최초 요청
|
||||
|
||||
1. 브라우저가 다음 URL을 요청합니다.
|
||||
|
||||
```text
|
||||
GET http://localhost:5000/
|
||||
```
|
||||
|
||||
2. 요청은 Docker host port `5000`으로 들어오고 `baron_gateway` Nginx가 받습니다.
|
||||
|
||||
관련 파일:
|
||||
|
||||
```text
|
||||
/mnt/e/h_workspace/baron-sso/gateway/nginx.conf
|
||||
```
|
||||
|
||||
관련 설정:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 5000;
|
||||
|
||||
set $backend_upstream http://baron_backend:3000;
|
||||
set $userfront_upstream http://baron_userfront:5000;
|
||||
set $oathkeeper_upstream http://oathkeeper:4455;
|
||||
|
||||
location / {
|
||||
proxy_pass $userfront_upstream;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. `/`는 `location /`에 매칭되고 `baron_userfront:5000`으로 proxy됩니다.
|
||||
|
||||
```text
|
||||
browser -> localhost:5000 -> baron_gateway -> baron_userfront:5000
|
||||
```
|
||||
|
||||
### 2.2 UserFront 정적 bootstrap 응답
|
||||
|
||||
4. UserFront가 Flutter Web entry HTML을 반환합니다.
|
||||
|
||||
관련 파일:
|
||||
|
||||
```text
|
||||
/mnt/e/h_workspace/baron-sso/userfront/web/index.html
|
||||
```
|
||||
|
||||
5. `index.html`은 처음에 bootstrap shell을 보여줍니다. 이 시점에 보이는 것은 실제 Flutter 로그인 화면이 아니라 로딩용 HTML입니다.
|
||||
|
||||
주요 DOM:
|
||||
|
||||
```html
|
||||
<main id="baron-bootstrap-shell" aria-live="polite">
|
||||
<h1>Baron SW Portal</h1>
|
||||
<div class="loader" aria-hidden="true"></div>
|
||||
<p>Loading sign-in</p>
|
||||
<div class="signin-preview" aria-hidden="true">Sign in</div>
|
||||
</main>
|
||||
```
|
||||
|
||||
6. `index.html`은 로컬 개발 환경에서 기존 service worker와 Flutter cache를 제거한 뒤 `flutter_bootstrap.js`를 로드합니다.
|
||||
|
||||
사용 함수/동작:
|
||||
|
||||
- `loadFlutter()`
|
||||
- `navigator.serviceWorker.getRegistrations()`
|
||||
- `registration.unregister()`
|
||||
- `caches.keys()`
|
||||
- `caches.delete()`
|
||||
|
||||
이후 브라우저는 일반적으로 다음 정적 리소스를 가져옵니다.
|
||||
|
||||
```text
|
||||
GET /flutter_bootstrap.js
|
||||
GET /manifest.json
|
||||
GET /main.dart.mjs
|
||||
GET /canvaskit/skwasm.js
|
||||
GET /canvaskit/skwasm.wasm
|
||||
GET /favicon.ico
|
||||
```
|
||||
|
||||
### 2.3 Flutter 앱 시작
|
||||
|
||||
7. Flutter runtime이 `main.dart`의 `main()`을 실행합니다.
|
||||
|
||||
관련 파일:
|
||||
|
||||
```text
|
||||
/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart
|
||||
```
|
||||
|
||||
주요 함수:
|
||||
|
||||
- `main()`
|
||||
- `WidgetsFlutterBinding.ensureInitialized()`
|
||||
- `usePathUrlStrategy()`
|
||||
- `EasyLocalization.ensureInitialized()`
|
||||
- `LocaleRegistry.primeWithDefaults()`
|
||||
- `LoggerService.init()`
|
||||
- `runApp(...)`
|
||||
|
||||
8. `runApp()`은 다음 wrapper를 구성합니다.
|
||||
|
||||
```text
|
||||
EasyLocalization
|
||||
-> ProviderScope
|
||||
-> BaronSSOApp
|
||||
```
|
||||
|
||||
9. `BaronSSOApp.build()`가 `MaterialApp.router`를 생성하고 `_router`를 연결합니다.
|
||||
|
||||
주요 함수/객체:
|
||||
|
||||
- `BaronSSOApp`
|
||||
- `_BaronSSOAppState.build()`
|
||||
- `MaterialApp.router(...)`
|
||||
- `routerConfig: _router`
|
||||
|
||||
## 3. GoRouter 라우팅 흐름
|
||||
|
||||
라우터는 `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart`에 정의되어 있습니다.
|
||||
|
||||
주요 객체:
|
||||
|
||||
```dart
|
||||
final _router = GoRouter(
|
||||
initialLocation: '/',
|
||||
refreshListenable: AuthNotifier.instance,
|
||||
routes: [...],
|
||||
redirect: (context, state) { ... },
|
||||
);
|
||||
```
|
||||
|
||||
### 3.1 locale 결정
|
||||
|
||||
`http://localhost:5000/`에는 locale path segment가 없습니다. 따라서 앱은 선호 locale을 계산합니다.
|
||||
|
||||
사용 함수:
|
||||
|
||||
- `extractLocaleFromPath(Uri uri)`
|
||||
- `resolvePreferredLocaleCode()`
|
||||
- `normalizeLocaleCode(String? code)`
|
||||
- `buildLocalizedPath(String localeCode, Uri uri)`
|
||||
- `buildLocalizedHomePath(Uri uri, {String? preferredLocaleCode})`
|
||||
|
||||
관련 파일:
|
||||
|
||||
```text
|
||||
/mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart
|
||||
```
|
||||
|
||||
locale 결정 기준:
|
||||
|
||||
1. 저장된 locale이 있으면 사용
|
||||
2. 없으면 브라우저/디바이스 locale 사용
|
||||
3. 지원하지 않는 값이면 fallback locale 사용
|
||||
|
||||
따라서 한국어 환경에서는 보통 `ko`, 영어 환경에서는 `en`이 됩니다.
|
||||
|
||||
### 3.2 세션 상태 확인
|
||||
|
||||
라우터와 앱 초기화 코드는 로컬 토큰 또는 쿠키 기반 세션 여부를 봅니다.
|
||||
|
||||
사용 함수:
|
||||
|
||||
- `_hasActiveLocalSession()`
|
||||
- `AuthTokenStore.getToken()`
|
||||
- `AuthTokenStore.usesCookie()`
|
||||
- `_shouldRunStartupSessionRecovery(Uri uri)`
|
||||
- `_silentSessionRecovery()`
|
||||
- `AuthProxyService.getMe()`
|
||||
- `AuthProxyService.getSessionStatus()`
|
||||
|
||||
관련 파일:
|
||||
|
||||
```text
|
||||
/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart
|
||||
/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart
|
||||
/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart
|
||||
```
|
||||
|
||||
세션 확인 시 호출될 수 있는 backend API:
|
||||
|
||||
```text
|
||||
GET /api/v1/user/me
|
||||
```
|
||||
|
||||
gateway 기준 흐름:
|
||||
|
||||
```text
|
||||
browser -> /api/v1/user/me -> baron_gateway location /api -> baron_backend:3000
|
||||
```
|
||||
|
||||
### 3.3 미로그인 상태에서 최종 로그인 route로 이동
|
||||
|
||||
미로그인 상태라면 최종적으로 다음 경로로 이동합니다.
|
||||
|
||||
```text
|
||||
/{locale}/signin
|
||||
```
|
||||
|
||||
예:
|
||||
|
||||
```text
|
||||
http://localhost:5000/ko/signin
|
||||
http://localhost:5000/en/signin
|
||||
```
|
||||
|
||||
관련 redirect 함수:
|
||||
|
||||
- root route redirect: `buildLocalizedHomePath(...)`
|
||||
- global router redirect: `redirect: (context, state) { ... }`
|
||||
- private route guard: `_redirectPrivateLocaleRoute(...)`
|
||||
- locale entry fallback: `LocaleEntryRedirectScreen._redirect()`
|
||||
- signin path builder: `buildSigninRedirectPath(...)`
|
||||
|
||||
중요 분기:
|
||||
|
||||
- locale이 없으면 locale이 붙은 path로 보정합니다.
|
||||
- public auth path는 그대로 통과합니다.
|
||||
- 로그인하지 않았고 private path 또는 locale root에 있으면 `/{locale}/signin`으로 보냅니다.
|
||||
- 로그인되어 있고 locale root에 있으면 `/{locale}/dashboard`로 보냅니다.
|
||||
|
||||
## 4. 로그인 화면 route와 화면 생성
|
||||
|
||||
로그인 화면 route는 `main.dart`의 `/:locale/signin`입니다.
|
||||
|
||||
브라우저 URL:
|
||||
|
||||
```text
|
||||
http://localhost:5000/ko/signin
|
||||
```
|
||||
|
||||
라우터 path:
|
||||
|
||||
```text
|
||||
/:locale/signin
|
||||
```
|
||||
|
||||
관련 코드 구조:
|
||||
|
||||
```dart
|
||||
GoRoute(
|
||||
path: 'signin',
|
||||
builder: (context, state) {
|
||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||
final redirectUrl =
|
||||
state.uri.queryParameters['redirect_uri'] ??
|
||||
state.uri.queryParameters['redirect_url'];
|
||||
return ScopedTheme(
|
||||
controller: ThemeController.auth,
|
||||
child: LoginScreen(
|
||||
key: state.pageKey,
|
||||
loginChallenge: loginChallenge,
|
||||
redirectUrl: redirectUrl,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
생성되는 widget:
|
||||
|
||||
```text
|
||||
ScopedTheme
|
||||
-> LoginScreen
|
||||
```
|
||||
|
||||
로그인 화면 구현 파일:
|
||||
|
||||
```text
|
||||
/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart
|
||||
```
|
||||
|
||||
주요 class/function:
|
||||
|
||||
- `LoginScreen`
|
||||
- `_LoginScreenState`
|
||||
- `_LoginScreenState.initState()`
|
||||
- `_LoginScreenState.build()`
|
||||
- `_handlePasswordLogin()`
|
||||
- `_handleLinkLogin()`
|
||||
- `_startEnchantedFlow()`
|
||||
- `_pollForSession()`
|
||||
- `_startQrFlow()`
|
||||
- `_startQrPolling()`
|
||||
- `_tryCookieSession()`
|
||||
- `_attemptOidcAutoAccept()`
|
||||
- `_acceptOidcLoginAndRedirect()`
|
||||
- `_onLoginSuccess()`
|
||||
|
||||
## 5. LoginScreen 초기화 흐름
|
||||
|
||||
`LoginScreen`이 생성되면 `_LoginScreenState.initState()`가 실행됩니다.
|
||||
|
||||
초기화 순서:
|
||||
|
||||
1. `TabController(length: 3)` 생성
|
||||
2. password/link/QR 입력 controller 준비
|
||||
3. `drySend` query parameter 확인
|
||||
4. `redirect_uri` 또는 `redirect_url` query parameter 확인
|
||||
5. `login_challenge` 확인
|
||||
6. magic link, short code, verification token이 있는지 확인
|
||||
7. QR 로그인 필요 안내 notice 확인
|
||||
8. OIDC login challenge가 있으면 자동 accept 시도
|
||||
9. tenant-info 조회로 login ID label 커스터마이징
|
||||
10. login challenge가 없으면 쿠키 세션 확인
|
||||
|
||||
초기 진입이 단순 `http://localhost:5000/`인 경우 보통 query parameter가 없으므로 다음 정도가 수행됩니다.
|
||||
|
||||
```text
|
||||
LoginScreen.initState()
|
||||
-> _resolveLoginChallenge()
|
||||
-> _attemptOidcAutoAccept() // login_challenge 없으면 바로 return
|
||||
-> AuthProxyService.getTenantInfo()
|
||||
-> _tryCookieSession()
|
||||
```
|
||||
|
||||
초기 화면 렌더링에 영향을 줄 수 있는 backend API:
|
||||
|
||||
```text
|
||||
GET /api/v1/auth/tenant-info
|
||||
GET /api/v1/user/me
|
||||
```
|
||||
|
||||
## 6. 로그인 화면 UI 구성
|
||||
|
||||
`LoginScreen.build()`는 탭 기반 로그인 UI를 구성합니다.
|
||||
|
||||
탭:
|
||||
|
||||
```text
|
||||
1. 비밀번호
|
||||
2. 로그인 링크
|
||||
3. QR 코드
|
||||
```
|
||||
|
||||
관련 UI 코드:
|
||||
|
||||
- `TabBar`
|
||||
- `TabBarView`
|
||||
- password tab
|
||||
- link tab
|
||||
- QR tab
|
||||
|
||||
### 6.1 비밀번호 탭
|
||||
|
||||
UI 요소:
|
||||
|
||||
- 로그인 ID 입력
|
||||
- 비밀번호 입력
|
||||
- 로그인 버튼
|
||||
|
||||
주요 key:
|
||||
|
||||
```text
|
||||
password_login_id_input
|
||||
password_login_password_input
|
||||
password_login_submit_button
|
||||
```
|
||||
|
||||
사용 함수:
|
||||
|
||||
- `_handlePasswordLogin()`
|
||||
- `AuthProxyService.loginWithPassword(...)`
|
||||
- `_onLoginSuccess(...)`
|
||||
|
||||
호출 API:
|
||||
|
||||
```text
|
||||
POST /api/v1/auth/password/login
|
||||
```
|
||||
|
||||
### 6.2 로그인 링크 탭
|
||||
|
||||
UI 요소:
|
||||
|
||||
- 로그인 ID 입력
|
||||
- 로그인 링크 전송 버튼
|
||||
- short code 입력 UI
|
||||
- 재전송 버튼
|
||||
|
||||
사용 함수:
|
||||
|
||||
- `_handleLinkLogin()`
|
||||
- `_startEnchantedFlow(...)`
|
||||
- `_pollForSession(...)`
|
||||
- `_verifyShortCode(...)`
|
||||
- `AuthProxyService.initEnchantedLink(...)`
|
||||
- `AuthProxyService.pollEnchantedLink(...)`
|
||||
- `AuthProxyService.verifyLoginShortCode(...)`
|
||||
|
||||
호출 API:
|
||||
|
||||
```text
|
||||
POST /api/v1/auth/enchanted-link/init
|
||||
POST /api/v1/auth/enchanted-link/poll
|
||||
POST /api/v1/auth/login/code/verify-short
|
||||
```
|
||||
|
||||
### 6.3 QR 코드 탭
|
||||
|
||||
QR 탭은 선택될 때 QR flow를 시작합니다.
|
||||
|
||||
사용 함수:
|
||||
|
||||
- `_handleTabSelection()`
|
||||
- `_startQrFlow()`
|
||||
- `_startQrPolling()`
|
||||
- `_startCountdown()`
|
||||
- `AuthProxyService.initQrLogin()`
|
||||
- `AuthProxyService.pollQrStatus(...)`
|
||||
|
||||
호출 API:
|
||||
|
||||
```text
|
||||
POST /api/v1/auth/qr/init
|
||||
POST /api/v1/auth/qr/poll
|
||||
```
|
||||
|
||||
## 7. OIDC login_challenge가 있는 경우
|
||||
|
||||
외부 RP 또는 Hydra 흐름에서 다음과 같은 URL로 들어올 수 있습니다.
|
||||
|
||||
```text
|
||||
http://localhost:5000/ko/signin?login_challenge=...
|
||||
```
|
||||
|
||||
이 경우 `LoginScreen`은 `login_challenge`를 보관하고 다음을 시도합니다.
|
||||
|
||||
1. `_attemptOidcAutoAccept()`
|
||||
2. 로컬 token 또는 cookie session이 있으면 backend에 login accept 요청
|
||||
3. backend가 반환한 `redirectTo`로 브라우저 이동
|
||||
|
||||
사용 함수:
|
||||
|
||||
- `_resolveLoginChallenge(...)`
|
||||
- `_attemptOidcAutoAccept()`
|
||||
- `_acceptOidcLoginAndRedirect(...)`
|
||||
- `_redirectToOidcTarget(...)`
|
||||
- `AuthProxyService.acceptOidcLogin(...)`
|
||||
|
||||
호출 API:
|
||||
|
||||
```text
|
||||
POST /api/v1/auth/oidc/login/accept
|
||||
```
|
||||
|
||||
로그인 화면에서 사용자가 비밀번호 로그인을 완료한 뒤에도 `_onLoginSuccess()`가 `login_challenge`를 감지하면 같은 accept 흐름을 수행합니다.
|
||||
|
||||
## 8. 페이지/route 목록
|
||||
|
||||
UserFront auth 관련 route는 locale 아래에 붙습니다.
|
||||
|
||||
| 브라우저 절대 URL 예시 | GoRouter path | 생성 화면 | 구현 파일 |
|
||||
| --- | --- | --- | --- |
|
||||
| `http://localhost:5000/ko/signin` | `/:locale/signin` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` |
|
||||
| `http://localhost:5000/ko/login` | `/:locale/login` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` |
|
||||
| `http://localhost:5000/ko/signup` | `/:locale/signup` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` |
|
||||
| `http://localhost:5000/ko/registration` | `/:locale/registration` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` |
|
||||
| `http://localhost:5000/ko/consent?consent_challenge=...` | `/:locale/consent` | `ConsentScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/consent_screen.dart` |
|
||||
| `http://localhost:5000/ko/forgot-password` | `/:locale/forgot-password` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` |
|
||||
| `http://localhost:5000/ko/recovery` | `/:locale/recovery` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` |
|
||||
| `http://localhost:5000/ko/reset-password` | `/:locale/reset-password` | `ResetPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/reset_password_screen.dart` |
|
||||
| `http://localhost:5000/ko/error` | `/:locale/error` | `ErrorScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/error_screen.dart` |
|
||||
| `http://localhost:5000/ko/dashboard` | `/:locale/dashboard` | `DashboardScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/dashboard/presentation/dashboard_screen.dart` |
|
||||
|
||||
## 9. 가장 중요한 파일 요약
|
||||
|
||||
| 목적 | 파일 절대경로 |
|
||||
| --- | --- |
|
||||
| gateway proxy 설정 | `/mnt/e/h_workspace/baron-sso/gateway/nginx.conf` |
|
||||
| Flutter Web HTML bootstrap | `/mnt/e/h_workspace/baron-sso/userfront/web/index.html` |
|
||||
| 앱 시작점과 GoRouter 정의 | `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart` |
|
||||
| 로그인 화면 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` |
|
||||
| 로그인/API proxy client | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart` |
|
||||
| token/cookie session store adapter | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart` |
|
||||
| locale path helper | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart` |
|
||||
| public auth path 판정 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/domain/login_link_route_policy.dart` |
|
||||
|
||||
## 10. 짧은 호출 흐름 다이어그램
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Browser
|
||||
participant G as baron_gateway<br/>localhost:5000
|
||||
participant UF as baron_userfront:5000
|
||||
participant FE as Flutter UserFront
|
||||
participant BE as baron_backend:3000
|
||||
|
||||
B->>G: GET /
|
||||
G->>UF: proxy GET /
|
||||
UF-->>G: index.html
|
||||
G-->>B: index.html
|
||||
B->>G: GET /flutter_bootstrap.js
|
||||
G->>UF: proxy static asset
|
||||
B->>G: GET /main.dart.mjs 등
|
||||
G->>UF: proxy static assets
|
||||
FE->>FE: main()
|
||||
FE->>FE: MaterialApp.router(_router)
|
||||
FE->>FE: locale 결정
|
||||
FE->>FE: local token/cookie 상태 확인
|
||||
alt 미로그인
|
||||
FE->>FE: route -> /{locale}/signin
|
||||
FE->>BE: GET /api/v1/auth/tenant-info
|
||||
FE->>BE: GET /api/v1/user/me (cookie/session check 가능)
|
||||
FE->>B: LoginScreen 렌더링
|
||||
else 로그인됨
|
||||
FE->>FE: route -> /{locale}/dashboard
|
||||
end
|
||||
```
|
||||
|
||||
## 11. 디버깅 포인트
|
||||
|
||||
로그인 화면이 뜨지 않을 때 먼저 확인할 것:
|
||||
|
||||
1. `baron_gateway`가 떠 있는지
|
||||
2. `baron_userfront`가 healthy인지
|
||||
3. gateway 로그에 `baron_userfront could not be resolved` 또는 `connect() failed`가 있는지
|
||||
4. 브라우저 Network 탭에서 `/flutter_bootstrap.js`, `/main.dart.mjs`가 200인지
|
||||
5. Flutter route가 `/ko/signin` 또는 `/en/signin`으로 이동하는지
|
||||
6. `/api/v1/auth/tenant-info`, `/api/v1/user/me` 실패가 화면 렌더링을 막는지
|
||||
|
||||
Reference in New Issue
Block a user