위저드(1~5단계)는 AppShell 사이드바가 사용자와 [로그아웃]을 들고 있는데, 에디터(6단계)는 전체 화면이라 AppShell 을 안 쓴다. 그래서 편집 화면에 들어가는 순간 **누구로 로그인했는지도, 나가는 방법도 화면에서 사라졌다.** - BuilderPage: 에디터 상단 바 오른쪽에 사용자 · 상호와 [로그아웃] 추가 - stores/auth.userLabel: 이름 → 이메일 → 아이디 순. 구글 계정의 로그인 아이디는 google_<sub> 라 그대로 보이면 안 된다. AppShell 도 같은 규칙을 쓰게 바꿨다 (기존 `name ?? id` 는 이름이 빈 문자열이면 그대로 통과시켰다) 브라우저 확인: 가입 → 로그인 → 사이드바 '김사장 · 달빛스테이', 에디터 상단 바 동일 표시, [로그아웃] 클릭 시 RequireAuth 가 /login 으로 되돌림. tsc·eslint·vite build 통과.
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import {create} from 'zustand';
|
|
import {clearTokens, getAccessToken, setTokens, UserRole} from '@/api';
|
|
import type {ResMe} from '@/api';
|
|
|
|
export interface AuthUser {
|
|
userId: string;
|
|
id: string;
|
|
name?: string;
|
|
email?: string;
|
|
role: number;
|
|
companyId?: string;
|
|
companyName?: string;
|
|
}
|
|
|
|
/**
|
|
* `GET /v1/auth/me` 응답 → 화면이 쓰는 사용자.
|
|
*
|
|
* ★ 생성 모델의 필드가 전부 optional 인 것이 이상해 보이지만 맞다 — 백엔드가 기본값을 준
|
|
* 필드(`user_id: str = ""`)는 OpenAPI 에서 required 가 아니고, 응답은 `RemoveNoneResponse`
|
|
* 를 지나며 None 필드가 키째 사라진다. 그래서 없을 수 있다고 보고 여기 한 곳에서만 메운다.
|
|
*/
|
|
export function toAuthUser(res: ResMe): AuthUser {
|
|
return {
|
|
userId: res.user_id ?? '',
|
|
id: res.id ?? '',
|
|
name: res.name ?? undefined,
|
|
email: res.email ?? undefined,
|
|
role: res.role ?? UserRole.USER,
|
|
companyId: res.company?.company_id,
|
|
companyName: res.company?.name,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 화면에 띄울 이름. 구글 계정의 로그인 아이디는 `google_<sub>` 라 그대로 보이면 안 된다 —
|
|
* 이름 → 이메일 순으로 떨어뜨리고 아이디는 마지막이다.
|
|
*/
|
|
export function userLabel(user: AuthUser): string {
|
|
return user.name || user.email || user.id;
|
|
}
|
|
|
|
interface AuthState {
|
|
user: AuthUser | null;
|
|
/** 부팅 시 저장된 토큰을 확인하기 전까지 true. 가드가 이 동안 리다이렉트를 미룬다. */
|
|
isRestoring: boolean;
|
|
isAuthenticated: () => boolean;
|
|
signIn: (tokens: {accessToken: string; refreshToken?: string}, user: AuthUser) => void;
|
|
signOut: () => void;
|
|
setUser: (user: AuthUser | null) => void;
|
|
finishRestore: () => void;
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>((set, get) => ({
|
|
user: null,
|
|
isRestoring: true,
|
|
|
|
// 토큰 보관소는 custom-fetch 가 유일한 소스다. 스토어가 사본을 들지 않는다 —
|
|
// 두 곳에 두면 refresh 로 갱신됐을 때 한쪽만 낡는다.
|
|
isAuthenticated: () => Boolean(getAccessToken()) && Boolean(get().user),
|
|
|
|
signIn: (tokens, user) => {
|
|
setTokens(tokens.accessToken, tokens.refreshToken ?? null);
|
|
set({user, isRestoring: false});
|
|
},
|
|
|
|
signOut: () => {
|
|
clearTokens();
|
|
set({user: null, isRestoring: false});
|
|
},
|
|
|
|
setUser: (user) => set({user}),
|
|
finishRestore: () => set({isRestoring: false}),
|
|
}));
|