This commit is contained in:
Rinjae
2025-08-07 15:36:26 +09:00
commit cc925e0475
1172 changed files with 142573 additions and 0 deletions
@@ -0,0 +1,26 @@
package com.eactive.apim.portal.apps;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class DTOFieldUtils {
public static List<String> getFieldNames(Class<?> dtoClass) {
List<String> fieldNames = new ArrayList<>();
for (Field field : dtoClass.getDeclaredFields()) {
fieldNames.add(field.getName());
}
return fieldNames;
}
// Optional: Retrieve fields with specific annotations (e.g., @NotNull)
public static List<String> getFieldNamesWithAnnotation(Class<?> dtoClass, Class annotationClass) {
List<String> fieldNames = new ArrayList<>();
for (Field field : dtoClass.getDeclaredFields()) {
if (field.isAnnotationPresent(annotationClass)) {
fieldNames.add(field.getName());
}
}
return fieldNames;
}
}
@@ -0,0 +1,161 @@
package com.eactive.apim.portal.apps.user;
import com.eactive.apim.portal.apps.user.controller.AccountController;
import com.eactive.apim.portal.apps.user.dto.PasswordChangeRequestDTO;
import com.eactive.apim.portal.apps.user.dto.PortalOrgDTO;
import com.eactive.apim.portal.apps.user.service.PortalUserService;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.file.service.FileService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.servlet.ModelAndView;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SuppressWarnings("all")
@ExtendWith(MockitoExtension.class)
class AccountControllerTest {
@Mock
private Validator validator;
@Mock
private PortalUserService userService;
@Mock
private FileService fileService;
@InjectMocks
private AccountController accountController;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(accountController).build();
}
@Test
void testChangePassword() throws Exception {
mockMvc.perform(get("/change_password.do"))
.andExpect(status().isOk())
.andExpect(view().name("apps/main/myUpdatePw"))
.andExpect(model().attributeExists("passwordChangeRequest"));
}
@Test
void testUpdatePassword() throws Exception {
PasswordChangeRequestDTO dto = new PasswordChangeRequestDTO();
dto.setOldPassword("old");
dto.setNewPassword("new");
dto.setNewPassword2("new");
try (MockedStatic<SecurityUtil> securityUtil = mockStatic(SecurityUtil.class)) {
securityUtil.when(SecurityUtil::getCurrentLoginId).thenReturn("testUser");
// when(userService.findByLoginId("testUser")).thenReturn(true);
mockMvc.perform(post("/update_password.do")
.flashAttr("passwordChangeRequest", dto))
.andExpect(status().isOk())
.andExpect(view().name("apps/main/myUpdatePw"))
.andExpect(model().attributeExists("passwordChangeRequest"));
verify(validator).validate(any(), any());
}
}
@Test
void testUpdatePasswordWithErrors() throws Exception {
PasswordChangeRequestDTO dto = new PasswordChangeRequestDTO();
dto.setOldPassword("old");
dto.setNewPassword("new");
dto.setNewPassword2("different");
doAnswer(invocation -> {
BindingResult bindingResult = invocation.getArgument(1);
bindingResult.rejectValue("newPassword2", "error.password", "Passwords do not match");
return null;
}).when(validator).validate(any(), any(BindingResult.class));
mockMvc.perform(post("/update_password.do")
.flashAttr("passwordChangeRequest", dto))
.andExpect(status().isOk())
.andExpect(view().name("apps/main/myUpdatePw"))
.andExpect(model().attributeExists("errors"));
}
@Test
void testMypage() {
try (MockedStatic<SecurityUtil> securityUtil = mockStatic(SecurityUtil.class)) {
PortalAuthenticatedUser user = new PortalAuthenticatedUser();
securityUtil.when(SecurityUtil::getPortalAuthenticatedUser).thenReturn(user);
ModelAndView mav = accountController.mypage();
assertEquals("apps/main/myPageEnt", mav.getViewName());
assertEquals(user, mav.getModel().get("user"));
}
}
@Test
void testMyCompanyInfo() throws Exception {
try (MockedStatic<SecurityUtil> securityUtil = mockStatic(SecurityUtil.class)) {
securityUtil.when(SecurityUtil::getPortalAuthenticatedUser).thenReturn(new PortalAuthenticatedUser());
mockMvc.perform(get("/my_company_info.do"))
.andExpect(status().isOk())
.andExpect(view().name("apps/main/myPageEntCompany"))
.andExpect(model().attributeExists("user"));
}
}
@Test
void testUpdateCompanyInfo() throws Exception {
PortalOrgDTO dto = new PortalOrgDTO();
dto.setOrgName("Test Company");
try (MockedStatic<SecurityUtil> securityUtil = mockStatic(SecurityUtil.class)) {
securityUtil.when(SecurityUtil::getCurrentLoginId).thenReturn("testUser");
mockMvc.perform(post("/update_my_company_info.do")
.flashAttr("user", dto))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/my_company_info.do"));
verify(validator).validate(eq(dto), any());
}
}
@Test
void testUpdateCompanyInfoWithErrors() throws Exception {
PortalOrgDTO dto = new PortalOrgDTO();
doAnswer(invocation -> {
BindingResult bindingResult = invocation.getArgument(1);
bindingResult.rejectValue("companyName", "error.companyName", "Company name is required");
return null;
}).when(validator).validate(any(), any(BindingResult.class));
mockMvc.perform(post("/update_my_company_info.do")
.flashAttr("user", dto))
.andExpect(status().isOk())
.andExpect(view().name("apps/main/myPageEntCompany"));
}
}
@@ -0,0 +1,237 @@
package com.eactive.apim.portal.apps.user;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@SuppressWarnings("all")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class BaseWebTest {
public static final String HOST_URL = "http://localhost:8080";
protected static final String TEST_FILES_DIR = "src/test/resources/test-files";
protected static final int INPUT_DELAY_MILLIS = 1500; // 대기 시간
protected WebDriver driver;
protected WebDriverWait wait;
protected JavascriptExecutor js;
protected Actions actions;
@BeforeAll
public void setupClass() {
WebDriverManager.chromedriver().setup();
}
@BeforeEach
public void setupTest() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
options.addArguments("--remote-allow-origins=*");
ChromeDriverService service = new ChromeDriverService.Builder()
.usingAnyFreePort()
.build();
driver = new ChromeDriver(service, options);
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.manage().window().setSize(new Dimension(1280, 800)); // Set window size
wait = new WebDriverWait(driver, Duration.ofSeconds(60)); // 대기 시간을 60초로 늘림
js = (JavascriptExecutor) driver;
actions = new Actions(driver);
}
@AfterEach
public void teardown() {
if (driver != null) {
driver.quit();
}
}
protected void navigateTo(String url) {
driver.get(url);
}
protected void triggerBlurEvent(By locator) {
try {
WebElement element = wait.until(ExpectedConditions.presenceOfElementLocated(locator));
// First try using JavaScript blur()
try {
js.executeScript("arguments[0].blur();", element);
} catch (Exception jsException) {
// If JavaScript fails, try simulating tab key press
actions.moveToElement(element)
.click()
.sendKeys(Keys.TAB)
.perform();
}
// Add a small delay to allow blur event handlers to execute
Thread.sleep(500);
} catch (Exception e) {
System.err.println("Failed to trigger blur event: " + e.getMessage());
throw new RuntimeException("Failed to trigger blur event", e);
}
}
protected void slowSendKeys(By by, String keys) throws InterruptedException {
WebElement element = driver.findElement(by);
for (char c : keys.toCharArray()) {
element.sendKeys(String.valueOf(c));
Thread.sleep(10);
}
}
private void createTestFilesDirectoryIfNotExists() {
File directory = new File(TEST_FILES_DIR);
if (!directory.exists()) {
directory.mkdirs();
}
}
private String getTestFilePath(String fileName) {
Path resourcePath = Paths.get(TEST_FILES_DIR, fileName);
return resourcePath.toAbsolutePath().toString();
}
protected void uploadFile(String inputId, String fileName) throws InterruptedException {
try {
String filePath = getTestFilePath(fileName);
File file = new File(filePath);
if (!file.exists()) {
System.err.println("Warning: Test file not found at " + filePath);
return;
}
// Wait for file input to be present
WebElement fileInput = wait.until(ExpectedConditions.presenceOfElementLocated(By.id(inputId)));
// Send the file path to the input
fileInput.sendKeys(filePath);
Thread.sleep(INPUT_DELAY_MILLIS);
} catch (Exception e) {
System.err.println("Failed to upload file: " + e.getMessage());
throw e;
}
}
protected void clickElement(By by) {
WebElement element = wait.until(ExpectedConditions.elementToBeClickable(by));
try {
element.click();
} catch (Exception e) {
try {
((JavascriptExecutor) driver).executeScript("arguments[0].click();", element);
} catch (Exception jsException) {
new Actions(driver).moveToElement(element).click().perform();
}
}
}
protected void slowClick(By by) throws InterruptedException {
Thread.sleep(500);
clickElement(by);
Thread.sleep(500);
}
protected void checkCheckbox(By by) throws InterruptedException {
WebElement checkbox = wait.until(ExpectedConditions.elementToBeClickable(by));
if (!checkbox.isSelected()) {
clickElement(by);
}
}
protected void scrollIntoView(WebElement element) {
((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", element);
}
protected boolean isButtonDisabled(By locator) {
WebElement button = driver.findElement(locator);
return !button.isEnabled() || button.getAttribute("disabled") != null;
}
protected void handleEmailValidationAlert(String buttonId, String buttonName) {
try {
WebDriverWait alertWait = new WebDriverWait(driver, Duration.ofSeconds(5));
WebElement customAlert = alertWait.until(ExpectedConditions.visibilityOfElementLocated(By.id("emailValidationPopup")));
// 커스텀 알림 메시지 출력
//WebElement alertMessage = customAlert.findElement(By.id("customAlertMessage"));
//System.out.println("Custom Alert message: " + alertMessage.getText());
// '확인' 버튼 클릭
WebElement okButton = customAlert.findElement(By.id(buttonId));
clickButtonSafely(okButton, buttonName);
// 알림창이 사라질 때까지 대기
alertWait.until(ExpectedConditions.invisibilityOf(customAlert));
} catch (TimeoutException e) {
System.out.println("No custom alert appeared.");
}
}
protected void handleAlert() {
try {
WebDriverWait alertWait = new WebDriverWait(driver, Duration.ofSeconds(5));
WebElement customAlert = alertWait.until(ExpectedConditions.visibilityOfElementLocated(By.id("customAlert")));
// 커스텀 알림 메시지 출력
WebElement alertMessage = customAlert.findElement(By.id("customAlertMessage"));
System.out.println("Custom Alert message: " + alertMessage.getText());
// '확인' 버튼 클릭
WebElement okButton = customAlert.findElement(By.id("customAlertOkButton"));
clickButtonSafely(okButton, "알림 확인");
// 알림창이 사라질 때까지 대기
alertWait.until(ExpectedConditions.invisibilityOf(customAlert));
} catch (TimeoutException e) {
System.out.println("No custom alert appeared.");
}
}
protected void clickButtonSafely(WebElement button, String buttonName) {
try {
// 버튼이 보이도록 스크롤
((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", button);
Thread.sleep(500); // 스크롤 후 잠시 대기
// 먼저 일반적인 클릭 시도
button.click();
} catch (ElementClickInterceptedException | InterruptedException e) {
System.out.println(buttonName + " 버튼 클릭 실패. JavaScript로 클릭을 시도합니다.");
try {
// JavaScript를 사용하여 클릭
js.executeScript("arguments[0].click();", button);
} catch (Exception jsException) {
System.out.println(buttonName + " 버튼 JavaScript 클릭도 실패. Actions를 사용하여 클릭을 시도합니다.");
// Actions를 사용하여 클릭
actions.moveToElement(button).click().perform();
}
}
}
protected void assertElementText(By locator, String expectedText) {
WebElement element = wait.until(ExpectedConditions.presenceOfElementLocated(locator));
Assertions.assertEquals(expectedText, element.getText(), "Element text does not match expected value");
}
protected void assertUrlContains(String expectedUrlPart) {
wait.until(ExpectedConditions.urlContains(expectedUrlPart));
}
}
@@ -0,0 +1,297 @@
package com.eactive.apim.portal.apps.user;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
@SuppressWarnings("all")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class PortalOrgRegistrationRetainTest extends BaseWebTest {
public static final String HOST_URL = "http://localhost:8080";
private final String USER_ID_FILE = "user_id_counter.txt";
private static final String BUSINESS_LICENSE_FILE = "business-license.pptx";
@BeforeAll
public void setupClass() {
WebDriverManager.chromedriver().setup();
}
@AfterEach
public void teardown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void testSignupScenarios() throws InterruptedException {
navigateToRegistrationPage();
fillRegistrationForm();
}
public void navigateToRegistrationPage() throws InterruptedException {
driver.get(HOST_URL + "/signup");
// '기관회원가입' 링크를 찾아 클릭
WebElement orgSignupLink = wait.until(ExpectedConditions.elementToBeClickable(
By.xpath("//div[contains(@class, 'mb_group2')]/a/span[text()='법인회원가입']")));
orgSignupLink.click();
// 새 URL로 이동했는지 확인
wait.until(ExpectedConditions.urlToBe(HOST_URL + "/signup/portalOrg"));
// 페이지 로딩을 위한 추가 대기 시간
Thread.sleep(INPUT_DELAY_MILLIS);
}
public void fillRegistrationForm() throws InterruptedException {
Thread.sleep(INPUT_DELAY_MILLIS);
// 약관 동의
WebElement agreeAll = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("agree_all")));
scrollIntoView(agreeAll);
checkCheckbox(By.id("agree_all"));
Thread.sleep(INPUT_DELAY_MILLIS);
// 사업자 등록번호
slowSendKeys(By.id("compRegNo1"), "527");
slowSendKeys(By.id("compRegNo2"), "88");
slowSendKeys(By.id("compRegNo3"), "00686");
handleAlert();
Thread.sleep(3000);
// 법인 등록번호
slowSendKeys(By.id("corpRegNo1"), "131111");
slowSendKeys(By.id("corpRegNo2"), "0478337");
handleAlert();
Thread.sleep(3000);
// 기관명
slowSendKeys(By.id("orgName"), "카카오페이");
// 대표자 성명
slowSendKeys(By.id("ceoName"), "신원근");
// 사업장 소재지 주소
WebElement field = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("ceoName")));
scrollIntoView(field);
slowSendKeys(By.id("orgAddr"), "경기도 성남시 분당구 판교역로 166, 비동 15층(백현동)");
// 업태
slowSendKeys(By.id("orgSectors"), "금융 지원 서비스업");
// 종목
slowSendKeys(By.id("orgIndustryType"), "전자상거래 소매업");
// 회사 전화번호
closeDropdownIfOpen("orgAreaCode");
selectCustomOption("orgAreaCode", "없음");
slowSendKeys(By.id("orgPhoneNumber1"), "1644");
slowSendKeys(By.id("orgPhoneNumber2"), "7405");
// 서비스명
slowSendKeys(By.id("serviceName"), "카카오페이");
// 고객센터 전화번호
selectCustomOption("scAreaCode", "없음");
slowSendKeys(By.id("scPhoneNumber1"), "1644");
slowSendKeys(By.id("scPhoneNumber2"), "7405");
// 여기에 파일 업로드 로직 추가 (사업자 등록증)
uploadFile("files", BUSINESS_LICENSE_FILE);
// 기본 관리자 정보
slowSendKeys(By.id("userId"), "user");
slowSendKeys(By.id("domain"), "eee.com");
WebElement checkEmailButton = wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".btn_check .btn_check_email")));
clickButtonSafely(checkEmailButton, "이메일 중복체크");
handleEmailValidationAlert("emailConvertButton", "이메일 유지");
Thread.sleep(INPUT_DELAY_MILLIS);
slowSendKeys(By.id("passwordConfirmIndividual"), "bravo135!");
Thread.sleep(INPUT_DELAY_MILLIS); //추가 대기 시간
// 가입신청 버튼 클릭 (실제 제출은 하지 않습니다)
WebElement submitButton = wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".btn_register")));
submitButton.click(); // 주석 처리하여 실제 제출은 하지 않습니다
Thread.sleep(10000);
}
private void fillFieldAndHandleAlert(String fieldName, String value) throws InterruptedException {
WebElement field = wait.until(ExpectedConditions.presenceOfElementLocated(By.id(fieldName)));
field.clear();
field.sendKeys(value);
Thread.sleep(INPUT_DELAY_MILLIS);
handleAlert();
}
private void verifyPhoneNumber() throws InterruptedException {
// 인증번호 받기 버튼 찾기 (여러 방법 시도)
WebElement getAuthButton = null;
try {
getAuthButton = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector(".btn_check .common_btn_type_2.cbt.btn_auth")));
} catch (TimeoutException e) {
try {
getAuthButton = wait.until(ExpectedConditions.elementToBeClickable(
By.xpath("//a[contains(@class, 'btn_auth') and .//span[text()='인증번호 받기']]")));
} catch (TimeoutException e2) {
getAuthButton = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector("[hx-post*='/request_register_auth.do']")));
}
}
if (getAuthButton == null) {
throw new NoSuchElementException("인증번호 받기 버튼을 찾을 수 없습니다.");
}
// 버튼 클릭
clickButtonSafely(getAuthButton, "인증번호 받기");
Thread.sleep(INPUT_DELAY_MILLIS);
// 알림창 처리
handleAlert();
Thread.sleep(INPUT_DELAY_MILLIS);
// 인증번호 입력 컨테이너가 나타날 때까지 대기
WebElement authNumberContainer = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("authNumberContainer")));
// 인증번호 입력
slowSendKeys(By.id("authNumber"), "123456");
Thread.sleep(INPUT_DELAY_MILLIS);
// 인증번호 확인 버튼 클릭
WebElement verifyButton = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector(".btn_verify_auth")));
clickButtonSafely(verifyButton, "인증번호 확인");
Thread.sleep(INPUT_DELAY_MILLIS);
// 추가적인 알림창이 있다면 처리
handleAlert();
}
private void fillSplitFieldByName(String fieldName, String[] values) throws InterruptedException {
for (int i = 0; i < values.length; i++) {
WebElement field = wait.until(ExpectedConditions.presenceOfElementLocated(By.name(fieldName + (i + 1))));
field.clear();
field.sendKeys(values[i]);
Thread.sleep(INPUT_DELAY_MILLIS);
}
}
private void selectCustomOption(String selectId, String optionValue) throws InterruptedException {
try {
// Wait for and click the select_selected div to open dropdown
WebElement selectSelected = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector("#" + selectId + " .select_selected")));
// Wait for and verify the element doesn't have 'active' class before clicking
wait.until(ExpectedConditions.not(
ExpectedConditions.attributeContains(selectSelected, "class", "active")));
actions.moveToElement(selectSelected).click().perform();
// Wait for the dropdown to become visible by checking both conditions:
// 1. select_selected should have 'active' class
// 2. common_selecttype should not have 'select_hide' class
wait.until(ExpectedConditions.attributeContains(selectSelected, "class", "active"));
wait.until(ExpectedConditions.presenceOfElementLocated(
By.cssSelector("#" + selectId + " .common_selecttype:not(.select_hide)")));
// Find and click the option
WebElement option = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector("#" + selectId + " .common_selecttype li")));
actions.moveToElement(option).click().perform();
// After clicking, verify:
// 1. The dropdown is hidden again (select_hide class is present)
// 2. select_selected no longer has active class
// 3. The selected text is updated
wait.until(ExpectedConditions.presenceOfElementLocated(
By.cssSelector("#" + selectId + " .common_selecttype.select_hide")));
wait.until(ExpectedConditions.not(
ExpectedConditions.attributeContains(selectSelected, "class", "active")));
wait.until(ExpectedConditions.textToBe(
By.cssSelector("#" + selectId + " .select_selected"), optionValue));
Thread.sleep(INPUT_DELAY_MILLIS);
} catch (TimeoutException e) {
throw new RuntimeException("Failed to select option '" + optionValue +
"' from custom select '" + selectId + "'", e);
}
}
// Utility method to check if dropdown is open
private boolean isDropdownOpen(String selectId) {
try {
WebElement selectSelected = driver.findElement(By.cssSelector("#" + selectId + " .select_selected"));
WebElement selectItems = driver.findElement(By.cssSelector("#" + selectId + " .common_selecttype"));
return selectSelected.getAttribute("class").contains("active") &&
!selectItems.getAttribute("class").contains("select_hide");
} catch (Exception e) {
return false;
}
}
// Utility method to get currently selected value
private String getSelectedValue(String selectId) {
return driver.findElement(By.cssSelector("#" + selectId + " .select_selected"))
.getText();
}
// Utility method to close dropdown if open
private void closeDropdownIfOpen(String selectId) {
if (isDropdownOpen(selectId)) {
actions.click(driver.findElement(By.cssSelector("body"))).perform();
wait.until(ExpectedConditions.presenceOfElementLocated(
By.cssSelector("#" + selectId + " .common_selecttype.select_hide")));
}
}
private String generateUserId() {
int currentId = readUserIdFromFile();
int newId = currentId + 1;
writeUserIdToFile(newId);
return String.format("user%03d", newId);
}
private int readUserIdFromFile() {
try (BufferedReader reader = new BufferedReader(new FileReader(USER_ID_FILE))) {
String line = reader.readLine();
return line != null ? Integer.parseInt(line.trim()) : 0;
} catch (IOException e) {
System.out.println("파일을 읽는 중 오류가 발생했습니다. 새로운 ID를 1부터 시작합니다.");
return 0;
}
}
private void writeUserIdToFile(int id) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(USER_ID_FILE))) {
writer.write(String.valueOf(id));
} catch (IOException e) {
System.out.println("파일에 ID를 쓰는 중 오류가 발생했습니다.");
}
}
}
@@ -0,0 +1,318 @@
package com.eactive.apim.portal.apps.user;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
@SuppressWarnings("all")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class PortalOrgRegistrationTest extends BaseWebTest {
public static final int INPUT_DELAY_MILLIS = 1500; // 대기 시간을 2000ms로 늘림
private static final String BUSINESS_LICENSE_FILE = "business-license.pptx";
private final String USER_ID_FILE = "user_id_counter.txt";
@BeforeAll
public void setupClass() {
WebDriverManager.chromedriver().setup();
}
@AfterEach
public void teardown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void testSignupScenarios() throws InterruptedException {
navigateToRegistrationPage();
fillRegistrationForm();
}
public void navigateToRegistrationPage() throws InterruptedException {
driver.get(HOST_URL + "/signup");
// '기관회원가입' 링크를 찾아 클릭
WebElement orgSignupLink = wait.until(ExpectedConditions.elementToBeClickable(
By.xpath("//div[contains(@class, 'mb_group2')]/a/span[text()='법인회원가입']")));
orgSignupLink.click();
// 새 URL로 이동했는지 확인
wait.until(ExpectedConditions.urlToBe(HOST_URL + "/signup/portalOrg"));
// 페이지 로딩을 위한 추가 대기 시간
Thread.sleep(INPUT_DELAY_MILLIS);
}
public void fillRegistrationForm() throws InterruptedException {
Thread.sleep(INPUT_DELAY_MILLIS);
// 약관 동의
WebElement agreeAll = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("agree_all")));
scrollIntoView(agreeAll);
checkCheckbox(By.id("agree_all"));
Thread.sleep(INPUT_DELAY_MILLIS);
// 사업자 등록번호
slowSendKeys(By.id("compRegNo1"), "527");
slowSendKeys(By.id("compRegNo2"), "88");
slowSendKeys(By.id("compRegNo3"), "00686");
handleAlert();
Thread.sleep(3000);
// 법인 등록번호
slowSendKeys(By.id("corpRegNo1"), "131111");
slowSendKeys(By.id("corpRegNo2"), "0478337");
handleAlert();
Thread.sleep(3000);
// 기관명
slowSendKeys(By.id("orgName"), "카카오페이");
// 대표자 성명
slowSendKeys(By.id("ceoName"), "신원근");
// 사업장 소재지 주소
WebElement field = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("orgAddr")));
scrollIntoView(field);
slowSendKeys(By.id("orgAddr"), "경기도 성남시 분당구 판교역로 166, 비동 15층(백현동)");
// 업태
slowSendKeys(By.id("orgSectors"), "금융 지원 서비스업");
// 종목
slowSendKeys(By.id("orgIndustryType"), "전자상거래 소매업");
// 회사 전화번호
closeDropdownIfOpen("orgAreaCode");
selectCustomOption("orgAreaCode", "없음");
slowSendKeys(By.id("orgPhoneNumber1"), "1644");
slowSendKeys(By.id("orgPhoneNumber2"), "7405");
// 서비스명
slowSendKeys(By.id("serviceName"), "카카오페이");
// 고객센터 전화번호
selectCustomOption("scAreaCode", "없음");
slowSendKeys(By.id("scPhoneNumber1"), "1644");
slowSendKeys(By.id("scPhoneNumber2"), "7405");
// 여기에 파일 업로드 로직 추가 (사업자 등록증)
uploadFile("files", BUSINESS_LICENSE_FILE);
// 기본 관리자 정보
fillField("userId", generateUserId());
fillField("domain", "kakao.com");
WebElement checkEmailButton = wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".btn_check .btn_check_email")));
clickButtonSafely(checkEmailButton, "이메일 중복체크");
handleAlert();
Thread.sleep(INPUT_DELAY_MILLIS);
slowSendKeys(By.id("userName"), "홍길동");
// 휴대폰 번호
selectCustomOption("phoneMobile", "010");
slowSendKeys(By.id("cellPhone1"), "1234");
slowSendKeys(By.id("cellPhone2"), "1644");
verifyPhoneNumber();
slowSendKeys(By.id("password"), "bravo135!");
Thread.sleep(INPUT_DELAY_MILLIS); //추가 대기 시간
slowSendKeys(By.id("password2"), "bravo135!");
Thread.sleep(INPUT_DELAY_MILLIS); //추가 대기 시간
triggerBlurEvent(By.id("password2"));
Thread.sleep(INPUT_DELAY_MILLIS); //추가 대기 시간
// 가입신청 버튼 클릭
WebElement submitButton2 = wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".btn_register")));
submitButton2.click(); // 주석 처리하여 실제 제출은 하지 않습니다
Thread.sleep(10000);
}
private void fillFieldAndHandleAlert(String fieldName, String value) throws InterruptedException {
WebElement field = wait.until(ExpectedConditions.presenceOfElementLocated(By.id(fieldName)));
field.clear();
field.sendKeys(value);
Thread.sleep(INPUT_DELAY_MILLIS);
handleAlert();
}
private void verifyPhoneNumber() throws InterruptedException {
// 인증번호 받기 버튼 찾기 (여러 방법 시도)
WebElement getAuthButton = null;
try {
getAuthButton = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector(".btn_check .common_btn_type_2.cbt.btn_auth")));
} catch (TimeoutException e) {
try {
getAuthButton = wait.until(ExpectedConditions.elementToBeClickable(
By.xpath("//a[contains(@class, 'btn_auth') and .//span[text()='인증번호 받기']]")));
} catch (TimeoutException e2) {
getAuthButton = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector("[hx-post*='/request_register_auth.do']")));
}
}
if (getAuthButton == null) {
throw new NoSuchElementException("인증번호 받기 버튼을 찾을 수 없습니다.");
}
// 버튼 클릭
clickButtonSafely(getAuthButton, "인증번호 받기");
Thread.sleep(INPUT_DELAY_MILLIS);
// 알림창 처리
handleAlert();
Thread.sleep(INPUT_DELAY_MILLIS);
// 인증번호 입력 컨테이너가 나타날 때까지 대기
WebElement authNumberContainer = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("authNumberContainer")));
// 인증번호 입력
slowSendKeys(By.id("authNumber"), "123456");
Thread.sleep(INPUT_DELAY_MILLIS);
// 인증번호 확인 버튼 클릭
WebElement verifyButton = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector(".btn_verify_auth")));
clickButtonSafely(verifyButton, "인증번호 확인");
Thread.sleep(INPUT_DELAY_MILLIS);
// 추가적인 알림창이 있다면 처리
handleAlert();
}
private void fillSplitFieldByName(String fieldName, String[] values) throws InterruptedException {
for (int i = 0; i < values.length; i++) {
WebElement field = wait.until(ExpectedConditions.presenceOfElementLocated(By.name(fieldName + (i + 1))));
field.clear();
field.sendKeys(values[i]);
Thread.sleep(INPUT_DELAY_MILLIS);
}
}
private void fillField(String fieldName, String value) throws InterruptedException {
WebElement field = wait.until(ExpectedConditions.presenceOfElementLocated(By.id(fieldName)));
field.clear();
field.sendKeys(value);
Thread.sleep(INPUT_DELAY_MILLIS);
}
private void selectCustomOption(String selectId, String optionValue) throws InterruptedException {
try {
// Wait for and click the select_selected div to open dropdown
WebElement selectSelected = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector("#" + selectId + " .select_selected")));
// Wait for and verify the element doesn't have 'active' class before clicking
wait.until(ExpectedConditions.not(
ExpectedConditions.attributeContains(selectSelected, "class", "active")));
actions.moveToElement(selectSelected).click().perform();
// Wait for the dropdown to become visible by checking both conditions:
// 1. select_selected should have 'active' class
// 2. common_selecttype should not have 'select_hide' class
wait.until(ExpectedConditions.attributeContains(selectSelected, "class", "active"));
wait.until(ExpectedConditions.presenceOfElementLocated(
By.cssSelector("#" + selectId + " .common_selecttype:not(.select_hide)")));
// Find and click the option
WebElement option = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector("#" + selectId + " .common_selecttype li")));
actions.moveToElement(option).click().perform();
// After clicking, verify:
// 1. The dropdown is hidden again (select_hide class is present)
// 2. select_selected no longer has active class
// 3. The selected text is updated
wait.until(ExpectedConditions.presenceOfElementLocated(
By.cssSelector("#" + selectId + " .common_selecttype.select_hide")));
wait.until(ExpectedConditions.not(
ExpectedConditions.attributeContains(selectSelected, "class", "active")));
wait.until(ExpectedConditions.textToBe(
By.cssSelector("#" + selectId + " .select_selected"), optionValue));
Thread.sleep(INPUT_DELAY_MILLIS);
} catch (TimeoutException e) {
throw new RuntimeException("Failed to select option '" + optionValue +
"' from custom select '" + selectId + "'", e);
}
}
// Utility method to check if dropdown is open
private boolean isDropdownOpen(String selectId) {
try {
WebElement selectSelected = driver.findElement(By.cssSelector("#" + selectId + " .select_selected"));
WebElement selectItems = driver.findElement(By.cssSelector("#" + selectId + " .common_selecttype"));
return selectSelected.getAttribute("class").contains("active") &&
!selectItems.getAttribute("class").contains("select_hide");
} catch (Exception e) {
return false;
}
}
// Utility method to get currently selected value
private String getSelectedValue(String selectId) {
return driver.findElement(By.cssSelector("#" + selectId + " .select_selected"))
.getText();
}
// Utility method to close dropdown if open
private void closeDropdownIfOpen(String selectId) {
if (isDropdownOpen(selectId)) {
actions.click(driver.findElement(By.cssSelector("body"))).perform();
wait.until(ExpectedConditions.presenceOfElementLocated(
By.cssSelector("#" + selectId + " .common_selecttype.select_hide")));
}
}
private String generateUserId() {
int currentId = readUserIdFromFile();
int newId = currentId + 1;
writeUserIdToFile(newId);
return String.format("user%03d", newId);
}
private int readUserIdFromFile() {
try (BufferedReader reader = new BufferedReader(new FileReader(USER_ID_FILE))) {
String line = reader.readLine();
return line != null ? Integer.parseInt(line.trim()) : 0;
} catch (IOException e) {
System.out.println("파일을 읽는 중 오류가 발생했습니다. 새로운 ID를 1부터 시작합니다.");
return 0;
}
}
private void writeUserIdToFile(int id) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(USER_ID_FILE))) {
writer.write(String.valueOf(id));
} catch (IOException e) {
System.out.println("파일에 ID를 쓰는 중 오류가 발생했습니다.");
}
}
}
@@ -0,0 +1,49 @@
package com.eactive.apim.portal.apps.user;
import com.eactive.apim.portal.apps.DTOFieldUtils;
import com.eactive.apim.portal.apps.user.dto.PortalUserRegistrationDTO;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import static org.junit.jupiter.api.Assertions.assertTrue;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class PortalUserRegisterFieldTest extends BaseWebTest{
@BeforeAll
public void setupClass() {
WebDriverManager.chromedriver().setup();
}
@AfterEach
public void teardown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void testForm() {
String pageUrl = HOST_URL + "/signup/portalUser";
Class<?> dtoClass = PortalUserRegistrationDTO.class;
driver.get(pageUrl);
List<String> expectedFields = DTOFieldUtils.getFieldNames(dtoClass);
for (String fieldName : expectedFields) {
List<WebElement> elements = driver.findElements(By.name(fieldName));
assertTrue(elements.size() > 0, "Field '" + fieldName + "' is missing on the page");
}
}
}
@@ -0,0 +1,84 @@
package com.eactive.apim.portal.apps.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.eactive.apim.portal.PortalApplication;
import java.util.Objects;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
@SuppressWarnings("all")
@SpringBootTest(classes = PortalApplication.class)
@ActiveProfiles({"dev", "unittest"})
@AutoConfigureMockMvc
class SignupFileTest {
@Autowired
MockMvc mockMvc;
@Test
void test() {
try {
byte[] fileContent = "test file content".getBytes();
MockMultipartFile mockFile = new MockMultipartFile("files", "test.txt", "text/plain", fileContent);
String csrfToken = Objects.requireNonNull(mockMvc.perform(get("/login"))
.andExpect(status().isOk())
.andReturn().getResponse().getCookie("XSRF-TOKEN")).getValue();
MockHttpSession session = new MockHttpSession();
// loginCsrf("hyunsp@gmail.com", "vhxkf123", session, csrfToken);
// Call the endpoint and get the response
mockMvc.perform(
multipart("/signup/portalOrg")
.file(mockFile)
.param("termsOfUse", "on")
.param("privacyPolicy", "on")
// .param("nttSj", "Article File Test")
// .param("contents", "<p>wefwef</p><p>fwefwef</p><p>wefwef</p><p><br></p><script>\n" +
// " alert('13');\n" +
// " </script>")
.session(session)
.header("X-XSRF-TOKEN", csrfToken)
.cookie(new Cookie("XSRF-TOKEN", csrfToken))
)
.andExpect(status().is2xxSuccessful());
// .andReturn();
// Verify the response
// String response = result.getResponse().getContentAsString();
// assertEquals("File uploaded successfully", response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// MockHttpServletResponse loginCsrf(String username, String password, MockHttpSession session, String csrfToken) throws Exception {
//
// ResultActions result = mockMvc.perform(post("/actionLogin.do")
// .session(session)
// .param("id", username)
// .param("password", password)
// .header("X-XSRF-TOKEN", csrfToken)
// .cookie(new Cookie("XSRF-TOKEN", csrfToken)))
// .andExpect(status().is3xxRedirection());
//
// return result.andReturn().getResponse();
//
// }
}
@@ -0,0 +1,79 @@
package com.eactive.apim.portal.apps.user;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
@SuppressWarnings("all")
class SignupPageTest extends BaseWebTest {
private final String USER_ID_FILE = "user_id_counter.txt";
@TestFactory
Stream<DynamicTest> testSignupScenarios() throws IOException {
Path csvPath = Paths.get("src", "test", "resources", "signup_test_cases.csv");
return Files.lines(csvPath)
.skip(1)
.filter(s -> !s.startsWith("#"))// Skip header
.map(line -> line.split(","))
.map(data -> DynamicTest.dynamicTest("Test: " + data[0], () -> {
runTest(data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
}));
}
private void runTest(String email, String phone, String verificationCode,
String name, String password, String confirmPassword, String expectedResult) throws InterruptedException {
navigateTo("http://devportal.elink365.net/join.do");
Thread.sleep(1000);
WebElement allCheckbox = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("all")));
scrollIntoView(allCheckbox);
checkCheckbox(By.id("all"));
Thread.sleep(1000);
WebElement registerButton = wait.until(ExpectedConditions.presenceOfElementLocated(By.className("btn_register")));
scrollIntoView(registerButton);
slowClick(By.className("btn_register"));
Thread.sleep(3000);
slowSendKeys(By.id("userId"), email);
slowSendKeys(By.id("cellPhone"), phone);
slowClick(By.className("btn_auth"));
handleAlert();
slowSendKeys(By.id("authNumber"), verificationCode);
slowSendKeys(By.id("username"), name);
slowSendKeys(By.id("password"), password);
slowSendKeys(By.id("password2"), confirmPassword);
Thread.sleep(1000);
boolean isButtonEnabled = !isButtonDisabled(By.className("btn_register"));
if (isButtonEnabled) {
slowClick(By.className("btn_register"));
if ("회원 등록 완료".equals(expectedResult)) {
assertUrlContains("/user_register.do");
assertElementText(By.xpath("//h3[contains(text(), '회원 등록 완료')]"), "회원 등록 완료");
} else {
String result = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("validationUsernameInvalidFeedback"))).getText();
Assertions.assertTrue(result.contains(expectedResult),
"Expected result '" + expectedResult + "' not found in actual result: " + result);
}
} else {
if ("Button Disabled".equals(expectedResult)) {
Assertions.assertFalse(isButtonEnabled, "Form is invalid as expected and the button is disabled");
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
점검항목,이메일,휴대전화,인증번호,신청자명,비밀번호,비밀번호확인,예상결과
이메일 형식 확인,invalid_email,01012345678,123456,홍길동,Strong@Password1,Strong@Password1,Button Disabled
이메일 중복 체크,hyunsp@gmail.com,01012345678,123456,홍길동,Strong@Password1,Strong@Password1,Button Disabled
휴대전화 형식 확인,test_user2@example.com,0101234567,123456,홍길동,Strong@Password1,Strong@Password1,Button Disabled
#휴대전화 중복 체크,test_user3@example.com,01012345678,123456,홍길동,Strong@Password1,Strong@Password1,Button Disabled
인증번호 유효성,test_user4@example.com,01012345678,12345,홍길동,Strong@Password1,Strong@Password1,Button Disabled
신청자명 유효성,test_user5@example.com,01012345678,123456,홍길동!@#,Strong@Password1,Strong@Password1,Button Disabled
비밀번호 복잡도,test_user6@example.com,01012345678,123456,홍길동,weak,weak,Button Disabled
비밀번호 확인 일치,test_user7@example.com,01012345678,123456,홍길동,Strong@Password1,DifferentPassword,Button Disabled
필수 필드 - 이메일,,01012345678,123456,홍길동,Strong@Password1,Strong@Password1,Button Disabled
#필수 필드 - 휴대전화,test_user8@example.com,,123456,홍길동,Strong@Password1,Strong@Password1,Button Disabled
필수 필드 - 인증번호,test_user9@example.com,01012345678,,홍길동,Strong@Password1,Strong@Password1,Button Disabled
#필수 필드 - 신청자명,test_user10@example.com,01012345678,123456,,Strong@Password1,Strong@Password1,Button Disabled
필수 필드 - 비밀번호,test_user11@example.com,01012345678,123456,홍길동,,Strong@Password1,Button Disabled
필수 필드 - 비밀번호 확인,test_user12@example.com,01012345678,123456,홍길동,Strong@Password1,,Button Disabled
#유효한 데이터 - 정상 가입,valid_user@example.com,01087654321,123456,김철수,Secure135!@#,Secure135!@#,회원 등록 완료
1 점검항목 이메일 휴대전화 인증번호 신청자명 비밀번호 비밀번호확인 예상결과
2 이메일 형식 확인 invalid_email 01012345678 123456 홍길동 Strong@Password1 Strong@Password1 Button Disabled
3 이메일 중복 체크 hyunsp@gmail.com 01012345678 123456 홍길동 Strong@Password1 Strong@Password1 Button Disabled
4 휴대전화 형식 확인 test_user2@example.com 0101234567 123456 홍길동 Strong@Password1 Strong@Password1 Button Disabled
5 #휴대전화 중복 체크 test_user3@example.com 01012345678 123456 홍길동 Strong@Password1 Strong@Password1 Button Disabled
6 인증번호 유효성 test_user4@example.com 01012345678 12345 홍길동 Strong@Password1 Strong@Password1 Button Disabled
7 신청자명 유효성 test_user5@example.com 01012345678 123456 홍길동!@# Strong@Password1 Strong@Password1 Button Disabled
8 비밀번호 복잡도 test_user6@example.com 01012345678 123456 홍길동 weak weak Button Disabled
9 비밀번호 확인 일치 test_user7@example.com 01012345678 123456 홍길동 Strong@Password1 DifferentPassword Button Disabled
10 필수 필드 - 이메일 01012345678 123456 홍길동 Strong@Password1 Strong@Password1 Button Disabled
11 #필수 필드 - 휴대전화 test_user8@example.com 123456 홍길동 Strong@Password1 Strong@Password1 Button Disabled
12 필수 필드 - 인증번호 test_user9@example.com 01012345678 홍길동 Strong@Password1 Strong@Password1 Button Disabled
13 #필수 필드 - 신청자명 test_user10@example.com 01012345678 123456 Strong@Password1 Strong@Password1 Button Disabled
14 필수 필드 - 비밀번호 test_user11@example.com 01012345678 123456 홍길동 Strong@Password1 Button Disabled
15 필수 필드 - 비밀번호 확인 test_user12@example.com 01012345678 123456 홍길동 Strong@Password1 Button Disabled
16 #유효한 데이터 - 정상 가입 valid_user@example.com 01087654321 123456 김철수 Secure135!@# Secure135!@# 회원 등록 완료
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
점검항목,이메일,휴대전화,인증번호,신청자명,비밀번호,비밀번호확인,예상결과
이메일 형식 확인,invalid_email,01012345678,123456,홍길동,Strong@Password1,Strong@Password1,Button Disabled
1 점검항목 이메일 휴대전화 인증번호 신청자명 비밀번호 비밀번호확인 예상결과
2 이메일 형식 확인 invalid_email 01012345678 123456 홍길동 Strong@Password1 Strong@Password1 Button Disabled