Files
Aislo/A06_Login/A06_Login_Api_Fetch.ts
T
eomsangdonandClaude Opus 5 8a0a8f18e3 feat(화면): 화면 배치를 계정에 저장 — PC 를 바꿔도 따라오게
사용자 승인(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>
2026-09-07 14:50:31 +09:00

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