개요
userType을 쿼리 문자열로 넣어 Client/Mover 통합.
- service 단까지는 각자 구현하다가 controller 단위에서 통합하면서 토큰을 넣음. 일반 회원가입/로그인에서 body로 토큰을 보냈기 때문에, 그에 맞춰 BE에서 accessToken을 쿼리로 넘김
res.json과 res.redirect 동시 적용이 안 되므로, BE에서는 res.redirect를 적용하고 FE에서 api route 처리
FE
- Client/Mover 모두
href로 기본 url 연결하고 상위 페이지에서 userType을 내려줌.
export default function EasyLoginForm({ userType }: Prop) {
const t = useTranslations("Sign");
const handleGoogleLogin = () => {
location.href = `${BASE_URL}/auth/google?userType=${userType}`;
};
const handlekakaoLogin = () => {
location.href = `${BASE_URL}/auth/kakao?userType=${userType}`;
};
const handlenaverLogin = () => {
location.href = `${BASE_URL}/auth/naver?userType=${userType}`;
};
app/api/auth/callback 경로에서 route.ts 파일 작성. 해당 페이지에서 오류/토큰 여부 판별하고 곧바로 페이지를 넘겨줌.
import { setServerToken } from "@/lib/utils/server-token.util";
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const token = req.nextUrl.searchParams.get("token");
const error = req.nextUrl.searchParams.get("error");
if (error) {
return NextResponse.redirect(new URL(`/sign-in?error=${error}`, req.url));
}
if (token) {
await setServerToken(token); // ✅ 서버 쿠키 저장
return NextResponse.redirect(new URL("/mover-search", req.url));
}
return NextResponse.redirect(new URL("/sign-in?error=no_token", req.url));
}
BE 작업 과정
- 첫 등록/수정 시 같은 엔드포인트를 적용. service 단위에서 분기처리.
- 프로필 수정 시 덮어쓴 이름을 원상복구하지 않도록
update 시 이름은 따로 뺌.
// 소셜 로그인
async function save(user: SignUpDataSocial) {
const newClient = await prisma.client.create({
data: {
name: user.name,
email: user.email!,
phone: user.phone,
provider: user.provider,
providerId: user.providerId,
},
});
return { ...newClient, userType: "client" }; // userType: 헤더에서 씀
}
async function update(id: string, user: Omit<SignUpDataSocial, "email" | "name">) {
const newClient = await prisma.client.update({
where: { id },
data: user, // 이름은 빼고 받음 (덮어쓰기 방지)
});
return { ...newClient, userType: "client" };
}
//
async function oAuthCreateOrUpdate(data: SignUpDataSocial) {
// 1. 이메일로 사용자가 있는지 찾음
const { email, ...rest } = data;
const existingUser = await authClientRepository.findByEmailRaw(email);
// 2. 이미 가입된 이메일로 또 가입하려 할 때 오류 뱉음 = 소셜끼리
// ex. 카카오 회원가입 시 네이버 이메일을 썼는데 네이버로 가입하려는 경우
let user;
if (existingUser) {
if (existingUser.provider !== data.provider)
throw new BadRequestError(`이미 ${existingUser.provider} 가입 시 사용된 이메일입니다.`);
// + 사용자가 이름을 수정할 수 있게 덮어쓰기 하지 않음
const { name, ...dataWithoutName } = rest;
user = await authClientRepository.update(existingUser.id, dataWithoutName);
} else {
// 3. 없으면 자료 자체를 새로 생성
user = await authClientRepository.save(data);
}
return filterSensitiveUserData(user);
}
- controller에서 토큰 넣고 redirect 처리 — 한 것을 router에서 씀
- passport에서 userType 처리
export const googleStrategyOptions = {
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: "/auth/google/callback",
passReqToCallback: true as const, // 쿼리 문자열에 userType 넣음
};
// 인증 함수 실행해서 프로필 정보를 id 코드로 넘김
export async function verify(
req: Request,
accessToken: string,
refreshToken: string,
profile: Profile,
done: (error: any, user?: any) => void,
) {
try {
// enum <-> string 변환
const providerEnumValue = providerMap[profile.provider];
const userType = req.query.state || "client";
// 이메일 없으면 오류 처리
if (!profile.emails || profile.emails.length === 0) {
return done(new BadRequestError("이메일을 받지 못해 구글 로그인에 실패했습니다."));
}
// 사용자 데이터
let userInfo;
if (userType === "client") {
// 사용자 데이터
userInfo = await authClientService.oAuthCreateOrUpdate({
provider: providerEnumValue,
providerId: profile.id,
email: profile.emails[0].value,
name: "구글", // 구글 이름 안 받음
});
} else if (userType === "mover") {
userInfo = await authMoverService.oAuthCreateOrUpdate({
provider: providerEnumValue,
providerId: profile.id,
email: profile.emails[0].value,
name: "구글",
});
} else {
throw new BadRequestError("소셜 로그인: userType을 식별하지 못했습니다.");
}
done(null, userInfo); // req.user = user;
} catch (error: any) {
if (error.name === "Bad Request") {
return done(error);
}
return done(new BadRequestError("소셜 로그인 중 오류가 발생했습니다."));
}
}
const googleStrategy = new GoogleStrategy(googleStrategyOptions, verify);
export default googleStrategy;
- 콜백 엔드포인트와 로그인 시도 엔드포인트를 따로 제작(router), 각각 passport 미들웨어와 controller를 불러옴
authRouter.get("/google", (req, res, next) => {
const userType = (req.query.userType as string) || "client";
passport.authenticate("google", {
scope: ["profile", "email"],
state: userType,
})(req, res, next);
});
authRouter.get(
"/google/callback",
createSocialAuthMiddleware("google"),
authController.signInEasily,
);