o2o-negosium-original/negodata/front/src/features/auth/service.ts
Mina Choi a40257ddc1 [feat] negodata: 회원관리 / 최고관리자(OWNER)
- OWNER 권한 신설, 최고관리자가 자기 회사 소속 직원(USER) 계정 생성·수정·삭제 관리
- 회사 사용자 API(/v1/company/user/*), 회원관리 페이지·폼
- 로그인/프로필 흐름 정비, 무인증 계정생성(/auth/create) 제거

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:31:32 +09:00

109 lines
3.9 KiB
TypeScript

import {setAccessToken, customFetch} from '../../api/mutator/custom-fetch';
import {
login as loginRequest,
refreshToken as refreshRequest,
me as fetchMe,
} from '../../api/generated/auth/auth';
import type {ResMe} from '../../api/generated/model/resMe';
import type {ErrorInfo} from '../../api/generated/model/errorInfo';
import {useAuthStore, type AuthUser, type UserRole} from '../../stores/auth';
import {UserRole as UserRoleCode} from '../../api/generated/model';
import {USER_ROLE_LABEL} from '../../lib/enumLabels';
import {toMessage, resultMessage} from '../../lib/apiError';
const ACCESS_KEY = 'negodata.accessToken';
const REFRESH_KEY = 'negodata.refreshToken';
// 백엔드 공통 응답: { result: {success, code, desc}, ...payload }.
// result.success 가 false 면 desc 로 throw (HTTP 4xx 는 customFetch 가 ApiError 로 던짐).
function ensureOk<T extends {result?: ErrorInfo}>(res: T): T {
const r = res.result;
if (r && r.success === false) throw new Error(r.desc || String(r.code ?? '') || 'request failed');
return res;
}
function toAuthUser(me: ResMe): AuthUser {
return {
userId: me.user_id ?? '',
company: me.company?.name ?? '',
name: me.name ?? me.id ?? '',
loginId: me.id ?? '',
email: me.email ?? '',
contact: me.contact_number ?? '',
role: (USER_ROLE_LABEL[me.role ?? UserRoleCode.USER] ?? '일반') as UserRole,
};
}
async function restore(): Promise<void> {
const access = localStorage.getItem(ACCESS_KEY);
const refresh = localStorage.getItem(REFRESH_KEY);
if (!access && !refresh) return;
if (access) setAccessToken(access);
try {
const me = ensureOk(await fetchMe());
useAuthStore.getState().setUser(toAuthUser(me));
} catch {
try {
if (!refresh) throw new Error('no refresh token');
// refresh 토큰은 Authorization 헤더(Bearer)로 보낸다 → accessToken 슬롯에 잠시 실어 보냄.
setAccessToken(refresh);
const {access_token = ''} = ensureOk(await refreshRequest());
localStorage.setItem(ACCESS_KEY, access_token);
setAccessToken(access_token);
const me = ensureOk(await fetchMe());
useAuthStore.getState().setUser(toAuthUser(me));
} catch {
localStorage.removeItem(ACCESS_KEY);
localStorage.removeItem(REFRESH_KEY);
setAccessToken(null);
}
}
}
let restorePromise: Promise<void> | null = null;
export function initAuth(): Promise<void> {
if (!restorePromise) restorePromise = restore();
return restorePromise;
}
export async function login(loginId: string, password: string): Promise<ResMe> {
const tokens = ensureOk(await loginRequest({id: loginId, password}));
localStorage.setItem(ACCESS_KEY, tokens.access_token ?? '');
localStorage.setItem(REFRESH_KEY, tokens.refresh_token ?? '');
setAccessToken(tokens.access_token ?? null);
const me = ensureOk(await fetchMe());
useAuthStore.getState().setUser(toAuthUser(me));
return me;
}
export interface UpdateMePayload {
name?: string | null;
email?: string | null;
contact_number?: string | null;
password?: string | null;
}
// 본인 정보 수정(PATCH /v1/auth/me). 성공 시 최신 정보로 스토어 갱신.
// HTTP 에러(ApiError)·응답봉투 모두 toMessage/resultMessage 로 한글 통일.
export async function updateMe(payload: UpdateMePayload): Promise<ResMe> {
let me: ResMe;
try {
me = await customFetch<ResMe>({url: '/v1/auth/me', method: 'PATCH', data: payload});
} catch (err) {
throw new Error(toMessage(err));
}
const msg = resultMessage(me.result);
if (msg) throw new Error(msg);
useAuthStore.getState().setUser(toAuthUser(me));
return me;
}
// 백엔드에 logout 엔드포인트가 없으므로 클라이언트 상태만 비운다.
export async function logout(): Promise<void> {
localStorage.removeItem(ACCESS_KEY);
localStorage.removeItem(REFRESH_KEY);
setAccessToken(null);
useAuthStore.getState().setUser(null);
}