f78949c21a
Build & Deploy / build-and-deploy (push) Failing after 9s
Backend (server/): - Fastify + Prisma + PostgreSQL 16 - JWT 인증 (bcrypt) + 카카오 OAuth (/auth/kakao — kapi.kakao.com 호출) - REST API: auth, users, family, policies, claims, score, notifications, diagnosis, consults - 실제 보험점수 알고리즘 (카테고리별 가중치·최소보장 기반) - Multipart 업로드 (영수증/진단서 → 디스크 persistence) - Swagger UI /docs Client: - api/client.ts + api/endpoints.ts (fetch 래퍼 + AsyncStorage 토큰) - 인증 스토어 (hydrate/login/register/kakao/logout) - 로그인/회원가입 화면 + 카카오 버튼 - 홈/내보험/가족/점수/청구 API 연동 (pull-to-refresh) - 보험 추가 모달 + 가족 구성원 추가 모달 - 로그인 전/후 스택 분기 (RootNavigator) Infra: - docker-compose.yml (로컬 Postgres+API) - server/Dockerfile (Prisma migrate deploy + node) - deploy/k8s/postgres.yaml (StatefulSet + 10Gi PVC) - deploy/k8s/api.yaml (Deployment + Ingress api.insurance.junggomoa.com) - CI workflow 확장 (web + api 동시 빌드·배포) - POSTGRES_PASSWORD / JWT_SECRET Gitea Secrets 추가 필요 - 반응형 웹 레이아웃 (max-width 480px 폰 프레임) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
2.3 KiB
TypeScript
93 lines
2.3 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { Platform } from 'react-native';
|
|
|
|
const defaultBase =
|
|
Platform.OS === 'web'
|
|
? (typeof window !== 'undefined' && (window as any).__API_BASE__) || 'http://localhost:4000'
|
|
: 'http://10.0.2.2:4000';
|
|
|
|
export const API_BASE = (process.env.EXPO_PUBLIC_API_BASE as string) || defaultBase;
|
|
|
|
const TOKEN_KEY = 'insurance.token';
|
|
|
|
let memoryToken: string | null = null;
|
|
|
|
export async function loadToken() {
|
|
if (memoryToken) return memoryToken;
|
|
try {
|
|
memoryToken = await AsyncStorage.getItem(TOKEN_KEY);
|
|
} catch {
|
|
memoryToken = null;
|
|
}
|
|
return memoryToken;
|
|
}
|
|
export async function saveToken(t: string) {
|
|
memoryToken = t;
|
|
try {
|
|
await AsyncStorage.setItem(TOKEN_KEY, t);
|
|
} catch {}
|
|
}
|
|
export async function clearToken() {
|
|
memoryToken = null;
|
|
try {
|
|
await AsyncStorage.removeItem(TOKEN_KEY);
|
|
} catch {}
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
constructor(public status: number, public payload: any, message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
type Options = {
|
|
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
body?: any;
|
|
query?: Record<string, any>;
|
|
multipart?: boolean;
|
|
skipAuth?: boolean;
|
|
};
|
|
|
|
export async function api<T = any>(path: string, opts: Options = {}): Promise<T> {
|
|
const { method = 'GET', body, query, multipart, skipAuth } = opts;
|
|
|
|
let url = `${API_BASE}${path}`;
|
|
if (query) {
|
|
const qs = new URLSearchParams();
|
|
Object.entries(query).forEach(([k, v]) => v !== undefined && qs.append(k, String(v)));
|
|
const q = qs.toString();
|
|
if (q) url += `?${q}`;
|
|
}
|
|
|
|
const headers: Record<string, string> = {};
|
|
if (!multipart) headers['Content-Type'] = 'application/json';
|
|
|
|
if (!skipAuth) {
|
|
const tok = await loadToken();
|
|
if (tok) headers['Authorization'] = `Bearer ${tok}`;
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers,
|
|
body: multipart ? body : body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
|
|
const text = await res.text();
|
|
const payload = text ? safeParse(text) : null;
|
|
|
|
if (!res.ok) {
|
|
const msg = payload?.message ?? `HTTP ${res.status}`;
|
|
throw new ApiError(res.status, payload, msg);
|
|
}
|
|
return payload as T;
|
|
}
|
|
|
|
function safeParse(t: string) {
|
|
try {
|
|
return JSON.parse(t);
|
|
} catch {
|
|
return t;
|
|
}
|
|
}
|