프로젝트 작업일지 : 로그인 (아이디 찾기)

작업일지에 들어가며

이번에는 사용자 아이디 찾기 기능을 구현해보겠다.

 

아이디(이메일)찾기 버튼을 누르면 모달로 사용자 정보 중 휴대전화번호가 중복이 되지 않기 때문에

 

사용자 이름과 휴대전화 번호를 입력받고 서버에서 검증하여 검증이 완료되면

 

해당 유저의 이메일을 모달에 바로 띄우는 방향으로 진행해보겠다.


백엔드 코드

1. Respository

public interface MembersRepository extends JpaRepository<Members, Integer> {

	Members findByEmail(String email);

	// 이메일 중복 검사
	boolean existsByEmail(String email);

	// 휴대전화 중복 검사
	boolean existsByPhone(String phone);

	// 이메일 찾기
	Optional<Members> findByPhone(String phone);
}

 

2. Service

AuthProcess

public interface AuthProcess {
	
	ResponseEntity<Map<String, Object>> findEmail(Map<String, String> user);
}

 

AuthProcessImpl

@Service
@RequiredArgsConstructor
public class AuthProcessImpl implements AuthProcess {

	private final MembersRepository membersRepository;

	// 이메일 찾기
	@Override
	public ResponseEntity<Map<String, Object>> findEmail(Map<String, String> user) {
		Optional<Members> memberOptional = membersRepository.findByPhone(user.get("phone"));

		Map<String, Object> response = new HashMap();

		if (memberOptional.isPresent()) {
			Members members = memberOptional.get();
			String name = members.getName();
			String inputName = user.get("name");

			if (name.equals(inputName)) {
				try {
					String email = membersMain.getEmail();
					response.put("status", "success");
					response.put("email", email);
					return ResponseEntity.ok(response); // JSON 형식으로 반환
				} catch (Exception e) {
					response.put("status", "error");
					response.put("message", "이메일 반환 중 오류가 발생했습니다.");
					return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
				}
			} else {
				response.put("status", "error");
				response.put("message", "입력된 이름이 일치하지 않습니다.");
				return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
			}
		}

		response.put("status", "error");
		response.put("message", "해당 전화번호로 등록된 사용자가 없습니다.");
		return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
	}
}

 

3. Controller

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

	private final AuthProcess authProcess;

	// 이메일 찾기
	@PostMapping("/find-email")
	public ResponseEntity<Map<String, Object>> findEmail(@RequestBody Map<String, String> user) {
		// System.out.println(user.get("name"));
		// System.out.println(user.get("phone"));
		return authProcess.findEmail(user);
	}
}

프론트엔드 코드

1. 차크라UI의 Dialog, React Hook Form 컴포넌트 사용

const FindEmailModal = () => {

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = handleSubmit((data) => {
    console.log(data);
  });

  return (
    <form onSubmit={onSubmit}>
      <DialogRoot>
        <DialogTrigger>
          <Button>
            이메일
          </Button>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>이메일 찾기</DialogTitle>
          </DialogHeader>
          <DialogBody>
            <Stack>
              <Field
                label="이름"
                invalid={!!errors.name}
                errorText={errors.name?.message}
              >
                <Input
                  placeholder="가입하신 계정의 이름을 입력해주세요."
                  {...register("name", {
                    required: "이름은 필수 입력입니다.",
                    pattern: {
                      value: /^[a-zA-Z가-힣]{2,20}$/,
                      message: "이름은 한글과 영어만 입력해야 합니다.",
                    },
                  })}
                />
              </Field>
              <Field
                label="휴대전화"
                invalid={!!errors.phone}
                errorText={errors.phone?.message}
              >
                <Input
                  placeholder="가입하신 계정의 휴대전화를 입력해주세요."
                  {...register("phone", {
                    required: "휴대전화는 필수 입력입니다.",
                    pattern: {
                      value: /^[0-9]{11}$/,
                      message: "'-'를 제외한 숫자 11자리를 입력해주세요.",
                    },
                  })}
                />
              </Field>
            </Stack>
          </DialogBody>
          <DialogFooter>
            <Stack>
              <Button>
                이메일 찾기
              </Button>
              <DialogActionTrigger>
                <Button>
                  취소
                </Button>
              </DialogActionTrigger>
            </Stack>
          </DialogFooter>
        </DialogContent>
      </DialogRoot>
    </form>
  );
};

export default FindEmailModal;

 

2. 핸들러 작성

const handleFindEmail = async (data) => {
    try {
      const response = await axios.post(
        "http://localhost:8080/auth/find-email",
        data
      );
      console.log(response.data);
      alert("이메일을 확인해주세요.");
    } catch (error) {
      console.error("이메일 찾기 에러 : ", error);
    }
  };

 

3. 세부 CSS 적용

const FindEmailModal = () => {
  const ref = useRef < HTMLInputElement > null;
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const handleFindEmail = async (data) => {
    try {
      const response = await axios.post(
        "http://localhost:8080/auth/find-email",
        data
      );
      console.log(response.data);
      alert("이메일을 확인해주세요.");
    } catch (error) {
      console.error("이메일 찾기 에러 : ", error);
    }
  };

  const onSubmit = handleSubmit((data) => {
    handleFindEmail(data);
  });

  return (
    <form onSubmit={onSubmit}>
      <DialogRoot initialFocusEl={() => ref.current}>
        <DialogTrigger asChild>
          <Button variant="link" fontSize="sm" p={0} minW="unset">
            이메일
          </Button>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>이메일 찾기</DialogTitle>
          </DialogHeader>
          <DialogBody pb="4">
            <Stack gap="4" align="flex-start" width="full">
              <Field
                label="이름"
                invalid={!!errors.name}
                errorText={errors.name?.message}
              >
                <Input
                  size="md"
                  placeholder="가입하신 계정의 이름을 입력해주세요."
                  width="100%"
                  {...register("name", {
                    required: "이름은 필수 입력입니다.",
                    pattern: {
                      value: /^[a-zA-Z가-힣]{2,20}$/,
                      message: "이름은 한글과 영어만 입력해야 합니다.",
                    },
                  })}
                />
              </Field>
              <Field
                label="휴대전화"
                invalid={!!errors.phone}
                errorText={errors.phone?.message}
              >
                <Input
                  size="md"
                  placeholder="가입하신 계정의 휴대전화를 입력해주세요."
                  width="100%"
                  {...register("phone", {
                    required: "휴대전화는 필수 입력입니다.",
                    pattern: {
                      value: /^[0-9]{11}$/,
                      message: "'-'를 제외한 숫자 11자리를 입력해주세요.",
                    },
                  })}
                />
              </Field>
            </Stack>
          </DialogBody>
          <DialogFooter>
            <Stack direction="row" spacing="4" width="100%" justify="flex-end">
              <Button colorPalette="orange" width="auto">
                이메일 찾기
              </Button>
              <DialogActionTrigger asChild>
                <Button variant="outline" width="auto">
                  취소
                </Button>
              </DialogActionTrigger>
            </Stack>
          </DialogFooter>
        </DialogContent>
      </DialogRoot>
    </form>
  );
};

export default FindEmailModal;

아이디 찾기 테스트

🚨 hook.js:608 Warning: validateDOMNesting(...): <form> cannot appear as a descendant of <form>. Error Component Stack

해당 경고가 콘솔에 뜨면서 폼 제출이 안된다. 아! 로그인 폼안에 이메일 찾기 폼이 존재해서 폼 태그 중첩으로 폼 제출이 안되는 모양이다. 폼을 제거해주고 div로 묶어줘야겠다.

그래도 아무런 에러 메세지도 안뜨면서 폼 제출이 안된다. GPT에 물어보니 차크라UI의 Dialog가 Hook form과 충돌되는 것 같다고 한다. 그래서 GPT에게 부탁하여 CSS로 모달을 만들기로하였다. 

 

FindEmail 수정

const FindEmail = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm();

  const [isModalOpen, setModalOpen] = useState(false);
  const [isLoading, setLoading] = useState(false);
  const [result, setResult] = useState(null);

  const handleFindEmail = async (data) => {
    setLoading(true);

    try {
      const response = await axios.post(
        "http://localhost:8080/auth/find-email",
        data
      );
      const email = response.data.email;
      setResult({
        status: "success",
        title: "인증 성공",
        message: `회원님의 이메일은 ${email} 입니다.`,
      });
    } catch (error) {
      setResult({
        status: "error",
        title: "정보 불일치",
        message: "정보 불일치 : 입력하신 정보를 확인해주세요.",
      });
    } finally {
      setLoading(false);
    }
  };

  const handleCloseModal = () => {
    setModalOpen(false);
    reset();
    setResult(null);
  };

  const modalStyles = {
    modal: {
      position: "fixed",
      top: 0,
      left: 0,
      width: "100%",
      height: "100%",
      background: "rgba(0, 0, 0, 0.5)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      zIndex: 1000,
    },
    modalContent: {
      background: "white",
      padding: "40px",
      borderRadius: "12px",
      boxShadow: "0 4px 15px rgba(0, 0, 0, 0.2)",
      animation: "fadeIn 0.3s ease",
      width: "700px",
      maxWidth: "90%",
      height: "auto",
      textAlign: "left",
      position: "relative",
      top: "-50px",
      fontFamily: "'Arial', sans-serif",
    },
    button: {
      width: "auto",
      padding: "10px 20px",
      height: "40px",
      borderRadius: "8px",
      backgroundColor: "#ff6f00",
      color: "white",
      fontSize: "1em",
      border: "none",
      cursor: "pointer",
      marginBottom: "10px",
      transition: "background-color 0.3s ease",
    },
    buttonDisabled: {
      backgroundColor: "#ffcc80",
      cursor: "not-allowed",
    },
    cancelButton: {
      width: "auto",
      padding: "10px 20px",
      height: "40px",
      borderRadius: "8px",
      border: "2px solid lightgray",
      backgroundColor: "white",
      color: "black",
      fontSize: "1em",
      cursor: "pointer",
      transition: "background-color 0.3s ease",
    },
    errorMessage: {
      color: "red",
      fontSize: "0.9em",
      marginTop: "-15px",
    },
    modalTitle: {
      marginBottom: "20px",
      fontSize: "1.5em",
    },
    inputField: {
      width: "100%",
      padding: "12px",
      borderRadius: "8px",
      border: "1px solid #ccc",
      marginBottom: "15px",
      fontSize: "1em",
    },
    buttonContainer: {
      display: "flex",
      justifyContent: "flex-end",
      gap: "10px",
      width: "100%",
    },
    resultMessage: {
      color: result?.status === "error" ? "red" : "green",
      fontSize: "1em",
      marginLeft: "15px",
      display: "flex",
      alignItems: "center",
    },
  };

  return (
    <>
      <button
        type="button"
        onClick={() => setModalOpen(true)}
        style={{ fontSize: "0.9em" }}
      >
        이메일 /
      </button>

      {isModalOpen && (
        <div style={modalStyles.modal} onClick={() => setModalOpen(false)}>
          <div
            style={modalStyles.modalContent}
            onClick={(e) => e.stopPropagation()}
          >
            <h2 style={modalStyles.modalTitle}>이메일 찾기</h2>
            <div>
              <label>이름</label>
              <input
                type="text"
                placeholder="가입하신 계정의 이름을 입력해주세요."
                {...register("name", {
                  required: "이름은 필수 입력입니다.",
                  pattern: {
                    value: /^[a-zA-Z가-힣]{2,20}$/,
                    message: "이름은 한글과 영어만 입력해야 합니다.",
                  },
                })}
                style={modalStyles.inputField}
              />
              {errors.name && (
                <p style={modalStyles.errorMessage}>{errors.name.message}</p>
              )}
            </div>
            <div>
              <label>휴대전화</label>
              <input
                type="text"
                placeholder="'-'를 제외한 숫자 11자리를 입력해주세요."
                {...register("phone", {
                  required: "휴대전화는 필수 입력입니다.",
                  pattern: {
                    value: /^[0-9]{11}$/,
                    message: "'-'를 제외한 숫자 11자리를 입력해주세요.",
                  },
                })}
                style={modalStyles.inputField}
              />
              {errors.phone && (
                <p style={modalStyles.errorMessage}>{errors.phone.message}</p>
              )}
            </div>
            <div style={modalStyles.buttonContainer}>
              {result && (
                <div style={modalStyles.resultMessage}>{result.message}</div>
              )}
              <button
                type="button"
                style={
                  isLoading ? modalStyles.buttonDisabled : modalStyles.button
                }
                disabled={isLoading}
                onClick={handleSubmit(handleFindEmail)}
              >
                {isLoading ? "처리 중..." : "이메일 찾기"}
              </button>
              <button
                type="button"
                onClick={handleCloseModal}
                style={modalStyles.cancelButton}
              >
                나가기
              </button>
            </div>
          </div>
        </div>
      )}
    </>
  );
};

export default FindEmail;

 

아이디 찾기 테스트 2

 

 

 

정상적으로 출력되는 모습!

 

정보 불일치 시


작업일지를 마치며

✨ 나의 생각

폼 태그를 중복시키는 대참사를 제외하면 큰 어려움 없이 기능을 구현했다.

로그인 페이지 안에도 수많은 기능들이 들어있는데 점점 완성되어가는 모습을 보니 뿌듯하다!


다음 기능은 이어서 비밀번호 찾기 기능을 구현해봐야겠다. 참고할 블로그도 찾아놓았으니

비밀번호 찾기 기능도 쉽게 만들어졌음 한다.