- frontend: visitor public home (AiAssistant, AiPlanningBriefing, VisitorTrackPage), 3-track public routing (PublicShell/App), i18n ko/en/zh/ja, favicon - backend: public AI visitor-assistant + event calendar API (publicsite/*), profile avatar API (auth/profile/*), SecurityConfig CORS whitelist, SecretStartupValidator (B12 fail-fast, prod only), application-prod.yml, AppIntegrity, /api/auth/me expansion (MeResponse) - db: V42 bulk demo seed, V43 visitor_guide/transport + event calendar view, V44 performance indexes, V45 app_user profile photo columns (all idempotent) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
3.1 KiB
TypeScript
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: '' };
|
|
}
|