사용자 승인(2026-09-07). 앞서 취향을 localStorage 로 옮겨 **탭·재시작** 문제는 풀었고, 이 변경은 그 위에 **다른 PC 에서도 같은 배치**를 얹음. 사용자가 노트북·데스크톱 두 대를 오감. - `db_management/018_user_ui_prefs.sql` — 사용자당 한 줄, `prefs` JSON 한 칸. 칸을 나누면 취향이 늘 때마다 마이그레이션이 또 필요해 한 칸에 담음. - `GET/PUT /api/dashboard/me/ui-prefs` — 배치 값만. **설계값은 안 담음**(문자열만 받음). - 로그인이 확인된 첫 순간에 한 번 받아 로컬 위에 얹고, 취향이 바뀌면 1.5초 모아 올림. 서버가 없거나 못 읽으면 **로컬 값으로 그대로 돔**(계획서 원문). ⚠ 같은 함정을 또 밟을 뻔했음 — 키만 아는 자리가 저장소를 직접 고르면 **올려보내기도 안 걸림**. 그래서 `storageOf` 를 없애고 **읽기·쓰기 창구 하나**(`readByKey`/`writeByKey`)로 모음. 저장소 선택과 서버 올려보내기가 그 한 곳에만 있음. 그물도 그 이름으로 갱신. **DB 는 아직 적용하지 않았음** — 표가 없으면 API 가 실패하고 화면은 로컬 값으로 도는 것이 정상 동작임. 적용 시점은 다른 창들과 맞춘 뒤. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { syncUiPrefs } from "../A00_Common/b_page_state";
|
|
|
|
export interface ApiResult {
|
|
status: string;
|
|
detail?: string;
|
|
reason?: string;
|
|
}
|
|
|
|
export interface SessionUser {
|
|
id: string;
|
|
email: string;
|
|
name?: string;
|
|
role?: string;
|
|
company_id?: number | null;
|
|
is_master?: boolean;
|
|
}
|
|
|
|
async function post(path: string, body?: object): Promise<ApiResult> {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const result = (await response.json()) as ApiResult;
|
|
if (!response.ok) throw new Error(result.detail ?? "Request failed");
|
|
return result;
|
|
}
|
|
|
|
export function requestLogin(email: string, password: string): Promise<ApiResult> {
|
|
return post("/auth/login/request", { email, password });
|
|
}
|
|
|
|
export function verifyLogin(email: string, otpCode: string): Promise<ApiResult> {
|
|
clearSessionCache();
|
|
return post("/auth/login/verify", { email, otp_code: otpCode });
|
|
}
|
|
|
|
/* 세션 조회는 한 화면을 여는 동안 라우터 가드·상단 바·워크플로 확인이 각각 부른다 —
|
|
실측 B05 진입에서 `GET /auth/session` 이 네 번 나갔다(2026-09-06). 같은 답을 네 번
|
|
받을 이유가 없으므로 **짧은 시간 동안 한 번만** 부르고 나눠 쓴다. 로그인·로그아웃
|
|
에서는 즉시 버려 다음 조회가 서버를 다시 본다. */
|
|
const SESSION_CACHE_MS = 5000;
|
|
let sessionCache: { at: number; value: Promise<SessionUser | null> } | null = null;
|
|
/** 계정 취향 받아오기는 한 번만 — 세션 조회는 화면마다 여러 번 불린다. */
|
|
let uiPrefsSynced = false;
|
|
|
|
/** 세션 캐시를 버린다 — 로그인·로그아웃처럼 상태가 바뀌는 자리에서 부른다. */
|
|
export function clearSessionCache(): void {
|
|
sessionCache = null;
|
|
// 로그인·로그아웃이면 다음 세션에서 취향을 다시 받아 온다(다른 계정일 수 있다).
|
|
uiPrefsSynced = false;
|
|
}
|
|
|
|
function sessionOnce(): Promise<SessionUser | null> {
|
|
const now = Date.now();
|
|
if (sessionCache && now - sessionCache.at < SESSION_CACHE_MS) return sessionCache.value;
|
|
const value = (async (): Promise<SessionUser | null> => {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/auth/session`, { credentials: "include" });
|
|
if (!response.ok) return null;
|
|
const data = (await response.json()) as { status: string; user?: SessionUser };
|
|
// 로그인이 확인된 첫 순간에 계정 취향을 한 번 받아 온다 — PC 를 바꿔도 같은 배치가
|
|
// 되게(2026-09-07). 실패해도 화면은 브라우저에 남은 값으로 그대로 돈다.
|
|
if (data.user && !uiPrefsSynced) {
|
|
uiPrefsSynced = true;
|
|
void syncUiPrefs();
|
|
}
|
|
return data.user ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
sessionCache = { at: now, value };
|
|
return value;
|
|
}
|
|
|
|
export async function fetchSession(): Promise<boolean> {
|
|
return (await sessionOnce()) !== null;
|
|
}
|
|
|
|
export async function fetchSessionUser(): Promise<SessionUser | null> {
|
|
return sessionOnce();
|
|
}
|
|
|
|
export function logout(): Promise<ApiResult> {
|
|
clearSessionCache();
|
|
return post("/auth/logout");
|
|
}
|