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>
160 lines
4.6 KiB
TypeScript
160 lines
4.6 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import bcrypt from 'bcrypt';
|
|
import { z } from 'zod';
|
|
|
|
const RegisterBody = z.object({
|
|
email: z.string().email(),
|
|
password: z.string().min(8),
|
|
name: z.string().min(1),
|
|
phone: z.string().optional(),
|
|
age: z.number().int().min(0).max(120),
|
|
gender: z.enum(['MALE', 'FEMALE']),
|
|
job: z.string().optional(),
|
|
});
|
|
|
|
const LoginBody = z.object({
|
|
email: z.string().email(),
|
|
password: z.string().min(1),
|
|
});
|
|
|
|
const KakaoBody = z.object({
|
|
accessToken: z.string().min(10),
|
|
});
|
|
|
|
type KakaoMe = {
|
|
id: number;
|
|
kakao_account?: {
|
|
email?: string;
|
|
profile?: { nickname?: string; profile_image_url?: string };
|
|
phone_number?: string;
|
|
};
|
|
properties?: { nickname?: string; profile_image?: string };
|
|
};
|
|
|
|
async function fetchKakaoProfile(accessToken: string): Promise<KakaoMe> {
|
|
const res = await fetch('https://kapi.kakao.com/v2/user/me', {
|
|
method: 'GET',
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
if (!res.ok) throw new Error(`Kakao profile fetch failed: ${res.status}`);
|
|
return (await res.json()) as KakaoMe;
|
|
}
|
|
|
|
export async function authRoutes(app: FastifyInstance) {
|
|
app.post('/register', async (req, reply) => {
|
|
const body = RegisterBody.parse(req.body);
|
|
const exists = await app.prisma.user.findUnique({ where: { email: body.email } });
|
|
if (exists) return reply.code(409).send({ message: '이미 가입된 이메일입니다' });
|
|
|
|
const passwordHash = await bcrypt.hash(body.password, 10);
|
|
const user = await app.prisma.user.create({
|
|
data: {
|
|
email: body.email,
|
|
passwordHash,
|
|
name: body.name,
|
|
phone: body.phone,
|
|
provider: 'EMAIL',
|
|
profile: {
|
|
create: {
|
|
age: body.age,
|
|
gender: body.gender,
|
|
job: body.job ?? '기타',
|
|
},
|
|
},
|
|
},
|
|
include: { profile: true },
|
|
});
|
|
|
|
const token = await reply.jwtSign({ sub: user.id, email: user.email ?? undefined });
|
|
return { token, user: publicUser(user) };
|
|
});
|
|
|
|
app.post('/login', async (req, reply) => {
|
|
const body = LoginBody.parse(req.body);
|
|
const user = await app.prisma.user.findUnique({
|
|
where: { email: body.email },
|
|
include: { profile: true },
|
|
});
|
|
if (!user || !user.passwordHash) return reply.code(401).send({ message: '이메일 또는 비밀번호가 틀렸습니다' });
|
|
|
|
const ok = await bcrypt.compare(body.password, user.passwordHash);
|
|
if (!ok) return reply.code(401).send({ message: '이메일 또는 비밀번호가 틀렸습니다' });
|
|
|
|
const token = await reply.jwtSign({ sub: user.id, email: user.email ?? undefined });
|
|
return { token, user: publicUser(user) };
|
|
});
|
|
|
|
app.post('/kakao', async (req, reply) => {
|
|
const body = KakaoBody.parse(req.body);
|
|
let me: KakaoMe;
|
|
try {
|
|
me = await fetchKakaoProfile(body.accessToken);
|
|
} catch (e) {
|
|
return reply.code(401).send({ message: '카카오 인증 실패' });
|
|
}
|
|
|
|
const kakaoId = String(me.id);
|
|
const email = me.kakao_account?.email;
|
|
const name = me.kakao_account?.profile?.nickname ?? me.properties?.nickname ?? '카카오사용자';
|
|
const profileImage = me.kakao_account?.profile?.profile_image_url ?? me.properties?.profile_image;
|
|
|
|
let user = await app.prisma.user.findUnique({
|
|
where: { kakaoId },
|
|
include: { profile: true },
|
|
});
|
|
|
|
if (!user) {
|
|
user = await app.prisma.user.create({
|
|
data: {
|
|
kakaoId,
|
|
email: email ?? null,
|
|
name,
|
|
phone: me.kakao_account?.phone_number,
|
|
provider: 'KAKAO',
|
|
profileImage,
|
|
profile: {
|
|
create: {
|
|
age: 30,
|
|
gender: 'MALE',
|
|
job: '기타',
|
|
},
|
|
},
|
|
},
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
|
|
const token = await reply.jwtSign({ sub: user.id, email: user.email ?? undefined });
|
|
return { token, user: publicUser(user) };
|
|
});
|
|
|
|
app.get('/me', { onRequest: [app.authenticate] }, async (req) => {
|
|
const user = await app.prisma.user.findUnique({
|
|
where: { id: req.user.sub },
|
|
include: { profile: true },
|
|
});
|
|
if (!user) throw new Error('User not found');
|
|
return publicUser(user);
|
|
});
|
|
}
|
|
|
|
function publicUser(u: any) {
|
|
return {
|
|
id: u.id,
|
|
email: u.email,
|
|
name: u.name,
|
|
phone: u.phone,
|
|
provider: u.provider,
|
|
profileImage: u.profileImage,
|
|
profile: u.profile
|
|
? {
|
|
age: u.profile.age,
|
|
gender: u.profile.gender,
|
|
job: u.profile.job,
|
|
monthlyPremium: u.profile.monthlyPremium,
|
|
score: u.profile.score,
|
|
}
|
|
: null,
|
|
};
|
|
}
|