kintex/mobile/lib/profile.ts

82 lines
3.1 KiB
TypeScript

/*
* 내정보(프로필) API 래퍼 — 조회·OTP 상태·아바타 업로드.
* 근거: /api/auth/me(KintexPrincipal)·/api/auth/otp/status(OtpStatusResponse).
* ★ 아바타 업로드/서빙 엔드포인트는 백엔드 미구현 — 제안 계약으로 배선하되 미구현(404/501/네트워크)
* 시 degrade(로컬 프리뷰 유지). 인계 노트: _workspace/mobile_myinfo_avatar_contract.md.
*/
import { API_BASE } from './config';
import { ApiRequestError, api, getAccessToken } from './api';
import type {
ApiResponse,
AvatarUploadResponse,
MePrincipal,
OtpStatusDto,
} from './types';
import type { PickedImage } from './imagePick';
import { toPhotoFormData } from './imagePick';
/** 내 신원·행사별 역할(GET /api/auth/me). */
export async function getMe(): Promise<MePrincipal> {
return api.get<MePrincipal>('/api/auth/me');
}
/** 본인 OTP(2차 인증) 상태(GET /api/auth/otp/status). */
export async function getOtpStatus(): Promise<OtpStatusDto> {
return api.get<OtpStatusDto>('/api/auth/otp/status');
}
/**
* 아바타 서빙 URL(제안 계약: GET /api/auth/users/{userId}/photo).
* <Image>는 axios/fetch 인터셉터를 거치지 않으므로 Avatar 컴포넌트에서 Bearer 헤더를 직접 부여한다.
*/
export function avatarUrl(userId: string): string {
return `${API_BASE}/api/auth/users/${encodeURIComponent(userId)}/photo`;
}
/**
* 아바타 업로드(제안 계약: POST /api/auth/profile/photo, multipart field "file").
* 성공 시 photoUrl. 미구현/네트워크 오류는 ApiRequestError로 전파 → 호출부가 degrade 처리.
* 멀티파트는 JSON api 클라이언트를 쓰지 않고 fetch 직접 호출(Content-Type 자동 boundary).
*/
export async function uploadAvatar(img: PickedImage): Promise<AvatarUploadResponse> {
const token = getAccessToken();
const headers: Record<string, string> = {};
if (token) headers.Authorization = `Bearer ${token}`;
// Content-Type은 지정하지 않는다 — RN이 multipart boundary를 자동 설정한다.
let res: Response;
try {
res = await fetch(`${API_BASE}/api/auth/profile/photo`, {
method: 'POST',
headers,
body: toPhotoFormData(img),
});
} catch {
throw new ApiRequestError('NETWORK', '네트워크 연결을 확인해 주세요.', 0);
}
let payload: ApiResponse<AvatarUploadResponse> | null = null;
const text = await res.text();
if (text) {
try {
payload = JSON.parse(text) as ApiResponse<AvatarUploadResponse>;
} catch {
payload = null;
}
}
if (!res.ok || (payload && payload.success === false)) {
const err = payload?.error ?? null;
throw new ApiRequestError(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(err?.code as any) ?? (res.status === 404 ? 'NOT_IMPLEMENTED' : 'UNKNOWN'),
err?.message ?? `프로필 사진 업로드를 처리하지 못했습니다. (${res.status})`,
res.status,
);
}
if (payload?.data?.photoUrl) return payload.data;
// 봉투는 성공이나 data 부재 — 표준 서빙 URL로 폴백.
return { photoUrl: '' };
}