[ 직렬화(serializable)와 serialVersionID란? ]



객체를 파일에 쓰거나, 파일에서 객체로 읽어오거나 혹은 네트워크를 통해 객체를 전송할 때 serializable 인터페이스를 구현(직렬화)하는 것을 본적 있을 것이다.


그림 직렬화란 무엇일까?

쉽게 말해


직렬화??

 - 객체를 전송하는 것은 파일로도 네트워크를 통해 서버로, 프로그램과 프로그램 사이에 많을 것이고

   또한, 서로 다른 언어를 사용하는 이들이 데이터를 주고 받으려면 주고받는 쪽이 모두 이해할 수 있는 언어가 필요할 것이다.

예를들자면

A -> B로 보낼 때 A는 프랑스 사람이고 B는 한국 사람인데 둘 다 서로의 언어는 모르지만 둘 다 영어는 할 줄 안다치면

둘이 데이터를 주고받을 때 객체를 영어로 변환(직렬화)하는 것이다. 그리고 주고 받으면 자신들의 언어로 역직렬화 하는 것!


그럼 비유를 마저 들어

serialVersionID는 무엇일까?

- 서로 데이터를 주고 받는 과정에서 여러군대와 주고받다보면 어떤 클래스인지 정확히 구분이 어려운 경우가 있을 것이다.

  즉, 주고받은 클래스 객체를 고유하게 구분하는 코드와 같은 것!


따라서, 주로 JAVA DTO 클래스에 implements Serializable했을 때 

final static Long serialVersionID = ~~ 라고 지정을 하지 않으면 노란색 삼각형으로 warning이 발생하는 것을 볼 수 있다.

하지만, 구현하지 않을 경우는 자바 컴파일러가 자동으로 default로 생성해주게 된다.

하지만, 경고는 보기 싫으니까 클래스 명에서 ctrl + 1눌러 serialVersionID를 자동으로 생성해주도록 하자.

(간혹 자동생성이 안뜨는 경우... 자동생성 플러그인을 이용하자.)


 

'스터디 > Java' 카테고리의 다른 글

스트림(Stream) - Java8  (0) 2019.07.11
자바 정규표현식  (0) 2018.10.19

[    JSP 자바 빈즈(JAVA BEANS)    ]







그렇다면 지금까지 살펴본 빈즈는 왜 사용하는 것일까?? 무엇인가 편리한게 있지 않을까?


그렇다. 빈즈를 이용하면 우리가 form 태그에서 input 태그 등을 이용해서 많은 값들을 submit으로 보낼 때


이러한 값들을 일일이 하나씩 request.getParameter("name") 등으로 가져오지 않고 한번에 빈즈 객체에 저장시킬 수 있다.


매우 편리하다!!


하지만, 조건이 있다. form태그에 있는 input 태그 등의 name값과 빈즈의 필드변수명이 일치해야만 한다.


아래의 예제를 보자.


-PeopleBeans 클래스



-beansMain.jsp : 폼태그가 있는 페이지


-beans.jsp : 폼태그에서 서브밋해 데이터를 전달받을 페이지



'스터디 > JSP' 카테고리의 다른 글

JSP 커넥션 풀(Connection Pool)  (0) 2017.08.16
JSP DB(데이터베이스)  (0) 2017.08.14
JSP 에러페이지에 대하여...  (0) 2017.08.11
JSP 쿠키(cookie)에 대하여...  (0) 2017.08.11
JSP에서의 세션(session)에 대해서...  (0) 2017.08.11

[    http://partnerjun.tistory.com/51 님 블로그 내용을 포스팅한 것을 미리 밝힙니다. ]


1. Chrome 개발자도구 Network 탭

크롬에서 F12키나 Ctrl+Shift+I 혹은 메뉴의 '도구 더 보기'에서 열 수 있는 개발자도구는 아주 강력하다. HTML dom 탐색은 물론 javascript나 css 소스를 탐색하고 수정할 수 있을뿐 아니라 수정해 곧바로 적용해 볼 수 있다. 이번 포스트에서 주로 사용하는 Network탭에서는 실시간으로 Request / Response 정보를 확인 수 있다.



각 요청이 시작되기 전/후를 스크린샷으로 남기는 기능과 원하는 요청만 표시하는 필터같은 유용한 기능도 있다. 다양한 기능을 한번씩 사용해 보면 많은 도움이 된다.




2. Jsoup로 네이버 검색어 자동완성 목록 얻어오기

네이버 검색창에 단어를 입력했을 때 나오는 검색어 자동완성 목록을 Jsoup로 얻어보자.


이거.



먼저 크롬의 개발자 도구를 열어 두고 네이버 검색창에 단어를 입력해 보자. 키를 누를 때마다 Request/Response가 감지된다.





검색창에 입력할 때마다 특정 URL로 get Request가 있다는 사실을 알 수 있다. Response 탭을 이용해 Response를 확인해 보자.




뭔가 이상하다. 이건 무슨 코드일까?




다시 Request의 헤더를 보자. _callback 파라미터와 Response의 첫 부분이 같다는 사실을 알 수 있다. 


 


또한 window라는 Javascript Object와 __jindo_callback... 형식이 함수와 유사하다는 점을 통해


 클라이언트에서 _callback 파라미터로 함수의 이름을 전달하고, 

 서버에서 _callback 파라미터로 전달된 함수의 파라미터로 '결과' Json을 적어 반환해 

 클라이언트에서 결과 '문자열'을 실행하거나 정의하는 형식이라고 추측할 수 있다. 


그림으로 표현하면 아래와 같다.



그야말로 막연한 추측이다. 하지만 _callback 파라미터를 조정해 볼 필요는 있다. 한번 시도해 보자.



Chrome 확장프로그램 Advanced REST client로 테스트한 결과.

_callback 파라미터를 공백으로 요청하자 Json 형태로 결과를 얻을 수 있었다.



개인적인 경험상, 개발의 편의성 때문인지 HTML 코드를 그대로 반환하는 사이트가 가장 많고, 그 다음으로 위와 같이 Json과 다른 형식의 코드가 섞인 경우가 많았다. Json 포맷으로 Response가 오는 정직한 경우는 별로 없으니 얻어낸 문자열을 다시 가공하거나 위의 경우처럼 파라미터를 조정해 볼 필요가 있다.



아무튼, 위에서 알아낸 URL과 Request 헤더들을 이용해 Jsoup로 네이버의 검색어 자동완성 목록을 얻어내 보자.


String q = "스칼라"; // 검색어

Document doc = Jsoup.connect("https://ac.search.naver.com/nx/ac")
.header("origin", "http://www.naver.com")
.header("referer", "https://www.naver.com/")
.header("accept-encoding", "gzip, deflate, sdch, br")
.data("con", "1")
.data("_callback", "") // _callback 파라미터를 비우면 JSON이 리턴된다!
.data("rev", "4")
.data("r_enc", "UTF-8")
.data("q", q) // 임의로 몇개의 파라미터와 헤더를 생략했다.
.data("st", "100") // 각 파라미터가 무엇을 뜻하는지를 확인해 적절하게 사용하는 것도 좋지만
.data("q_enc", "UTF-8") // 비정상적인 요청으로 감지해 아이디나 아이피가 밴 될 우려도 있으므로
.data("r_format", "json") // 특별한 이유가 없다면 모두 포함하는 것이 좋다.
.data("t_koreng", "1")
.data("ans", "2")
.data("run", "2")
.ignoreContentType(true) // HTML Document가 아니므로 Response의 컨텐트 타입을 무시한다.
.get();

List<String> result = new ArrayList<>();

// org.json 라이브러리를 사용해 결과를 파싱한다.
JSONObject jsonObject = new JSONObject(doc.text());

JSONArray items = (JSONArray) ((JSONArray) jsonObject.get("items")).get(0);
for(int i=0; i<items.length(); i++) {
String item = (String) (((JSONArray) items.get(i)).get(0));
result.add(item);
}

// 얻어낸 추천 검색어 목록.
// 테스트 프로젝트의 자바 버전이 낮아 for문을 사용했다.
for(String item : result) {
System.out.println(item);
}
/*
스칼라티움 강남
스칼라티움
구글스칼라
스칼라
강남 스칼라티움
첼로 스칼라티 105
스칼라티움 상암
수원 스칼라티움
상암 스칼라티움
첼로 스칼라티
구리 스칼라티움
스칼라 동시성 프로그래밍
스칼라 월드 북스 3
스칼라 월드 북스 4
스칼라 월드 북스 5
*/


원하는 정보를 얻어냈다.(웨딩홀 이름이 가장 위라니 조금 슬프다)




이 예제에는 없었지만 XMLHttpRequest 객체를 사용하는 Request에는 'X-Requested-With' 헤더 값으로 'XMLHttpRequest'가 전송되기도 한다. 다시한번 말하지만 사이트마다 다르고 비정상적인 요청으로 간주될 수 있으니 브라우저에서 직접 헤더를 확인해 보고 Jsoup의 헤더에 똑같이 작성하는 것이 좋다.

[ http://partnerjun.tistory.com/43 님의 티스토리 글을 포스팅한 내용임을 미리 밝힙니다. ]


이 포스트에서는 로그인이 필요한 사이트와 Request Header를 검사하는 사이트를 파싱하는 과정을 적어둔다.



0. 웹 사이트 로그인

먼저 웹 사이트에 로그인에 대해 다시 생각해 볼 필요가 있다. 최근 웹 사이트에서 사용되는 로그인 방법은 크게 두 가지로 볼 수 있다. 첫 번째는 세션을 이용한 방법이고, 두 번째는 Restful API에 주로 사용되는 토큰 인증이다. 발급 받은 토큰을 이용하는 방법은 이전 포스트에서 원하는 값을 Jsoup의 Document를 파싱해 얻어낸 것처럼, 간단하게 얻어낼 수 있다. 물론 토큰이 HTML요소가 아니라 Script 요소로 있는 경우도 많지만 정규식이나 replace, split 같은 메소드를 이용하면 별 어려움이 없다.


다시 첫 번째 세션 로그인으로 돌아가면, 세션은 결국 쿠키라는 사실을 기억해야 한다. 상태를 유지하지 않는 HTTP 프로토콜의 특성 상 사이트에 로그인하는데 성공하면 서버는 클라이언트에게 세션ID를 발급해주고 

ID/PW는 Request에, 세션ID는 쿠키에 담겨 있다

 

클라이언트는 서버로부터 받은 세션ID를 다음 Request부터 쿠키에 포함해 전송하게 된다. 서버는 클라이언트가 전송한 쿠키에서 얻어낸 세션ID를 이용해 이 유저가 '로그인 한' 유저인지 여부를 확인할 수 있게 된다.


Response에도 당연히 쿠키가 포함되어 있다.

뭐 결국 간단히 말하자면 세션으로 로그인을 체크하는 사이트라면 로그인하고 얻은 쿠키를 다음 Request부터 계속 사용하면 된다는 말이다.


 

0. 사이트의 CSRF Token, Request Header

대부분의 유명한(사용자가 많은) 사이트에서는 비정상적인 접근을 막기 위해 여러가지 방법을 사용한다. 그 중 신경써야 할 것은 CSRF 토큰과 Request Header이다. 

 

CSRF 토큰은 로그인 시도 전에 한 가지 단계를 더 거치면 된다. 로그인을 처리하는 URL에 바로 요청하는 것이 아니라 '로그인 페이지' 에 접근해서 토큰을 얻어낸 후로그인 처리 URL에 토큰을 포함해 요청해야 한다. 티스토리를 예로 들어보자.

 

파란색으로 표시된 '눈에 보이는' 직접 입력하는 fieldset 외에도 

특수한 키가 적혀있는 파라미터 두 개가 있다.

 

스크린샷에서 볼 수 있는 두 가지 파라미터 "ofp"와 "nfp"처럼 로그인 페이지에 접속해야 얻을 수 있는 값이 있다. 때문에 먼저 '로그인 페이지'에 접근해 저런 값들을 얻어낸 후, '로그인 처리 URL'에 보내는 데이터에 그 값들을 포함하면 된다.

 

Request Header는 HTTP 표준에 맞게 전송하는 것이 원칙이다. 정상적인 브라우저라면 따로 신경 쓸 필요가 없지만 Jsoup를 통한 접속에서는 신경써야 한다. 몇몇 사이트에서는 Request Header를 철저하게 검사해 접근을 막거나 아이디를 밴하기도 한다. 

Header값들을 얻는 가장 쉬운 방법은 브라우저로 직접 로그인 해 보고 헤더 값들을 모두 복사해 그대로 사용하는 것이다.


 

티스토리에 로그인할때 전송된 Request header

(Chrome 확장 프로그램 HTTP Headers를 이용함)

 

다른 값들은 그냥 넣는다고 해도 User-Agent만큼은 조금 신경 쓸 필요가 있다. 사용자의 브라우저를 확인하는 값이기 때문이다. 이 값을 모바일 브라우저로 변환한다면 모바일 페이지를 따로 사용하는 사이트에서는 모바일 페이지로 리다이렉트된다. 만약 얻어내고자 하는 값이 모바일 화면의 값이라면 적절한 User-Agent를 하나 구해 사용하자.(기종명 User Agent로 검색하면 다 나온다)




그럼 이제 티스토리에 로그인하고 블로그 관리 페이지에서 내 블로그 목록을 얻어내는 코드를 만들어 보자.


select의 option값


1. 티스토리 로그인 페이지에 접속해 토큰 얻어내기

위에서 본 것처럼 티스토리에서는 로그인에 두 가지 토큰을 발급받아 전송한다. 각각이 무슨 의미인지는 모르겠지만 일단 가져와 보자.

// 로그인 페이지 접속
Connection.Response loginPageResponse = Jsoup.connect("https://tistory.com/auth/login/")
.timeout(3000)
.header("Origin", "http://tistory.com/")
.header("Referer", "https://www.tistory.com/auth/login")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4")
.method(Connection.Method.GET)
.execute();

// 로그인 페이지에서 얻은 쿠키
Map<String, String> loginTryCookie = loginPageResponse.cookies();

// 로그인 페이지에서 로그인에 함께 전송하는 토큰 얻어내기
Document loginPageDocument = loginPageResponse.parse();

String ofp = loginPageDocument.select("input.ofp").val();
String nfp = loginPageDocument.select("input.nfp").val();

첫 번째 포스트와 마찬가지지만 필요한 헤더를 작성하고 get() 이나 post() 메소드가 아니라 execute() 메소드를 이용해 Document보다 상위 객체인 Response 객체를 얻어왔다. Response 객체의 cookies() 메소드를 이용해 쿠키를 얻어내고, parse() 메소드로 Document를 얻어낸 후 Document에서 두 가지 토큰을 가져왔다. 

티스토리는 로그인 페이지에 접근하기만 해도 뭔지 모를 쿠키들을 전송해 주기 때문에 로그인 페이지에서부터 쿠키를 가져왔다.



2. 로그인하고 로그인 세션ID 얻어내기

먼저 로그인을 처리하는 URL, 즉 form의 action과 method, 전송할 값들을 알아내야 한다.


form의 method와 action(로그인 처리 URL)


전송해야 하는 파라미터는 "redirectUrl", "loginId", "loginPw", "rememberLoginId"와 

토큰 "ofp", "nfp" 총 여섯 개다.

티스토리는 아주 정직하게 태그에 표시되어 쉽게 알 수 있지만 어떤 사이트는 자바스크립트로 어지럽게 작성되어 있다. 그런 경우는(특히 js가 압축된 경우!) 크롬 개발자도구의 Network 탭을 이용하면 편하다.


위에서 확인한 파라미터를 이용해 Jsoup Connection의 데이터로 추가하고 post로 요청하면 '로그인 된' 세션ID를 얻어낼 수 있다.

// Window, Chrome의 User Agent.
String userAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36";

// 전송할 폼 데이터
Map<String, String> data = new HashMap<>();
data.put("loginId", "아이디");
data.put("password", "비밀번호");
data.put("rememberLoginId", "1");
data.put("redirectUrl", "http://tistory.com/");
data.put("ofp", ofp); // 로그인 페이지에서 얻은 토큰들
data.put("nfp", nfp);

// 로그인(POST)
Connection.Response response = Jsoup.connect("https://www.tistory.com/auth/login")
.userAgent(userAgent)
.timeout(3000)
.header("Origin", "http://tistory.com/")
.header("Referer", "https://www.tistory.com/auth/login")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4")
.cookies(loginTryCookie)
.data(data)
.method(Connection.Method.POST)
.execute();

// 로그인 성공 후 얻은 쿠키.
// 쿠키 중 TSESSION 이라는 값을 확인할 수 있다.
Map<String, String> loginCookie = response.cookies();

이제 로그인에 성공했다. 얻어낸 이 '로그인 된' 쿠키를 계속 사용하면 된다. 세션ID의 키는 서버사이드 설정에 따라 언어의 기본 값(PHP는 PHPSESSID, JSP는 JSESSIONID 등)이거나 따로 지정한 이름이다. 딱히 중요한 내용은 아니지만 서버사이드 언어를 유추하는 방법 중 하나가 된다.



3. 티스토리 블로그 관리 페이지에서 내 블로그 목록 얻어내기

위에서 얻은 쿠키를 사용한다는 점 외에는 이전 포스트와 차이가 없다. 접속하고 값을 얻어내면 된다. 

// 티스토리 관리자 페이지
Document adminPageDocument = Jsoup.connect("http://partnerjun.tistory.com/admin")
.userAgent(userAgent)
.header("Referer", "http://www.tistory.com/")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept-Encoding", "gzip, deflate, sdch")
.header("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4")
.cookies(loginCookie) // 위에서 얻은 '로그인 된' 쿠키
.get();

// select 내의 option 태그 요소들
Elements blogOptions = adminPageDocument.select("select.opt_blog > option");

// 블로그 이름과 url 얻어내기
for(Element option : blogOptions) {
String blogName = option.text();
String blogUrl = option.attr("abs:value");

System.out.println(blogName); // 간단한 블로그
System.out.println(blogUrl); // http://partnerjun.tistory.com/admin/center/
}


최근 많은 사이트에서는 보안 목적으로 로그인 후에 추가적인 과정을 요구하기도 한다. 대표적인 것은 새로운 기기에서의 로그인 시 이메일 체크나 capcha다. 이메일 체크야 직접 한번 해주면 되지만 capcha는 아직 만만한 문제가 아니다. 기계학습을 이용해 capcha를 해결하는 방법이 나왔다는 이야기를 들었는데 얼마 후면 라이브러리로 제공될지도 모르겠다. 기술의 발전을 기뻐해야 하는 건지, 개발하는 측에 있는 사람으로써 두려움에 떨어야 하는지는 모를 일이다.


아무튼, Jsoup로 로그인하고 '로그인 한' 사용자만 접근 가능한 페이지의 값을 얻어내 보았다. 다음 포스트에서는 XMLHttpRequest 객체를 이용한 Ajax 요청을 Jsoup로 해 보려고 한다(사실 특별한 내용은 없지만 크롬 개발자도구의 Network탭 그림 때문에 분리한다).

[ 해당 포스팅은 http://partnerjun.tistory.com/guestbook 님 티스토리 블로그 내용을 포스팅 한 것임을 밝힙니다. ]



Jsoup는 아주 강력하고 재미있는 라이브러리다. 단순한 HTML 문서 파싱을 넘어 웹 사이트에 대한 Request, Response를 모두 처리할 수 있다. 덕분에 일부 특별한 경우(플래시, 애플릿, ActiveX같은 비표준이나 WebSocket)가 아니라면 브라우저로 사이트를 이용하는 상황을 그대로 재현해낼 수 있다. 다시 말해, 대부분의 사이트의 원하는 정보만 뽑아내는 '뷰어'를 만들 수 있다는 것이다. 몇 가지 간단한 예제를 통해 사이트에서 원하는 정보만 뽑아내는 과정을 적어보려 한다.



0. Gradle 디펜전시 추가

compile group: 'org.jsoup', name: 'jsoup', version: '1.10.2'

Maven Repository를 통해 간단하게 디펜전시를 추가 할 수 있다.


Jsoup는 크게 static 메소드를 체이닝해서 URL(혹은 로컬HTML)에 연결하고 결과를 얻어오는 org.jsoup.Jsoup 패키지와 얻어온 결과의 구조를 위한 객체들이 포함된 org.jsoup.nodes 패키지, 연결 방법과 Response, Request등을 가지고 있는 org.jsoup.Connection 패키지로 이루어져 있다. 



Jsoup의 주요 요소는 크게 다섯 가지로 볼 수 있다.


Document 

 Jsoup 얻어온 결과 HTML 전체 문서

Element

 Document의 HTML 요소

Elements

 Element가 모인 자료형. for나 while 등 반복문 사용이 가능하다.

Connection

 Jsoup의 connect 혹은 설정 메소드들을 이용해 만들어지는 객체, 연결을 하기 위한 정보를 담고 있다.

Response

 Jsoup가 URL에 접속해 얻어온 결과. Document와 다르게 status 코드, status 메시지나 charset같은 헤더 메시지와 쿠키등을 가지고 있다.




Jsoup로 하는 작업은 크게 Connection 객체를 통해 URL에 접속하고(혹은 로컬 파일/문자열), Response 객체에서 세션ID같은 쿠키와 HTML Document를 얻어낸 후, Document의 Element들을 파싱하는 과정으로 나누어진다고 볼 수 있다.




1. URL 접속해 결과 얻어오기

URL에 접속해 Document를 얻어내기는 아주 쉽다.

// 간략화된 GET, POST
Document google1 = Jsoup.connect("http://www.google.com").get();
Document google2 = Jsoup.connect("http://www.google.com").post();

// Response로부터 Document 얻어오기
Connection.Response response = Jsoup.connect("http://www.google.com")
.method(Connection.Method.GET)
.execute();
Document google3 = response.parse();

http://www.google.com에 접속하는 방법들



얻어낸 Document는 두가지 방법으로 출력할 수 있다. .html() 메소드와 .text() 메소드 두 가지다.

Connection.Response response = Jsoup.connect("http://www.google.com")
.method(Connection.Method.GET)
.execute();
Document document = response.parse();

String html = document.html();
String text = document.text();

html과 text는 JQuery의 메소드와 유사하다. 문서의 html 그 자체를 가져올지, html 태그 사이의 문자열만을 가져올지를 택하는 것이다.



document.html()의 결과

<!doctype html>

<html itemscope itemtype="http://schema.org/WebPage" lang="ko">

 <head>

  <meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png" itemprop="image">

  <link href="/images/branding/product/ico/googleg_lodp.ico" rel="shortcut icon">

  <meta content="origin" id="mref" name="referrer">

  <title>Google</title> 

  <script>(function(){window.google={kEI:'P7QKWeJtioTzBfTmg6AB',kEXPI:'1352553,3700294,3700347,4029815,4031109,4032677,4036527,4038214,4038394,4039268,4041776,4043492,4045096,4045293,4045841,4046904,4047140,4047454,4048347,4048980,4050750,4051887,4056126,4056682,4058016,4061666,4061980,4062724,4064468,4064796,4065786,4069829,4071757,4071842,4072270,4072364,4072774,4076095,4076999,4078430,4078588,4078763,4080760,4081038,4081165,4082131,4082230,4083046,4090550,4090553,4090806,4091353,4092934,4093313,4093498,4093524,4094251,4094544,4094837,4095910,4095999,4096323,4097150,4097922,4097929,4098096,4098458,4098721,4098728,4098752,4100169,4100174,4100376,4100679,4100714,4100828,4101376,4101429,4101750,4102028,4102032,4102107,4102238,4103215,4103254,4103475,4103845,4103849,4103999,4104202,4104204,4104527,4105085,4105099,4105178,4105317,4105470,4105788,4106085,4106209,4106949,4107094,4107221,4107395,4107422,4107525,4107555,4107628,4107634,4107807,4107895,4107900,4107957,4107966,4107968,4108012,4108016,4108027,4108033,4108417,4108479,4108537,4108539,4108553,4108687,4108885,4109075,4109293,4109316,4109489,4109498,4110094,8300508,8503585,8508113,8508229,8508931,8509037,8509373,8509826,10200083,10200096,19001874,19002112,19002127,41027342',authuser:0,j:{en:1,bv:24,pm:'p',u:'c9c918f0',qbp:0},kscs:'c9c918f0_24'};google.kHL='ko';})();(function(){google.lc=[];google.li=0;google.getEI=function(a){for(var b;a&&(!a.getAttribute||!(b=a.getAttribute("eid")));)a=a.parentNode;return b||google.kEI};google.getLEI=function(a){for(var b=null;a&&(!a.getAttribute||!(b=a.getAttribute("leid")));)a=a.parentNode;return b};google.https=function(){return"https:"==window.location.protocol};google.ml=function(){return null};google.wl=function(a,b){try{google.ml(Error(a),!1,b)}catch(c){}};google.time=function(){return(new Date).getTime()};google.log=function(a,b,c,d,g){a=google.logUrl(a,b,c,d,g);if(""!=a){b=new Image;var e=google.lc,f=google.li;e[f]=b;b.onerror=b.onload=b.onabort=function(){delete e[f]};window.google&&window.google.vel&&window.google.vel.lu&&window.google.vel.lu(a);b.src=a;google.li=f+1}};google.logUrl=function(a,b,c,d,g){var e="",f=google.ls||"";c||-1!=b.search("&ei=")||(e="&ei="+google.getEI(d),-1==b.search("&lei=")&&(d=google.getLEI(d))&&(e+="&lei="+d));a=c||"/"+(g||"gen_204")+"?atyp=i&ct="+a+"&cad="+b+e+f+"&zx="+google.time();/^http:/i.test(a)&&google.https()&&(google.ml(Error("a"),!1,{src:a,glmm:1}),a="");return a};google.y={};google.x=function(a,b){google.y[a.id]=[a,b];return!1};google.lq=[];google.load=function(a,b,c){google.lq.push([[a],b,c])};google.loadAll=function(a,b){google.lq.push([a,b])};}).call(this);

google.j.b=(!!location.hash&&!!location.hash.match('[#&]((q|fp)=|tbs=rimg|tbs=simg|tbs=sbi)'))

||(google.j.qbp==1);(function(){google.hs={h:true,pa:true,q:false};})();(function(){goo


(이하 생략)



document.text()의 결과

Google 스크린 리더 사용자는 여기를 클릭하여 Google 순간 검색을 설정 해제하시기 바랍니다. Gmail 이미지 로그인 Google 순간 검색을 사용할 수 없습니다. 검색어를 입력한 후 Enter를 누르세요. 자세히 알아보기 Google연결 속도 문제로 순간 검색이 중지되었습니다. 검색하려면 Enter를 누르세요. 검색하려면 Enter를 누르세요. 부적절한 예상 검색어 신고 × 한국 'Ok Google'이라고 말하면 음성 검색이 시작됩니다. 손가락 하나 움직이지 않고 검색해 보세요. 'Ok Google' 다음에 말한 내용을 Chrome에서 검색합니다. 자세히 알아보기아니요'Ok Google' 사용 개인정보처리방침 약관 설정 검색 설정 고급검색 기록 검색 도움말 의견 보내기 Google.com 사용 광고 비즈니스 Google 정보 내 계정 검색 지도 YouTube Play 뉴스 Gmail 드라이브 캘린더 Google+ 번역 사진더보기 문서 도서 Blogger 주소록 행아웃 KeepGoogle 제품 모두 보기



이 두가지 메소드는 Document뿐 아니라 Element에도 구현되어 있다. 



2. 얻어온 결과에서 특정 값 뽑아내기

특정 값, 그러니까 특정한 html 요소를 얻으려면 select("css query") 메소드를 사용하면 된다. 

구글 메인 페이지 검색 버튼의 value를 얻어 보자.


검색 버튼의 name은 btnK다.


Connection.Response response = Jsoup.connect("http://www.google.com")
.method(Connection.Method.GET)
.execute();
Document googleDocument = response.parse();
Element btnK = googleDocument.select("input[name=btnK]").first();
String btnKValue = btnK.attr("value");

System.out.println(btnKValue); // Google 검색

select의 결과는 Elements다. 그 중 첫번째 Element를 first() 메소드로 선택했다.





※ 목표가 있는 예제

불법만화로 유명한 그 사이트(머루)의 뷰어를 만든다고 상상해보자.

얻어내야 할 값은 크게 두 가지다.


1. 만화의 목록

2. 만화의 이미지 파일


이 값들을 얻어내기 위해서는 

1) 만화 목록을 얻어낸다. 

2) 글 내용에서 실제 만화 이미지가 있는 링크를 얻어낸다.

3) 이미지가 있는 링크에 접속한 후 이미지를 뽑아낸다. 

이렇게 세 가지 과정으로 진행해 보자.



1) 만화 목록 얻어내기

앞서 살펴본 Jsoup의 Conenction 메소드를 이용해 '업데이트' 페이지에 접속해 Doucment를 얻어낸다.

Document rawData = Jsoup.connect(URL)
.timeout(5000)
.get();

이 불법적이고 무서운 사이트는 Request Header를 검사하지 않는다. 그래서 위 코드처럼 아무런 추가적인 정보 없이 간단하게 결과를 얻어 올 수 있다. 하지만 Request를 철저하게 검사하는 사이트에는 더 많은 정보가 필요하다. 그런 사이트는 다음 글에 적을 예정이다.



아무튼, 이제 얻어낸 Document에서 정보를 뽑아낼 차례다. 구글 크롬의 개발자 도구를 이용해 업데이트 페이지를 확인해 보자.


'업데이트' 페이지 HTML


게시판은 table 태그를 사용하고, 각 행은 tr 태그에 매칭되며 공지사항은 tr_notice 클래스를 가지고 있다는 사실을 알 수 있다. 




tr 태그의 내부


tr 태그에 포함된 요소들을 살펴보자. 

a 태그로 글 내용에 해당하는 url을 얻을 수 있고, a태그의 첫번째 div에서 제목을 얻을 수 있다. 마지막으로 small 태그를 통해 글이 작성된 날짜를 얻을 수 있다. 


이렇게 얻어낸 사실들을 직접 코드로 구현하자.

Elements articles = rawData.select("tr:not(.tr_notice) a"); // 공지사항을 제외한 tr의 a 태그들을 얻어온다.

for(Element article : articles) {

String href = article.attr("abs:href"); // a태그 href의 절대주소를 얻어낸다.

// a 태그 안에 포함된 div들
Elements articleDiv = article.select("div");

String thumbUrl = ROOT_URL
+ articleDiv.first() // 첫 번째 div에서 썸네일 url을 얻어온다.
.attr("style")
.replace("background-image:url(", "")
.replace(")", "");

String title = articleDiv.get(1).ownText(); // 두 번째 div에서 제목을 얻어낸다.

String date = articleDiv.get(1).select("small").text()
.split("\\|")[0];

System.out.println(href); // http://ma../b/mangup/00000
System.out.println(thumbUrl); // http://ma../quickimage/...
System.out.println(title); // 제목
System.out.println(date); // 날짜
}

얻어내고자 한 요소들을 css 선택지로 얻어낸 후, split이나 replace등의 메소드를 이용해 정리한다.


이 '글 목록'에 해당하는 정보는 필요에 맞게 정의한 객체에 담아 보관하거나 유저에게 보여 줄 수 있다.




2) 글 내용에서 만화가 있는 링크 얻어내기

위에서 얻어낸 글 내용 url에 접속한 후, 실제 이미지가 있는 페이지에 접근할 차례다. 태그 분석을 위해 브라우저로 페이지에 직접 들어가 보자.


글 내용 HTML


글의 내용에 해당하는 div(#vContent)의 첫 번째 a 태그의 href 속성이 실제 만화 이미지가 포함된 URL이다.



Document rawData = Jsoup.connect(ARTICLE_URL)
.timeout(5000)
.get();

Elements contentATags = rawData.select("#vContent a"); // 공지사항을 제외한 tr의 a 태그들을 얻어온다.

String viewPageUrl = contentATags.first()
.attr("abs:href"); // 마찬가지로 절대주소 href를 얻어낸다

System.out.println(viewPageUrl); // http://wasabi.../archives/XXXXX...

아주 간단하게 이미지들이 포함된 주소를 얻어낼 수 있다. 




3) 만화 이미지가 있는 URL에 접속해 이미지 URL 얻어내기

마찬가지로 만화 이미지가 포함된 URL에 접속해 태그를 분석한다.


만화 이미지가 있는 페이지의 HTML


html 코드를 보면 이미지들이 가진 특정 클래스가 있다. 이 클래스를 가진 img 태그들을 얻어낸 후, data-src 속성을 뽑아내자.


  Document rawData = Jsoup.connect(VIEWER_URL)
.timeout(5000)
.get();

Elements imgs = rawData.select("img[class=lz-lazyload]"); // lz-lazyload 클래스를 가진 img들

List<String> imageUrls = new ArrayList<>();

for(Element img : imgs) {
imageUrls.add(img.attr("abs:data-src"));
}

System.out.println(imageUrls); // 이미지 URL들.
}


만화 내용이 되는 모든 이미지 URL을 뽑아넀다. 이 URL에 접속해 직접 파일로 다운로드 할 수도, 자기 나름의 뷰어에 출력 할 수도 있다. 또, 목록을 얻어 낼 때 필요한 data들을 포함함으로써 원하는 페이지나 검색까지 구현이 가능하다.



[    AJAX 통신시 컨트롤러에서 한글 문자열을 리턴해야할 경우 인코딩 문제    ]



자바스크립트에서 비동기로 ajax로 컨트롤러로 요청을 받아 db에서 값을 꺼내 문자열을 리턴할 경우, 한글 문자열을 리턴했을 때

ajax의 success:function(result) 안에서 result로 값을 받으면 ???? 로 한글이 깨지는 경우가 있다.

이럴 경우에는 컨트롤러에서 produces 부분을 지정해주면 된다.


코드를 보면


먼저, ajax 통신 부분이다. /board/category/getAddr로 요청을 보내고 




컨트롤러 부분이다.
컨트롤러에서 service.getAddr(userId)로 해당 사용자의 주소를 얻어온 뒤 그 주소를 반환하지만 한글로 반환하게 된다.
이 경우, ajax의 success:function(result) 에서 result 가 ????로 깨지는 것을 알 수 있다.

하지만, produces = "application/text;charset=utf8"을 지정해 줄 경우 한글을 인코딩해 보내서 깨지지 않고 처리할 수 있게 된다.





pf) 개인적으로 공부한 내용을 정리했습니다. 여러 블로그도 참고하였지만 주로 참고한 것은 "코드로 배우는 스프링 웹 프로젝트"(남가람북스) 책을 공부한 후 참고해 작성하였음을 미리 말씀드립니다.

[    1. 인터셉터란?    ]


특정 URI로 요청시 Controller로 가는 요청을 가로채는 역할을 한다.


[    2. Interceptor와 JSP Filter의 차이??    ]


- 공통점 : 둘 다 Controller로 들어가는 요청을 가로채 특정 작업을 하기 위한 용도로 사용된다.

- 차이점 : 케어할 수 있는 영역(범위)가 다르다. Filter는 같은 웹 어플리케이션 내에서만 접근이 가능하며,

  Interceptor의 경우 스프링에서 관리되기 때문에 스프링내의 모든 객체에 접근이 가능하다.

-> JSP Filter의 경우 주로 한글처리에 이용되고

-> Interceptor의 경우 "로그인 처리"에 이용이 된다.

- why 로그인 처리에 이용?? )

: 만약 인터셉터를 이용하지 않고, 로그인 처리를 한다면, 게시물을 작성("/board/register"), 게시물 수정("/board/modify"),

 게시물 삭제("/board/delete") 등 모든 요청마다 Controller에서 session을 통해 로그인 정보가 남아 있는지를 확인하는 코드

 를 중복해서 입력해야 할 것이다. 

 하지만!, 인터셉터를 이용하면, A, B, C 작업(A,B,C 경로로 요청)을 할 경우에는 ~~Interceptor를 먼저 수행해 session에서 

 로그인 정보가 있는지 확인해 주는 역할을 한다면, 중복 코드가 확 줄어들 수 있을 것이다. 이러한 장점 때문에 사용!


[    3. 인터셉터를 지원하는 인터페이스와 클래스, 메서드    ]


: Spring에서 인터셉터를 지원하기 위해서 

- HandlerInterceptor 인터페이스

- HandlerInterceptorAdapter 추상 클래스를 지원한다. => 요녀석은 위의 인터페이스를 사용하기 쉽게 구현해 놓은 추상클래스.


=> 이때, HandlerInterceptorAdaptor는 3가지 메서드를 제공한다. 이 3가지 메서드를 오버라이딩해서 우리가 이용할 수 있다.

1) public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

: Controller로 요청이 들어가기 전!!에 수행된다.

 : request, response, handler 등의 매개변수를 이용가능한데 우리가 아는 HttpServletRequest, HttpServletResponse,

  이고, 나머지 하나는 이 preHandle() 메서드를 수행하고 수행될 컨트롤러 메서드에 대한 정보를 담고 있는 handle

  이다.

2) postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,

ModelAndView modelAndView)

: 컨트롤러의 메서드의 처리가 끝나 return 되고 화면을 띄워주는 처리가 되기 직전에 이 메서드가 수행된다.

: ModelAndView 객체에 컨트롤러에서 전달해 온 Model 객체가 전달됨으로 컨트롤러에서 작업 후 

: postHandle() 에서 작업할 것이 있다면 ModelAndView를 이용하면 된다.

3) afterCompletion()

: 컨트롤러가 수행되고 화면처리까지 끝난 뒤 호출된다.



[    4. 인터셉터를 이용한 로그인 구현해 보기    ]



1) 로그인 처리를 위한 DB table을 만들자

: 여기서는 mysql을 이용해서 아래와 같이 테이블을 생성하였다.

: 이와 같이 id, pw, name, point 를 갖는 테이블을 만들고, 4개 정도 회원 정보를 insert 해 놓았다.


2) 로그인에 사용될 UserVO 클래스를 정의하자.

: DAO(Data Access Object)와 Controller의 ModelAttribute 부분에 사용하기 위해 UserVO를 id와 pwd, name, point 정보 등을

 넣어 아래와 같이 만들어 보자.


3) 로그인처리를 할 MyBatis userMapper.xml 을 작성하자.

     : 살펴보면, namespace명은 고유한 값을 지정하고 select 문에 호출시 전달받은 userId와 userPw를 통해

 DB에서 select한 뒤 해당 튜플 정보를 resultType = UserVO 로 반환한다.

 이때, #{userId} 부분은 전달 받은 객체에서 getter메서드를 통해 호출됨으로 UserVO의 getUserId() 를 호출해 자동 대입이 될

 것임을 알 수 있다.


4) UserDAO 인터페이스와 이를 구현한 UserDAOImpl 클래스를 작성하자.

먼저) UserDAO

UserDAOImpl 

UserDAOImpl은 UserDAO인터페이스를 구현하였고 DAO(Data Access Object)로 DB에 직접 접근하는 처리를 담당하는 

클래스이다. 이 클래스에서는 스프링 환경설정에서 만들어 놓은 mybatis 접근 객체 : 즉, SqlSessionTemplate 객체를 

이용해야함으로 sqlSession.selectOne("MyBatis Mapper의 namespace명.지정한id",넘겨줄 데이터 객체); 를 이용해서 

MyBatis를 통해 작성한 쿼리를 실행해 UserVO 객체를 얻어오는 작업을 해준다. 이때, @Inject나 @Autowired를 통해서

자동 주입을 한다. @Inject와 @Autowired는 ByType(타입이 같은 것을 자동 대입)하는 성질이며, 환경설정 파일에서 우리가

sqlSessionTemplate 빈을 생성해 놓았음으로 자동 대입이 가능하다.(이후, 이런 설명은 생략한다.)

마찬가지로 서비스 객체에서 DAO를 자동주입할 것임으로 UserDAOImpl 클래스 위에 @Repository 어노테이션을 붙였다.

이는, 이 클래스는 DAO 클래스에요! 라는 의미 + 자동으로 new해서 이 객체를 생성해 주세요라는 의미를 가지고 있다.

이후, Service 객체에서도 @Inject를 이용해 자동 주입하기 위해 생성해 놓는다. 단, @Component, @Repoisitory, 

@Service(서비스 자동생성) 등의 어노테이션을 인식하기 위해서는 환경설정 파일에서 <context:component-scan 부분을

잘 지정해야한다. 여기서 지정한 패키지 내에서 해당 어노테이션이 탐색되기 때문이다.


여하튼...

하나의 UserVO 객체가 반환되야 함으로 selectOne() 메서드를 사용한 것이고, 여러 객체를 담고 있는 List로 반환을 받을

경우 selectList() 메서드를 호출한다. 이런 내용은 기본 적인 내용이니까... 이해가 안되면 MyBatis 연동 부분을 살펴보길 바란다.



5) UserService 인터페이스와 UserServiceImpl 클래스를 정의한다.

별거 없다... 단순한 서비스 클래스이다...

 dao와 마찬가지로 @Service 어노테이션을 사용해 이 클래스는 서비스 클래스로 사용될 녀석이에요 !! + 자동 객체 생성을 해준다.

 컨트롤러에서 사용되어야 하니까... 역시 이 서비스 클래스가 있는 패키지 경로도 <context:component-scan 이 잘 지정되어 있어야하

 는 것은 당연하다...


자... 여기까지가 기본 세팅이다... 수고했다... 이제 본격적으로 로그인과 관련된 기능을 만들어 보자.

---------------------------------------------------------------------------------------------------------------------------------

6) UserController에서 로그인 처리를 작성해 보자.

: 먼저 로그인 요청시 로그인 폼 화면을 띄워주는 부분을 작성해야 한다.

  loginForm() 부분이 바로 그 부분! RequestMapping된 메서드의 return한 부분은 환경설정 파일의 ViewResolver에 맵핑

  됨으로 loginForm.jsp 파일을 view아래에 적당한 위치에 만든다음에 return을 잘해서 http://localhost:8080/login으로 요청시

  해당 웹페이지가 뜨도록 설정한다. 주소에 url 요청시 GET 방식임으로 GET으로 설정한다.


 그 다음은, 로그인을 처리하는 부분이다. 로그인을 처리할 때 해야할 작업은 2가지이다.

1) 새로 로그인 요청이 온다면, 기존에 세션에 저장되어 있던 이전 로그인 사용자의 정보를 제거해 주어야 한다.

2) 새로 로그인 하는 사용자의 정보가 일치한다면, 해당 사용자 정보를 DB에서 가져와 세션에 저장해 놓아야 한다.

 

service.login()을 수행->dao.login()->UserMapper.xml 순으로 수행되며 id와 pw가 일치하면 사용자 정보를 담은 UserVO를 반환

할 것이고, 틀렸다면 null이 반환될 것이다. 

null 이 아닐 경우, 세션을 통해 사용자 객체를 저장해 놓는다.


이때, 마지막으로 작업해 주어야 하는 부분이 어디로 이동하느냐에 대한 정보이다.

로그인에 실패할 경우 return "redirect:/login"; 을 통해 다시 로그인 폼으로 이동시켜 주어야 할 것이고,

로그인에 성공해 session에 사용자 객체를 저장한 이후에는 return "redirect:/board/listPage" 와 같이 최초 페이지로 이동시켜주어

야 할 것이다.


로그아웃 부분은 그냥 로그아웃 url 요청이 있으면 해당 브라우저의 세션을 통째로 날려버리면 된다.

물론, session.removeAttribute('변수명") 해서 하나를 날려도 되지만, 만약 날려야하는 정보가 여러개라면 일일이 하기 힘들 수 있

기 때문에 invalidate() 를 많이 사용한다.


===> 여기까지 작업을 완료했다면 로그인, 로그아웃 기능은 다 만든 것이다. 하지만, 가장 중요하게 처리해 주어야 할 부분이

남아 있다.!!


** 게시물을 등록, 수정, 삭제 등을 할 때는 반드시 로그인 된 사용자만 할 수 있도록 "인터셉터"를 적용하는 것 말이다.!! ***


7) AuthenticationInterceptor 를 만들어 게시물 등록, 수정, 삭제 요청 전에 로그인 여부를 확인하는 인터셉터 클래스를 작성하자.

먼저 Interceptor를 구현하기 위해서는 가장 쉽게 사용할 수 있는 것이 HandlerInterceptor 인터페이스를 구현해 놓은 HandlerInterceptorAdapter이다.


이를 extends해서 preHandle()과 postHandle() 을 오버라이딩한다. shift + alt + s + v 를 하면... 알죠...?

보통, 세션에 로그인 사용자 정보를 컨트롤러에서 저장하지 않고 여기서 저장할 것이었으면 postHandle() 부분에서 수행해 주어야 겠

지만, 우리는 간편하게.....(그냥 귀찮았어...ㅠㅠ) 컨트롤러에서 다 해버렸음으로 인터셉터 클래스에서는 preHandle() 메서드만 필요하다.


그럼, preHandle() 에서는 request에서 세션 객체를 가지고 오고 세션에서 login변수에 사용자 정보 객체가 담겨 있나 확인을 한다.

없으면?? response.sendRedirect("uri") 를 통해 uri경로로 날려버리면 된다. 사용자 정보가 세션에 없다는건 지금 로그인이 되어 있지

않다는 것을 의미함으로 response.sendRedirect("/login") 을 통해서 다시 로그인하는 폼으로 이동시켜버리면 된다.

단, 이때 이 메서드를 수행 후 return false를 한 이유는, 이 preHandle() 메서드의 return 이 의미하는 바는 true일 경우 preHandle() 

메서드를 수행한 뒤에 본래 요청한 Controller 를 수행한다는 의미이고 false를 주면 수행하지 않는다는 의미를 담고 있다.

따라서, 로그인 안된 상태에서 요청시 해당 컨트롤러로 요청이 가지 않도록 false를 해주자.


반대로 사용자 정보가 세션에 담겨 있는 경우에는 단순히 return true를 해줘서 본래 사용자가 요청했던 Controller의 RequestMapping부분이 수행될 수 있도록 해주면 된다.


8) 가장 중요한... servlet-context.xml에 인터셉터 설정 정보를 등록해야한다..


: 일단, 인터셉터 자체는 웹 관련 설정임으로 root-context.xml이 아닌 servlet-context.xml에 작성하자. 물론, 요소마다 설정파일을

  다 나누어서 설정하셨다면... 본인에 맞는 환경설정 파일에서 작업을 해주세요...


이때, 인터셉터 빈 객체를 생성해 놓고, 인터셉터 정보를 등록할 때 해당 객체와 어떤 url이 요청시에 인터셉터가 작동할지를

설정해 놓아야 한다.


여기서는 필요한 uri를 하나하나 지정했는대 이렇게 하지 않고, 전체 경로 /** 로 잡아 놓고

<exclue-mapping path="예외url" /> 을 지정해서 모든 페이지에 인터셉터를 적용하지만 예외는 ~다 라고 지정할 수도 있다.



9) 죄송합니다... 기존거에서 빠트린 부분이 있어 이부분 추가합니다. -> "자기 글만 수정, 삭제가 가능하도록"


여러분... 제가 이 부분을 빠트렸었내요 ㅠㅠ... 지금은 로그인만하면, 모든 게시글들을 다 삭제하고 수정할 수 있죠...

그러면 큰일나자나요... ㅠㅠ

자기가 작성한 글만 수정, 삭제를 할 수 있도록 구성해야합니다. 어떻게하면 좋을까요... 여러 방법이 있겠지만 전 2가지

정도 생각이 나내요... 인터셉터의 postHandler() 메서드를 통해서 요청 uri를 분석한다음 수정일 경우 postHandler()에서 전달

받은 ModelAndView로 사용자 정보를 전달받아서 하는 방법이 있겠고... 근대 이건 좀 귀찮고...


사실... 훨씬 간단한 방법이 있어요... 뷰 페이지에서 처리하면 말이죠...


머냐....


일단, 수정버튼과 삭제 버튼은 해당 글을 클릭했을 때만 뜬답니다. 그렇죠? 그러면 글을 읽는 /views/board/read.jsp 페이지에서

<c:if test="${login.userId eq boardVO.writer}">


</c:if>

인 경우에만 수정하고 삭제 버튼이 보이도록하면 어떨까요? 세션에 login 변수로 userVO 객체를 담고 있으니까

현재 로그인한 사용자 userId와 현재 게시글의 boardVO 에서 writer를 비교해서 eq(같은경우)만 수정과 삭제 버튼이 보이고

목록으로가는 버튼은 항상 보이도록 하는거죠.

그러면 별다른 처리없이 안보이면 사용자는 수정과 삭제버튼은 클릭조차 할 수 없기에 모든 처리가 끝나게 되죠..

아래처럼 /views/board/read.jsp 페이지를 수정합시다.

아직 댓글에 대한 처리를 안했지만, 댓글도 마찬가지로 이와 같은 방식으로 해주면 되겠죠? 


10) 결과를 확인해 보자!!!

메인페이지)



로그인이 되어 있지 않은 상태에서 글등록을 누르면, 로그인 폼 페이지로 이동된다.


로그인에 성공하면, 게시글 첫 페이지로 이동된다.



이제 로그인이 된 상태임으로 "글등록"을 누르면 정상적으로 글 등록 폼으로 이동된다.


그리고 마지막으로, 로그인은 한 상태지만 다른 사람의 글을 클릭했을 때 수정과 삭제가 보이지 않고 목록으로 버튼만 보이는 화면입니다.



이상입니다.


열심히 끝까지 읽어주셔서 감사합니다.



+ Recent posts