-오늘 수업 진도는 정말 쾌속 그 자체였다. 아무래도 시험과 다운로드 및 환경설정 때문에 시간을 너무 많이 써서 진도에 속도를 낼 필요가 있어서인 것 같다. 그래도 너무 빠른 진도에 진이 빠질 수준으로 빠른 수업을 진행하셨다.
-index.jsp파일에 로그인 화면을 띄워놓고 header부분과 navigation부분을 따로 include폴더에 빼서
<%@ include file="/include/head.jsp" %>
<%@ include file="/include/navigation.jsp" %>
이런식으로 include시켰다. 이렇게 해주는 이유는 어차피 매 페이지마다 들어갈 화면이기 때문에, 굳이 매 화면마다 작성해주는 것보다 이렇게 삽입시키는 것이 코드를 더 간결하게하고 수정도 용이하고, 재사용성을 높여주기 때문이다.
-로그인에 대한 정규식을 하지 않았고, 비어있는지만 확인하는 정도로만 jQuery를 만들었다. 다만, 한가지 전에 하지 않았던 것이 있는데, 바로 버튼을 누르는 것이 아니라 특정 input박스에서 특정 키를 눌렀을 때도 작동하도록 해보았다.
$("#userId").on("keypress", function(e){
if(e.which == 13)//enter란 소리 : jQuery의 keypress의 which를 찾아보면 됨. http://b1ix.net/170 참고하자.
{
//submit할 함수
fn_loginCheck(); //최종 submit 함수
}
});
여기서 e.which == 13은 enter키를 가리킨다. 숫자마다 가리키는 키가 있지만, 13번 말고는 딱히 쓰이지 않을 것 같은 느낌이 있다. 링크로 가면 각각의 값을 알 수 있게 주석으로 적어놓았다. 참고로 keypress말고 keyup과 keydown도 있는데, 계속 기능을 유지하기 위해서는(즉, 1회성이 아니기 위해서는) keypress를 쓰는 것이 바람직하다.
-AJAX통신으로 로그인한 아이디와 비밀번호의 값을 넘길 것이다. 그리고 타입은 JSON으로 속성과 값이 하나의 쌍으로 되어있는 구조의 타입이라고 보면 된다. 특히 수업에서는 제이쿼리의 AJAX통신을 사용했다.
$.ajax({
type: "POST",
url: "/loginProcAjax.jsp",
data:
{
userId: $("#userId").val(),
userPwd: $("#userPwd").val()
},
//data는 JSON방식이여서 속성과 값을 쌍으로
// 서버에서 받을 때 속성의 이름을 똑같이 받아야 함.
datatype: "JSON",
//돌아왔을 때 성공 실패를 구분해야 함.
success: function(obj) //여기서 성공여부는 서버에서의 성공이지 아이디와 비밀번호가 일치한다는 것이 아님
{
if(!icia.common.isEmpty(obj))// obj가 공백이 아님
{
icia.common.log(obj);
var data = JSON.parse(obj);
//JSON의 obj을 자를 것이라는 뜻. JSON의 짝꿍들을 짤라줌
//data를 받았는데, 그 중에서 "flag"인 값을 받음
//data의 flag 값이 없을 경우 인자 값 3번째가 -500을 세팅한다.
var flag = icia.common.objectValue(data, "flag", -500);
//flag가 0인 경우를 성공으로
if(flag == 0)
{
alert("로그인 성공!!");
}else //0이 아니면 오류인 경우
{
if(flag == -1) //-1인 경우는 비밀번호가 다름
{
alert("비밀번호가 올바르지 않습니다.")
$("#userPwd").focus();
}
else if(flag == -2) //-2는 블랙리스트나, 정지한 회원, 휴면계정 등등 사용이 정지된 아이디
{
alert("사용이 정지된 아이디입니다.");
$("#userId").focus();
}
else if(flag == -3) //아이디가 존재하지 않음.
{
alert("아이디와 일치하는 사용자 정보가 없습니다.");
$("#userId").focus();
}
else if(flag == -100) //사용자 페이지에서 아이디와 비밀번호가 아닌 것을 보냈음
{
alert("파라미터 값이 올바르지 않습니다.");
}
else //서버에선 왔는데 flag값이 이상한 경우가 있음
{
alert("오류가 발생했습니다.");
//뭔지 모르지만 오류났으니까 아이디 다시 입력해.
$("#userId").focus();
}
}
}
},
//응답이 종료되고 나서 실행되는 complete.
complete: function(data)
{
icia.common.log(data);
},
//얘는 고정, 실패의 경우
//프로그램 상의 오류가 아니라 서버 자체의 오류인 경우임.
error: function(xhr, status, error)
{
icia.common.log(error);
alert("로그인 에러!");
}
});
위에 잘 보면 해당 로그인의 결과를 loginProcAjax.jsp로 넘긴다. 여기서 AJAX통신을 왜 하는지에 대해 간단하게 설명하자면, 기존처럼 보내는 방식은 보낸 후에 결과가 리턴될 때까지 멈춰서 대기해야하는 동기방식이었지만, AJAX통신을 사용하면, 비동기 방식으로 내 로그인에 대한 처리는 loginProcAjax.jsp에서 해주고, 나는 현재 보여지는 화면에서 계속 다른 것을 할 수 있게 되는 것이다. 따라서 loginProcAjax.jsp는 따로 보여주는 화면이 없다. 단순히 넘겨받은 값을 DB의 값과 비교해서 결과인 flag값을 넘겨줄 뿐이다. 이 때 넘겨주는 flag값은 각각 index.jsp에서 정의한다.
-이제 DB에서 사용할 user의 아이디와 비밀번호 등의 정보를 담는 TBL_USER 테이블을 만들고, 원래같으면 회원가입을 해서 인서트해야겠지만, 우선은 직접 인서트해서 아이디가 test이고 비밀번호가 1234인 유저를 하나 만든다.
--비밀번호, 아이디,
CREATE TABLE TBL_USER
(
USER_ID VARCHAR2(20) NOT NULL,
USER_PWD VARCHAR2(20) NULL,
USER_NAME VARCHAR2(30) NULL,
USER_EMAIL VARCHAR2(50) NULL,
STATUS CHAR(1) NULL,
REG_DATE DATE NULL
);
--뭔지 알지 위한 커멘트
COMMENT ON COLUMN TBL_USER.USER_ID IS '사용자 아이디';
COMMENT ON COLUMN TBL_USER.USER_PWD IS '비밀번호';
COMMENT ON COLUMN TBL_USER.USER_NAME IS '사용자명';
COMMENT ON COLUMN TBL_USER.USER_EMAIL IS '사용자 이메일';
COMMENT ON COLUMN TBL_USER.STATUS IS '사용여부(Y:사용, N:정지)';
COMMENT ON COLUMN TBL_USER.REG_DATE IS '등록일';
--인덱스 생성
CREATE UNIQUE INDEX XPK_USER ON TBL_USER(USER_ID ASC);
--값은 대소문자 구분함
--가입하면 무조건 Y를 줄 것임. 가입한 시점의 SYSDATE를 넣어주면 됨.
INSERT INTO TBL_USER VALUES('test', '1234', '테스트1', 'test@icia.co.kr', 'Y', SYSDATE);
COMMIT;
--아이디를 조회할 때, 존재하는지 여부는 COUNT를 사용하면 됨.
--CNT는 변수명임.
SELECT COUNT(USER_ID) AS CNT
FROM TBL_USER
WHERE USER_ID = 'test';
--보통 비밀번호만 가져오는 쿼리는 만들지 않음.
--그냥 일단 전부 가져옴.
--사용자 아이디 정보 조회
SELECT
USER_ID,
NVL(USER_PWD, '') USER_PWD,
NVL(USER_NAME, '') USER_NAME,
NVL(USER_EMAIL, '') USER_EMAIL,
NVL(STATUS, '') STATUS,
NVL(TO_CHAR(REG_DATE, 'YYYY.MM.DD HH24:MI:SS'), '') AS REG_DATE
FROM
TBL_USER
WHERE USER_ID = 'test';
-아래쪽의 쿼리문은 나중에 데이터를 조회하기 위해 미리 써본 쿼리문이다.
이제 해당 테이블에 맞춰서 .java파일을 하나 만들어줘야 한다. 단, 이 때 DB의 정보가 노출되지 않도록 데이터를 캡슐화해야 하기 때문에 private 접근지정자를 설정하고 getter와 setter를 만든다.
package com.icia.web.model;
import java.io.Serializable;
public class User implements Serializable {
//서버랑 싱크를 맞추기 위한 용도
private static final long serialVersionUID = 1L;
private String userId;
private String userPwd;
private String userName;
private String userEmail;
private String status; //상태 Y는 사용, N은 정지
private String regDate;
public User()
{
//각각의 값들을 초기화 함. String type이니까 초기값을 ""으로 넣음.
userId = "";
userPwd = "";
userName = "";
userEmail = "";
status = "";
regDate = "";
}
//getter와 setter.
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserPwd() {
return userPwd;
}
public void setUserPwd(String userPwd) {
this.userPwd = userPwd;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserEmail() {
return userEmail;
}
public void setUserEmail(String userEmail) {
this.userEmail = userEmail;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getRegDate() {
return regDate;
}
public void setRegDate(String regDate) {
this.regDate = regDate;
}
}
-쿼리문에 대한 .java를 만들기 전에 DB와 연동하는 자바 코드를 작성해야 한다.
package com.icia.web.db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public final class DBManager {
//상수를 쓸 것이기 때문에 final 붙여줌.
//얘가 커넥션 풀이 될 것.
//jdbc 드라이버 명
private static final String driverClassName = "oracle.jdbc.OracleDriver";
//jdbc url
private static final String jdbcUrl = "jdbc:oracle:thin:@localhost:1521:xe";
//오라클 로그인 계정
private static final String userName = "c##iciauser";
private static final String password = "1234";
//생성자
private DBManager()
{
}
//데이터베이스 연결 객체 얻기
public static Connection getConnection()
{
Connection conn = null;
try
{
Class.forName(driverClassName);
conn = DriverManager.getConnection(jdbcUrl, userName, password);
}
catch(ClassNotFoundException e)
{
//콘솔로그에 찍는 것
System.out.println("[DBManager]getConnection ClassNotFoundException");
}
catch(SQLException e)
{
System.out.println("[DBManager]getConnection SQLException");
}
return conn;
}
//resultSet 객체 닫기
//DB를 닫을 때, 맥시멈 세 가지를 닫아야 함.
//DB에서 select를 했을 때, 여러 컬럼을 리턴함. 그때 오는 것이 ResultSet임.
//닫는 순서는 ResultSet(select일 때만 닫음), PreparedStatement(status일 때만 닫음. *를 전부 불러옴.), Connection 순서
public static void close(ResultSet rs)
{
//메소드 오버로딩 하는 것.
close(rs, null, null);
}
//PreparedStatement 객체 닫기
public static void close(PreparedStatement pstmt)
{
close(null, pstmt, null);
}
//Connection 객체 닫기.
public static void close(Connection conn)
{
close(null, null, conn);
}
//하나만 넘어오는 경우도 있지만, 두 개가 같이 넘어오는 경우가 당연히 있음.
//그래서 두 개짜리 다 해야됨.
//ResultSet, PreparedStatement 객체 닫기
public static void close(ResultSet rs, PreparedStatement pstmt)
{
close(rs, pstmt, null);
}
//PreparedStatement, Connection 객체 닫기, insert update delete의 경우
public static void close(PreparedStatement pstmt, Connection conn)
{
close(null, pstmt, conn);
}
//데이터베이스 객체 닫기
public static void close(ResultSet rs, PreparedStatement pstmt, Connection conn)
{
if(rs != null)
{
try
{
rs.close();
//예외처리를 이쪽으로 넘겼기 때문에 try catch구문으로 구현해야 함.
}catch(SQLException e)
{
System.out.println("[DBManager]close ResultSet SQLException");
}
}
if(pstmt != null)
{
try
{
pstmt.close();
}catch(SQLException e)
{
System.out.println("[DBManager]close PreparedStatement SQLException");
}
}
if(conn != null)
{
try
{
conn.close();
}catch(SQLException e)
{
System.out.println("[DBManager]close Connection SQLException");
}
}
}
//commit 모드 변경(auto냐, 직접 해야 커밋이 되냐)
//auto인 경우 수동으로 바꾸고, 수동인 경우 오토로 바꾸는 용도
//flag는 auto냐 아니냐
public static void setAutoCommit(Connection conn, boolean flag)
{
if(conn != null)
{
try
{
if(conn.getAutoCommit() != flag)
{
conn.setAutoCommit(flag);
}
}
catch(SQLException e)
{
System.out.println("[DBManager]setAutoCommit SQLException");
}
}
}
//rollback
public static void rollback(Connection conn)
{
if(conn != null)
{
try
{
conn.rollback();
}
catch(SQLException e)
{
System.out.println("[DBManager]rollback SQLException");
}
}
}
}
-여기서 중요한 점은, DB와 연결한 후 반드시 최대 3가지를 close해줘야 하는데 각각, ResultSet, PreparedStatement, Connection이다(순서도 이 순서로 닫아줘야 하며, null값이 오는 상황들을 전부 오버로딩해줘야 한다.). ResultSet은 SQL문으로 얻어온 컬럼의 값들을 테이블의 형태로 가지고 있는 객체이다. 따라서 서버로부터 데이터를 얻어올 때 반드시 사용해야 하는 개체이다. PreparedStatement는 ResultSet이라는 결과를 리턴하는 쿼리를 실행할 수 있는 객체이다.(.executeQuery()메서드를 통해서) 단, 매 실행시마다 컴파일을 해야하는 Statement와는 다르게 Statement를 상속받은 PreparedStatement는 ?를 통해서 쿼리문에 인자를 사용할 수 있다. 그래서 매번 컴파일을 진행하지 않더라도 ?의 부분을 변경하여서 해당 객체를 재사용할 수 있다. 단, 반드시 Connection 객체를 통해 연결해서 사용해야 한다.
-이제 UserDao라는 클래스를 만들어서 SQL문을 넣어준다.
package com.icia.web.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import com.icia.web.db.DBManager;
import com.icia.web.model.User;
//모든 sql문은 dao 패키지에 저장
public class UserDao {
public UserDao()
{
}
//사용자 아이디 체크
public int userIdSelectCount(String userId)
{
int count = 0;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
//얜 또 뭘까. 아마도 sql문을 받는 객체라고 생각하면 되겠지?....
StringBuilder sql = new StringBuilder();
//괄호 안에 공백 넣기 필수
sql.append("SELECT COUNT(USER_ID) AS CNT ");
sql.append(" FROM TBL_USER ");
sql.append(" WHERE USER_ID = ? ");
//?에는 값을 담아서 날릴 것임. 라운딩 변수라고 함. 찾아보기
try
{
conn = DBManager.getConnection();
//쿼리문 얻어올 때는 toString으로 변환
pstmt = conn.prepareStatement(sql.toString());
//인덱스 값과 받은 userId를 받음
pstmt.setString(1, userId);
//executeQuery에 인수를 안넣는 이유는 이미 pstmt에 담겨 있음.
rs = pstmt.executeQuery();
//rs는 처음에 아무값도 안가리키다가, next를 하면 1번째 값을 가리킴
if(rs.next())
{
count = rs.getInt("CNT");
}
}
catch(Exception e)
{
System.out.println("[UserDao]userIdSelectCount Exception");
}
finally
{
DBManager.close(rs, pstmt, conn);
}
return count;
}
//사용자 조회
//User 객체를 리턴하겠다. 사용자 정보 전체를 리턴하겠다.
public User userSelect(String userId)
{
User user = null;
int count = 0; //얘는 존재여부를 확인하기 위한 메소드.
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
StringBuilder sql = new StringBuilder();
sql.append("SELECT ");
sql.append(" USER_ID, ");
sql.append(" NVL(USER_PWD, '') USER_PWD, ");
sql.append(" NVL(USER_NAME, '') USER_NAME, ");
sql.append(" NVL(USER_EMAIL, '') USER_EMAIL, ");
sql.append(" NVL(STATUS, '') STATUS, ");
sql.append(" NVL(TO_CHAR(REG_DATE, 'YYYY.MM.DD HH24:MI:SS'), '') AS REG_DATE ");
sql.append(" FROM ");
sql.append(" TBL_USER ");
sql.append(" WHERE USER_ID = ? ");
try
{
conn = DBManager.getConnection();
//쿼리문 얻어올 때는 toString으로 변환
pstmt = conn.prepareStatement(sql.toString());
//인덱스 값과 받은 userId를 받음
pstmt.setString(1, userId);
//executeQuery에 인수를 안넣는 이유는 이미 pstmt에 담겨 있음.
rs = pstmt.executeQuery();
//rs는 처음에 아무값도 안가리키다가, next를 하면 1번째 값을 가리킴
if(rs.next())
{
user = new User();
//원래는 null체크 다 해야함. 원래 공통모듈 적용해서 null값을 제외해야 함.
user.setUserId(rs.getString("USER_ID"));
user.setUserPwd(rs.getString("USER_PWD"));
user.setUserName(rs.getString("USER_NAME"));
user.setUserEmail(rs.getString("USER_EMAIL"));
user.setStatus(rs.getString("STATUS"));
user.setRegDate(rs.getString("REG_DATE"));
}
}
catch(Exception e)
{
System.out.println("[UserDao]userSelect Exception");
}
finally
{
DBManager.close(rs, pstmt, conn);
}
return user;
}
}
-이제 해당 DB의 사용자 정보와 입력한 값이 일치하는지를 판단하는 loginProcAjax.jsp를 작성한다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.icia.web.model.User" %>
<%@ page import="com.icia.web.dao.UserDao" %>
<%
//아이디 비밀번호를 입력하면, UserDao와 User객체를 이용해서 결과값만 던져줌. index.jsp랑 User와 UserDao를 연결해주는 것.
//객체 임포트 시킴. 원래 로그인할 때 쿠키를 이용할 것인데, 오늘은 빼기.
String userId = request.getParameter("userId");
String userPwd = request.getParameter("userPwd");
//null 체크 하나도 안한 것, 내일 추가해줄 것!
//정상적일 때, 파라미터를 받았을 때
UserDao userDao = new UserDao();
//User객체에 UserDao의 쿼리문 날린 결과를 가져옴.
User user = userDao.userSelect(userId); //userDao의 userSelect의 리턴값이 User임.
if(user != null) //유저 정보가 있다는 소리
{
//상태가 휴먼이나 정지된 게정이 아닌지 확인
if(user.getStatus().equals("Y"))
{
//아이디와 비밀번호 따로하는 이유는 어디가 잘못되었는지 모르기 때문.
if(user.getUserPwd().equals(userPwd))
{
//입력한 비밀번호와 db의 비밀번호가 같냐?
//넘겨받을 때, AJAX통신의 JSON방식으로 받았기 때문에 넘길 때도 그렇게 넘겨야함.
response.getWriter().write("{\"flag\":0}");
}
else
{
//비밀번호 불일치
response.getWriter().write("{\"flag\":-1}");
}
}
else
{
//정지된 사용자
response.getWriter().write("{\"flag\":-2}");
}
}
else
{
//index.jsp에 flag값을 -100으로 보내주는 것.
response.getWriter().write("{\"flag\":-3}");
}
%>
이런식으로 하면 로그인 성공여부를 알 수 있음. 단 로그인 시 DB에서 인서트한 아이디와 비밀번호로만 성공할 수 있음
-사실 지금 이렇게 하나하나 뜯어보니까 막 못따라갈 수준은 아니고, 할 수 있겠다 싶은데, 수업당시에는 정말 멘붕에 멘붕에 멘붕이었다. 도대체 모르는 객체와 메소드들이 너무 많이 나와서 이걸 정말 쓸 수 있을까 하는 걱정거리만 한가득이었다. 그래서 많이 어려웠지만 이렇게 시간을 내서 복습을 하니 그래도 내일 따라갈 수 있겠다는 생각이 든다. 내일은 더 복잡해진다는데...그래도 오늘구조까지는 이해했으니까 파이팅하자!!