반응형

mysql 에서는 아주 간편하게 limit 함수를 이용해서 몇개의 데이터만 가지고 왓엇는데, rownum 이라는 함수를 사용해서인지 이유는 아직 모르겟다... 아직 더 자세히 공부를 하지는 못햇다. 

하지만, 일단 원하는 rownum의 범위로 검색

반응형
SELECT
 * 
FROM(
    SELECT 
        sub.*, 
        ROWNUM AS "OFFSET" 
    FROM
        (SELECT 
            WORKER_NAME AS "workerName",
            WORKER_TEL AS "workerTel",
            TIMELINE AS "timeline",
            COMUTE_TYPE AS "commuteType",
            TO_CHAR(TIMELINE, 'YYYY-MM-DD') AS "dateInfo"
        FROM 
            ATTENDANCE_INFO
            WHERE IS_STATISTICS = 0
            GROUP BY TO_CHAR(TIMELINE, 'YYYY-MM-DD'), WORKER_NAME, WORKER_TEL, TIMELINE, COMUTE_TYPE
            ORDER BY WORKER_NAME, TO_CHAR(TIMELINE, 'YYYY-MM-DD')
    ) sub)
    WHERE ROWNUM <= 80 
    AND OFFSET >= 30;

여기서

ROWNUM을 OFFSET으로 ALIAS 로 지정해두고, WHERE 절에 사용된 ROWNUM 은 sub 에서 select된 ROWNUM 이 아닌, 맨 마지막에 SELECT * 에서 선택되어지는 ROW들의 NUM 이라고 생각하면 된다.

그리고 그 안에서의 ROWNUM 은 OFFSET으로 지정된거다. 그래서 OFFSET이라는 변수에 저런식으로 지정해주면 된다!

반응형
반응형
SELECT 
	sub.*, 
	ROWNUM AS cntnt 
FROM
	(SELECT 
		WORKER_NAME AS "workerName",
		WORKER_TEL AS "workerTel",
		TIMELINE AS "timeline",
		COMUTE_TYPE AS "commuteType",
		TO_CHAR(TIMELINE, 'YYYY-MM-DD') AS "dateInfo"
	FROM 
		ATTENDANCE_INFO
		WHERE IS_STATISTICS = 0
		GROUP BY TO_CHAR(TIMELINE, 'YYYY-MM-DD'), WORKER_NAME, WORKER_TEL, TIMELINE, COMUTE_TYPE
		ORDER BY WORKER_NAME, TO_CHAR(TIMELINE, 'YYYY-MM-DD')
	) sub

ROWNUM 이라는 컬럼을 사용하면, 순번이 row의 번호가 붙어서 나오게된다.

 

GROUP BY  를 활용해서 조건으로 그룹지어 조회할 수 있다. 여기서 포인트는, 만약 SUM 이런걸 원한다면 select 에 컬럼명으로 sum(원하는 합산의 컬럼명) 으로 적고 group by 조건에는 넣어주지않아도된다. 하지만 다른 표시하고싶은 컬럼들은 모두 group by 에 넣어줘야한다. (이게 mysql 과 다른점이다.)

 

TO_CHAR 을 활용하여, 숫자나 날짜를 원하는 형태로 문자화 시켜서 출력할 수 있다.

쿼리 결과

반응형

 

반응형
반응형

공식 참고 사이트: https://github.com/jwtk/jjwt

              1. dependencies 추가              

Gradle

dependencies {
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5',
    // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
    //'org.bouncycastle:bcprov-jdk15on:1.70',
    'io.jsonwebtoken:jjwt-jackson:0.11.5' // or 'io.jsonwebtoken:jjwt-gson:0.11.5' for gson
}

 

여기서 잠깐! JJWT Dependencies 에 대해 초큼 알아보자

dependency의 선언을 보면, 한개만 compile-time dependency 이고, 나머지는 runtime으로 선언되어있다.

이것은, JJWT는 오직 설계한 application에 명시되어있는 APIs 에 의존하도록 설계가 되어있고,  경고 없이 변경될 수 있는 기타 모든 내부 구현 세부 정보는 runtime전용 종속성으로 분류되기 때문입니다. 이건, 만약 안정적인 JJWT사용이나 업데이트를 위해서는 매우 중요하다!

만약, jjwt-impl.jar 를 추가해서 사용한다면, jjwt에 업데이트가 있거나 수정사항이 있을 경우에 자동으로 compativility 되지 않을 거라는 경고도 되어있다. 그래서 jjwt-impl.jar 를 compile scope (implementation)에 넣지 말고 runtime scope에 넣으라고 되어있다.

반응형
              2. Token 생성해보기              
import java.util.Date;
import java.util.UUID;

import io.jsonwebtoken.Jwts;

public class JwtUtils {
	
	public static String createJWT() {
		Date now = new Date();
		Date expiredAt = new Date(now.getTime() + 10 * 60 * 1000);	// 유효시간 10분
		String jwtToken = Jwts.builder()
				.claim("name", "minah")
				.claim("email", "mapark@abc.co.kr")
				.setSubject("user")
				.setId(UUID.randomUUID().toString())
				.setIssuedAt(now)
				.setExpiration(expiredAt)
				.compact();
		return jwtToken;
		
	}
			
}

해석해보면,

name 에 minah를, email 에 mapark@abc.co.kr 을 각각 넣어둔것. user이라는 subject로 

token의 유효시간은 10분으로해서 compact 를 해둠.

그래서 로그를 찍어보니,

이렇게 잘 찍혓다. 그러면 이제 이걸 다시 재해석... 해야지?

 

              3. Token 재해석하여 보낸 데이터 확인              

재해석을 하는 과정에서,Jwts 의 parserBuilder() 를 이용하는데, 이제 어떠한 키를 가지고 해석해내지 않으면 할 수 없다고 에러가 떳다. 그래서 key 를 지정해서 jwt 를 생성하고, 그 key 를 가지고 재해석하는 과정을 넣었다.

수정한 메서드

import java.security.Key;
import java.util.Date;
import java.util.UUID;

import javax.crypto.SecretKey;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

public class JwtUtils {
	
	static Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
	
	public static String createJWT() {
		Date now = new Date();
		Date expiredAt = new Date(now.getTime() + 10 * 60 * 1000);	// 유효시간 10분
		String jwtToken = Jwts.builder()
				.claim("name", "minah")
				.claim("email", "mapark@abcd.co.kr")
				.setSubject("user")
				.setId(UUID.randomUUID().toString())
				.setIssuedAt(now)
				.setExpiration(expiredAt)
                // 지정한 key 로 sign 하겟다라는 설정
				.signWith(key)
				.compact();
		return jwtToken;
		
	}
	
	public static Jws<Claims> parseJwt(String jwtString){
		Jws<Claims> jwt = Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws(jwtString);
		return jwt;
	}
			
}

그래서 다시 확인해봤다.

parse jwtTest에, 내가 넣어두었던 name, email, sub 모두가 나왔다. 

다음 글에서는 vue.js에서 로그인해서 token을 header로 보내는 과정을 구현해보겠다.

반응형
반응형

JWT를 구현하기 위해 JWT 공식사이트(https://jwt.io/libraries?language=Java)에 들어가서 확인하는 중에, 발견한 것이 Java에서 사용할 경우, 여러가지의 라이브러리가 있었는데, 내가 알아본 예제들은 대부분, io.jsonwebtoken 을 사용했거나, ,com.auth0을 사용했다. 그래서 그 둘의 차이를 알아봤다.

반응형

Auth0 이란?

이름에서도 알 수 있다시피, authentication과 authorization에 관한 application 이다. 

어떤 경우에 Auth0을 사용할 수 있냐 :

- JavaScript 로 front-end app을 만들고, API의 access를 안전하게 하고 싶다면 사용 가능하다.
- SAML로 접근, 권한 설정을하는 web app을 만든다. 
SAML (Security Assertion Markup Language) 프로토콜은 open-standard, xml-based 프레임워크 에서의 authentication 과 authorization을 비밀번호 없이 관리할 수 있다. Auth0은 SAML 을 서포트해준다. 
- 사용자가 비밀번호가 아닌 이메일이나 SMS 로 보내진 one-time codes로 로그인하게 하고 싶다
.

여기서, Auth0 을 검색할때마다 OAuth 도 같이 나오는데, 차이가 뭔지 보면,

OAuth 2.0: 모든 소프트웨어(windwos, mobile 또는 web)의 인증을 위한 기준 또는 프로토콜
Auth0:  OAuth 2.0 프로토콜을 사용, 구현하는 소프트웨어 상품

즉, Auth는 OAuth 의 구현체 라고 나는 이해했다.

그래서, 알아본 OAuth

OAuth 란,
- google, github, 카카오톡 등 서비스에 인증을 맡기고, 접근 권한 관리만을 수행하는 인증방식

https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow


JJWT는 JVM이 제공하는 JWT 토큰 생성  JWT 토큰 파싱, 검증을 해주는 라이브러리 입니다.

그래서, 결국 나는 JJWT를 이용할 것이다. 왜? 아이디와 비번으로 로그인을 구현할 것이기때문이다.

반응형
반응형

참고: https://www.bezkoder.com/spring-boot-vue-js-authentication-jwt-spring-security/

BackEnd : JWT 를 위한 Spring Security , Spring Mysql, Mybatis
FrontEnd: Vuex, vee-validate

 

1. JWT (JSON Web Token) 란?

현재, JWT는 인증과 정보 교환에 매우 자주 사용된다. Session (Session-based Authentication)을 새로 생성하는 것 대신에, Server 가 데이터를 JSON Web Token 으로 encoding 해준 후에 Client 에게 보내준다 (3). 그러면  Client 는 JWT를 저장하고, Client 가 요청하는 모든 Request의 루트, 리소스를 보호하기 위해 JWT가 부착(4)되어있어야한다. (보통은 header에 붙어있다) 그 요청을 받은 Server는 그 JWT를 평가(5)한 후에 Response를 리턴(6)해줄 것이다. 

https://www.bezkoder.com/spring-boot-vue-js-authentication-jwt-spring-security/

Session을 기반으로 하는 인증 (Cookie 에 session을 저장해야함)과 다르게,

JWT (Token-based Quthentication)의 큰 장점은, Token 을 Client sdie 에 저장한다는거다!!! (브라우저의 Local Storage, IOS 와 Android 의Keychain).

 

JWT 의 중요한 3가지

Header,    Payload,   Signature

https://velog.io/@jkijki12/Spirng-Security-Jwt-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

JWT 가 더 궁금하시다구용? 자세한 내용은 https://minah-workmemory.tistory.com/37 에 올렸으니 참고 하실 분 참고!

 

JWT 란? (JWT vs Session)

참조 : https://www.bezkoder.com/jwt-json-web-token/ 1. Authentication 이란? 어떠한 웹사이트를 사용할 때, 계정을 생성하고, 어떠한 특정한 기능을 사용하기위해 로그인하여 접근해야한다. 그러한 action 을..

minah-workmemory.tistory.com

 

2. Spring Boot Vue.js Authentication 예제!

  • 사용자는 계정을 하나 생성하고, 이름과 비밀번호로 로그인
  • Authorization  사용자의 권한 (admin, moderator, user)

                                  로그인, 회원가입의 Flow chart                              

https://www.bezkoder.com/spring-boot-vue-js-authentication-jwt-spring-security/

 

 

                                  Back-end :  Spring Boot & Spring Security                              

https://www.bezkoder.com/spring-boot-vue-js-authentication-jwt-spring-security/

gradle 에 dependency 추가

// JWT 라이브러리
implementation 'io.jsonwebtoken:jjwt:0.9.1'

// spring Security 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'

JWT secret String / 유효기간 application.properties 에 설정

 

 

                                  Front-end :  Vue.js & Vuex                              

https://www.bezkoder.com/spring-boot-vue-js-authentication-jwt-spring-security/

 

반응형
반응형

환경: 전자정부프레임워크 (egov) , spring 4.3 version 을 사용

 

 

 

 

 

여기에서 공통 메세지를 관리하는 메세지들이 설정되어있음. 그

걸 사용하기 위해서는 아래와 같이 context-common.xml 의 bean 에 추가해줘야한다.

 

 

 

 

 

위와같이, messageSource로 message-common 이라는 파일을 모두 사용하겠다라고 넣어준다.

그 후에, jsp 에서 

 

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

 

추가해준다,

 spring 이라는 prefix로 사용할 수 있다.

 

나는 메세지를 아래와 같이 설정해두었다.

{0} 은, 0번째의 값을 파라미터로 갖겠다 라는 의미이다. 

이렇게 해두니, 알아서 arguments에 넣어둔 '학생을 추가하는' 이라는 파라미터가 들어가서 에러메시지가 뜬 것을 확인 할 수 있다.

만약, 파라미터를 여러개 받고싶다! 하면, {0} {1} {2} 이렇게 사용해서 메시지를 설정한 다음 

호출 할 때, arguments='파라미터1, 파라미터2' 이렇게 넣으면 알아서 잘 호출된다!

 


하지만, 파라미터 받아서 쓰는 것은 권장하지 않는다고한다.
왜? 다국어 지원해야할때는 파라미터로 받아온 단어들도 어차피 다시 정의를 해줘야하기 때문이다.
반응형
반응형

https://marobiana.tistory.com/112

일단, 여기서 해보라는건 모두 다 해봤다. 마지막꺼빼고, 

마지막에 시도한방법,

dispatcher-servlet.xml

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <!-- <property name="webBindingInitializer">
            <bean class="egovframework.example.cmmn.web.EgovBindingInitializer"/>
        </property> -->
        <property name="messageConverters">
            <list>
                <!--           -->
                <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                    <property name = "supportedMediaTypes">
                        <list>
                            <value>text/html;charset=UTF-8</value>
                            <value>application/json;charset=UTF-8</value>
                        </list>
                    </property>
                </bean>
                <!-- json    -->
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                    <property name="supportedMediaTypes">
                        <list>
                            <value>text/html;charset=UTF-8</value>
                            <value>application/json;charset=UTF-8</value>
                        </list>
                    </property>
                </bean>
            </list>
        </property>
    </bean>

일단, name="webVindingInitializer" 있는 부분을 주석처리해준다!

그러가 그 아래에, messageconverters 를 추가해준다.

pom.xml

<!-- jackson -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.6</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.6</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.9.6</version>
</dependency>

원래는 org.codehaus.jackson 이아이를 사용했었는데, https://www.egovframe.go.kr/home/qainfo/qainfoRead.do?menuNo=69&qaId=QA_00000000000018084 여기에 의하면, 스프링 4.1버전 이상일 경우에는 deprecate 됬기때문에 fasterxml 에 있는 jackson을 쓰라고 적혀있었다. 그래서 저걸 넣었다. 그리고 저 사이트에 나와있는 dependency 랑 다르다. 사이트에 나와있는대로 했는데 계속 에러가 나서 다른 것을 데려왔다.

 

그리고 여전히 모르겠지만, @RequestBody 가 아닌, @RequestParam 을 써야 가능하다.

@RequestMapping(value = "/admin/sutudentManagement/list.do" , method = RequestMethod.POST)
	@ResponseBody
	public String studentList(@RequestParam HashMap<String, Object> paramMap) {
		LOGGER.info("paramMap : "  + paramMap);
		int total = 0;
		List<HashMap<String, Object>> rows = new ArrayList<HashMap<String,Object>>();
		
		DataTables dataTables = null;
		
		try {
			HashMap<String, Object> searchMap = null;
			if(paramMap.containsKey("searchData")) {
				searchMap = (HashMap<String, Object>) paramMap.get("searchData");
				paramMap.putAll(searchMap);
			}
			
			rows = service.selectStudentInfoList();
			total = service.selectStudentCount();
			
			dataTables = new DataTables(paramMap, rows, total);
		}catch (NullPointerException e) {
			LOGGER.error("error: " + e.toString());
		} catch (Exception e) {
			LOGGER.error("error: " + e.toString());
		}
		if(dataTables == null) {
			return "";
		}
		
		return dataTables.getJsonString();
	}

 

반응형
반응형

 

1. main.js 에 넣어 기본적인 유효성 검사 실행해보기

먼저, vue 버전에 맞는 vee-validate를 다운로드해준다. 

나는 vue 2 에서 구현할 것이기때문에, 

※ vee validate를 다운로드 했는데, vee-validate 가 작동하지 않는다면, 버전이 잘못된것이다. 그래서 본인 vue에 맞는 vee-validate를 다운로드 해야한다. 

npm install vee-validate@"<3.0.0" --save

로 다운로드 해준다. 

main.js

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import VeeValidate from "vee-validate";

Vue.use(VeeValidate);
Vue.prototype.$axios = axios;
Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");

이렇게 vee-validate를 추가해준다. 그러면, 모든 component 에서 사용이 가능해진다.

MemberRegisterView.vue

<template>
  <b-container fluid>
    <h1>Sign Up</h1>
    <b-form>
		<b-form-group
        label="Email"
        label-for="email">
        <b-form-input
          type="email"
          id="email"
          placeholder="Enter email address"
          v-model="user.email"
          v-validate="'required|email'"
          name="email" />
        <span class="warningText">{{ errors.first('email') }}</span>
      </b-form-group>
      <b-button
        variant="primary"
        @click="handleRegister">
        Sign Up
      </b-button>
    </b-form>
  </b-container>
</template>

<style scoped>
h1 {
  margin-bottom: 1.5rem;
}
.warningText {
  color: crimson;
}
</style>

이렇게 input 박스에 v-validate="'required|email'" 을 넣어주고, name값에 email 유효성을 봐주고 만약, 그 email 유효성에 어긋난다면, 이메일 주소를 맞게 입력해달라는 메시지를 <span> 태그에 넣어줄 것이다

나는 span 에 class값을 주어서 빨간색으로 보이게 설정해뒀다. 그런데, 영어로 되어있으니, 한글로 바꿔보자!

vee-validate에 설정되어잇는 error message를 customize할 수 있는데, 유효성에 대한 데이터만 따로 빼고 싶어서, utils라는 폴더를 생성해 veevalidateUtils.js 파일을 따로 빼두어 main.js 에 추가해줬다.

main.js

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import VeeValidate from "vee-validate";
import '@/utils/veevalidateUtils'

Vue.use(VeeValidate);
Vue.prototype.$axios = axios;
Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");

여기서, 아까 말한 이메일 형식에 어긋났을때의 메시지를 수정해 줄 것이다.

veevalidateUtils.js

import {Validator} from 'vee-validate';
import {email} from 'vee-validate/dist/rules.esm' // 기본적으로 제공되는 규칙을 모아둔 파일

// 존재하는 rule의 메세지를 바꾸기위해
Validator.extend('email', {
    ...email,
    getMessage: '이메일 형식이 아닙니다.',
})
// 새로운 rule 추가

존재하는 email 이라는 rule 의 메시지를 변경할 것인데, 여기서 ...email 은 email이라는 객체의 값을 각각 새로운 객체에 복제하는 코드이다. 참고 : https://joshua1988.github.io/es6-online-book/spread-operator.html

해주면, 

에러 메시지가 바뀐 것을 확인 할 수 있다.


2. 유효성검사에 어긋나면 다음으로 넘어가지않게 설정

참고: https://vee-validate.logaretm.com/v2/guide/components/validation-observer.html#scoped-slot-data

https://vee-validate.logaretm.com/v2/guide/events.html#disabling-events-validation

$validator 를 이용하자

veevalidateUtils.js

import {Validator} from 'vee-validate';
import {required, email} from 'vee-validate/dist/rules.esm' // 기본적으로 제공되는 규칙을 모아둔 파일

// 존재하는 rule의 메세지를 바꾸기위해
Validator.extend('required', {
    ...required,
    getMessage: '필수 입력 사항입니다.',
})
Validator.extend('email', {
    ...email,
    getMessage: '이메일 형식이 아닙니다.',
})
// 새로운 rule 추가
// 이름
const NAME_MIN = 2
const NAME_MAX = 16
const NAME_REGEX = new RegExp("^[가-힣a-zA-Z]{"+ NAME_MIN + "," + NAME_MAX +"}$")

Validator.extend('name', {
    validate(value){
        return NAME_REGEX.test(value)
    },
    getMessage: `이름은 한글과 영문 대 소문자를 사용하게요. (특수기호, 공백 사용 불가, ${NAME_MIN} ~${NAME_MAX}자)`,
})
// 비밀번호
const PW_MIN = 8
const PW_MAX = 16
const PW_REGEX = new RegExp("^(?=.*?[a-zA-Z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{" + PW_MIN + "," + PW_MAX + "}$")
Validator.extend('pw', {
    validate(value){
        return PW_REGEX.test(value);
    },
    getMessage: `비밀번호는 ${PW_MIN}~${PW_MAX}자 영문 대 소문자, 숫자, 특수문자 조합을 사용하세요.`
})

MemberRegisterView.vue

<template>
  <b-container fluid>
    <h1>Sign Up</h1>
    <b-form>
      <b-form-group
        label="Name"
        label-for="username">
        <b-form-input
          type="text"
          id="username"
          placeholder="Enter name"
          v-model="user.userName"
          v-validate="'required|name'"
          name="name" />
          <span class="warningText">{{ errors.first('name') }}</span>
      </b-form-group>

      <b-form-group
        label="Email"
        label-for="email">
        <b-form-input
          type="email"
          id="email"
          placeholder="Enter email address"
          v-model="user.email"
          v-validate="'required|email'"
          name="email" />
        <span class="warningText">{{ errors.first('email') }}</span>
      </b-form-group>
      <b-form-group
        label="Password"
        label-for="password">
        <b-form-input
          type="password"
          id="password"
          placeholder="Enter password"
          v-model="user.password"
          v-validate="'required|pw'"
          name="pw" />
          <span class="warningText">{{ errors.first('pw') }}</span>
      </b-form-group>
      <b-button
        variant="primary"
        @click.prevent="handleRegister">
        Sign Up
      </b-button>
    </b-form>
  </b-container>
</template>

<script>

export default {
  name: "MemberRegisterView",
  data() {
    return {
      user: {},
      submitted: false,
      successful: false,
      message: "",
    };
  },
  methods: {
   async handleRegister() {
      const validForm = await this.$validator.validateAll();
      console.log(this.$validator)
      console.log(validForm)
      if(!validForm) return alert('내용을 한번 더 확인해주세요')
      else{
        this.$store.dispatch("loginModule/signUp", this.user)

      }
    },
  },
};
</script>

<style scoped>
h1 {
  margin-bottom: 1.5rem;
}
.warningText {
  color: crimson;
}
</style>

script 부분에,

this.$validator 를 콘솔에 찍을걸 확인해보면,

이런식으로, ErrorBag 이라는 객체가 들어있는데 그 객체는 길이가 3인 Array 로 되어있다.

이런식으로 validate 가 false 이면, alert창이 뜬다. 

반응형

+ Recent posts