o2o-site-AEO/solution/frontend/src/stores/auth.ts
Mina Choi 4d6802b4f2 [feat] solution/frontend: 에디터에 로그인 사용자 표시 — 6단계엔 신원도 나가는 길도 없었다
위저드(1~5단계)는 AppShell 사이드바가 사용자와 [로그아웃]을 들고 있는데, 에디터(6단계)는
전체 화면이라 AppShell 을 안 쓴다. 그래서 편집 화면에 들어가는 순간 **누구로 로그인했는지도,
나가는 방법도 화면에서 사라졌다.**

- BuilderPage: 에디터 상단 바 오른쪽에 사용자 · 상호와 [로그아웃] 추가
- stores/auth.userLabel: 이름 → 이메일 → 아이디 순. 구글 계정의 로그인 아이디는
  google_<sub> 라 그대로 보이면 안 된다. AppShell 도 같은 규칙을 쓰게 바꿨다
  (기존 `name ?? id` 는 이름이 빈 문자열이면 그대로 통과시켰다)

브라우저 확인: 가입 → 로그인 → 사이드바 '김사장 · 달빛스테이', 에디터 상단 바 동일 표시,
[로그아웃] 클릭 시 RequireAuth 가 /login 으로 되돌림. tsc·eslint·vite build 통과.
2026-09-02 17:29:02 +09:00

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}),
}));