작업 방식 및 고민한 점

  1. 유효성 검사 코드를 미들웨어로 분리할지 여부

    미들웨어로 빼는 게 순서상 맞고 효율적인 코드라고 판단. 최적화 시 회원가입 관련 유효성 코드는 미들웨어로 분리. 그러나 로그인은 service 단위에 구현하는 게 코드를 “덜” 쓰는 것 같아서 일단 내버려둠. 그 결과 통일성 저해됨. → 다음 프로젝트에서 처음부터 미들웨어로 분리하는 방법을 고려해볼 것. (= 유효성 검사가 들어가는 모든 코드에)

  2. 기획 요구사항대로, 회원가입 시 바로 로그인이 되도록 설정

  3. 데이터 구조

{
	message: "Client 일반 로그인 성공",
	data: { accessToken, refreshToken, user }
}
  1. zod로 DTO 작성 (회원가입만 예시 코드)
import z from "zod";
import { ErrorMessage } from "../constants/ErrorMessage";

// 일반 회원가입 개별 스키마
export const emailSchema = z.string().email().nonempty(ErrorMessage.NO_EMAIL);

export const passwordSchema = z
  .string()
  .min(8, ErrorMessage.PASSWORD_LENGTH_LIMIT)
  .max(16, ErrorMessage.PASSWORD_LENGTH_LIMIT)
  .regex(
    /^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\\d!@#$%^&*(),.?":{}|<>]{8,16}$/,
    ErrorMessage.PASSWORD_REGEX,
  )
  .nonempty(ErrorMessage.NO_PASSWORD);

export const nameSchema = z
  .string()
  .max(4, ErrorMessage.NAME_LENGTH_LIMIT)
  .nonempty(ErrorMessage.NO_NAME);

export const phoneSchema = z
  .string()
  .regex(/^\\d{9,11}$/, ErrorMessage.PHONE_REGEX)
  .nonempty(ErrorMessage.NO_PHONE);

// 일반 회원가입 DTO 및 zod 유효성 검사
export const signUpSchema = z
  .object({
    name: nameSchema,
    email: emailSchema,
    phone: phoneSchema,
    password: passwordSchema,
    passwordConfirmation: z.string(),
  })
  .refine((data) => data.password === data.passwordConfirmation, {
    message: ErrorMessage.PASSWORD_CONFIRMATION_NOT_MATCH,
    path: ["passwordConfirmation"],
  });

export type SignUpRequestDto = z.infer<typeof signUpSchema>;

// 일반 회원가입
async function signUp(req: Request<{}, {}, SignUpRequestDto>, res: Response, next: NextFunction) {
  try {
    const client = await authClientService.create(req.body);

    res.status(201).json({ message: "Client 일반 회원가입 성공", data: client });
  } catch (error) {
    next(error);
  }
}
  1. 기본적인 비밀번호 해싱 및 인증, filter 기능 삽입
async function loginWithLocal({ email, hashedPassword }: LoginDataLocal) {
  const client = await authClientRepository.findByEmail(email);

  if (!client || client.provider !== "LOCAL") {
    throw new NotFoundError(ErrorMessage.USER_NOT_FOUND);
  }

  // 비밀번호 확인 유효성 검사
  await verifyPassword(hashedPassword, client.hashedPassword!);

  // 토큰 넣음
  const accessToken = generateAccessToken({
    userId: client.id,
    email: client.email,
    name: client.name!,
    userType: client.userType,
    isProfileCompleted: client?.isProfileCompleted,
  });

  const refreshToken = generateRefreshToken({
    userId: client.id,
    email: client.email,
    name: client.name!,
    userType: client.userType,
    isProfileCompleted: client?.isProfileCompleted,
  });

  // 비밀번호와 전화번호 빼고 반환
  const user = filterSensitiveUserData(client);
  return { accessToken, refreshToken, user };
}