작업 방식

  1. 프로필 등록과 수정 모두 patch 메소드로 구현하므로, service 단위부터 if문으로 분기처리해 함수를 하나만 제작. if (!isRegistered) ... else ...
async function update(
  req: Request<{}, {}, ClientProfileRegisterDto>,
  res: Response,
  next: NextFunction,
) {
  try {
    const { userId } = req.auth!;

    // 프로필 등록 vs 수정 판단
    const existingProfile = await profileClientRepository.findById(userId);

    const mode = existingProfile?.isProfileCompleted === true ? "update" : "create";

    // 유형별 parse
    let parsedData;

    if (mode === "create") {
      parsedData = profileClientSchema("create").parse(req.body) as ClientProfileRegisterDto;
    } else {
      parsedData = profileClientSchema("update").parse(req.body) as ClientProfileUpdateDto;
    }

    const newProfile = await profileClientService.update(userId, parsedData);

    res.status(200).json({
      message: `일반 프로필 ${mode === "create" ? "등록" : "수정"} 성공`,
      data: newProfile,
    });
  } catch (error) {
    next(error);
  }
}
  1. 일반 로그인 시에는 비밀번호를 입력해야 프로필 정보를 바꿀 수 있음 — 소셜은 아예 비밀번호 입력창을 출력하지 않음.
export function profileClientSchema(mode: "create" | "update") {
  // 프로필 생성
  const create = z.object({
    profileImage: profileImageSchema,
    serviceType: serviceTypeSchema,
    livingArea: livingAreaSchema,
  });

  // 프로필 수정
  const update = z
    .object({ ... })
    .superRefine((data, ctx) => {
      const { newPassword, newPasswordConfirmation } = data;

      // 1. 새 비밀번호를 설정하면 확인도 해야 함
      const eitherPasswordExists = !!newPassword || !!newPasswordConfirmation;
      ...

어려웠던 구현 or 특이점 + 배운 점

프로필 상태 변경은 스키마에 속성 추가해야 한다는 점을 배움

FE ↔ BE 버튼 변환

  1. serviceType(= 소형이사…)은 enum, 지역은 string 형태의 객체(entity)로 구현되어 있음.
  2. 둘 다 버튼을 중복해 선택할 수 있어야 함. (DB에 여러 개 저장 가능 — 서울, 경기 ok)
  3. 지역의 경우, FE에 [서울, 경기] 형태로 데이터 구조를 내보냄. (= id 없이 — ❌{id: 1, regionName: “서울”})
// 프로필 생성
async function create(userId: Client["id"], profile: ClientProfileRegister) {
  // 배열1: serviceType. [user.serviceType]가 안 먹혀서 돌려씀
  const serviceTypes: MoveType[] | undefined = profile.serviceType
    ? profile.serviceType.map((type) => MoveType[type as keyof typeof MoveType])
    : undefined;

  // 배열2: 지역
  const livingAreaName = profile.livingArea
    ? {
        connect: profile.livingArea.map((regionName) => ({
          regionName,
        })),
      }
    : undefined;

  // Client 반환
  const newProfile = await prisma.client.update({
    where: { id: userId }, // 조건: 로그인한 사용자

    data: {
      profileImage: profile.profileImage,
      serviceType: serviceTypes,
      livingArea: livingAreaName,
      isProfileCompleted: true,
    },
  });

비밀번호 변경 유효성 검사 (일반 vs 소셜)

zod의 superRefineaddIssue 함수로 구현. 하지만 스스로 구현하는 쪽이 더 직관적이었을 듯함.

const basicPasswordSchema = z
  .string()
  .min(8, "기존 비밀번호를 입력해주세요.")
  .max(16, ErrorMessage.PASSWORD_LENGTH_LIMIT)
  .regex(
    /^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\\d!@#$%^&*(),.?":{}|<>]{8,16}$/,
    ErrorMessage.PASSWORD_REGEX,
  )
  .optional();
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,
  )
  .optional()
  .or(z.literal(""));