5월 ~ 7월) 웹/jsp

61Day -[홈페이지 만들기 4일차] review_detail.jsp / review_write.jsp/ review_write_action.jsp / ReviewDAO. // Utllity.java / review_write.jsp

첼로그 2023. 7. 3. 02:42

6월 30일 

- 리뷰쓰기 저장

- 비밀글

- 리뷰게시판에 이미지넣기  >> / 답글삽입쓰기

- XSS 공격 방어(마음대로 글씨색바꾸거나, 글씨크기 바꾸기방어) 

(아직 후기내용 구현안함)


*** 페이징처리 가장어려움/ 답글저장하는거 어려움!!!
에러떨어지면 왜그런지 알아야함!

 

+ webapp > WEB-INF > lib) 있어야함

cos.jar

cos.jar
0.05MB



webapp > review)
review_detail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%-- 글번호를 전달받아 REVIEW 테이블에 저장된 게시글을 검색하여 클라이언트에게 전달하여 응답하는 JSP 문서 --%>
<%-- => 전달된 페이지번호, 검색대상, 검색단어는 반환받아 [review_list.jsp] 문서를 요청할 때 전달 --%>


review_write.jsp 변수

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%-- 사용자로부터 게시글(새글 또는 답글) 정보를 입력받기 위한 JSP 문서 --%>
<%-- => 로그인 상태의 사용자만 요청 가능한 JSP 문서 --%>
<%-- => [글저장] 태그를 클릭한 경우 [review/review_write_action.jsp] 문서 - 입력값(게시글정보) 전달 --%>

<%-- 새글 : [review_list.jsp] 문서에 의해 [review_writer.jsp] 문서를 요청한 경우 - 전달값 : X --%>    
<%-- 답글 : [review_detail.jsp] 문서에 의해 [review_writer.jsp] 문서를 요청한 경우 - 전달값 : O --%>    
<%-- => [review_detail.jsp] 문서에서 부모 게시글 관련 정보(ref, restep, relevel, pageNum) 전달--%>

<%-- 비로그인 상태의 사용자가 JSP 문서를 요청한 경우 에러페이지로 이동되도록 응답 처리 --%>
<%@include file="/security/login_check.jspf" %>
<%
	//전달값을 반환받아 저장 - 전달값이 없는 경우(새글) 변수에 초기값 저장
	String ref="0", restep="0", relevel="0", pageNum="1";
	if(request.getParameter("ref")!=null) {//전달값이 있는 경우 - 답글
		ref=request.getParameter("ref");
		restep=request.getParameter("restep");
		relevel=request.getParameter("relevel");
		pageNum=request.getParameter("pageNum");
	}
%>
<style type="text/css">
table {
	margin: 0 auto;
}

th {
	width: 100px;
	font-weight: bold;
}

td {
	text-align: left;
}
</style>
<% if(ref.equals("0")) {//새글인 경우 %>
	<h1>새글쓰기</h1>
<% } else {//답글인 경우 %>
	<h1>답글쓰기</h1>
<% } %>
<%-- 파일을 입력받아 전달하기 위해 반드시 enctype 속성값을 [multipart/form-data]로 설정 --%>
<form action="<%=request.getContextPath() %>/review/review_write_action.jsp" 
	method="post" enctype="multipart/form-data" id="reviewForm">
	<input type="hidden" name="ref" value="<%=ref%>">
	<input type="hidden" name="restep" value="<%=restep%>">
	<input type="hidden" name="relevel" value="<%=relevel%>">
	<input type="hidden" name="pageNum" value="<%=pageNum%>">
	<table>
		<tr>
			<th>제목</th>
			<td>
				<input type="text" name="subject" id="subject" size="40">
				<input type="checkbox" name="secret" value="2">비밀글
			</td>
		</tr>
		<tr>
			<th>내용</th>
			<td>
				<textarea rows="7" cols="60" name="content" id="rContent"></textarea>
			</td>
		</tr>
		<tr>
			<th>리뷰이미지</th>
			<td>
				<input type="file" name="reviewimg">
			</td>
		</tr>
		<tr>
			<th colspan="2">
				<button type="submit">글저장</button>
				<button type="reset" id="resetBtn">다시쓰기</button>
			</th>
		</tr>
	</table>
</form>
<div id="message" style="color: red;"></div>

<script type="text/javascript">
$("#subject").focus();

$("#reviewForm").submit(function() {
	if($("#subject").val()=="") {
		$("#message").text("제목을 입력해 주세요.");
		$("#subject").focus();
		return false;
	}
	
	if($("#rContent").val()=="") {
		$("#message").text("내용을 입력해 주세요.");
		$("#rContent").focus();
		return false;
	}
});

$("#resetBtn").click(function() {
	$("#subject").focus();
	$("#message").text("");
});
</script>


review_write_action.jsp

<%@page import="xyz.itwill.util.Utility"%>
<%@page import="xyz.itwill.dto.ReviewDTO"%>
<%@page import="xyz.itwill.dao.ReviewDAO"%>
<%@page import="com.oreilly.servlet.multipart.DefaultFileRenamePolicy"%>
<%@page import="com.oreilly.servlet.MultipartRequest"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%-- 게시글(새글 또는 답글)을 전달받아 REVIEW 테이블에 삽입하고 [review/review_list.jsp] 문서로
이동하기 위한 URL 주소를 클라이언트에게 전달하여 응답하는 JSP 문서 --%>
<%-- => 로그인 상태의 사용자만 요청 가능한 JSP 문서 --%>
<%-- => [multipart/form-data] 형태로 값이 전달되므로 COS 라이브러리의 MultipartRequest 클래스를 사용하여 처리 --%>
<%-- => 전달받은 파일은 [/review_images] 서버 디렉토리에 업로드 처리하여 저장 --%>
<%@include file="/security/login_check.jspf" %>
<%
	if(request.getMethod().equals("GET")) {
		response.sendRedirect(request.getContextPath()+"/index.jsp?group=error&worker=error_400");
		return;
	}

	//전달파일을 저장할 서버 디렉토리(웹자원)의 파일 시스템 경로를 반환받아 저장
	//String saveDirectory=application.getRealPath("/review_images");
	String saveDirectory=request.getServletContext().getRealPath("/review_images");
	//System.out.println("saveDirectory = "+saveDirectory);

	//MultipartRequest 객체 생성 - 모든 전달파일을 서버 디렉토리에 업로드 처리하여 저장
	// => cos.jar 라이브러리 파일을 프로젝트에 반드시 빌드 처리
	MultipartRequest multipartRequest=new MultipartRequest(request, saveDirectory
			, 20*1024*1024, "utf-8", new DefaultFileRenamePolicy());

	//전달값을 반환받아 저장
	int ref=Integer.parseInt(multipartRequest.getParameter("ref"));
	int restep=Integer.parseInt(multipartRequest.getParameter("restep"));
	int relevel=Integer.parseInt(multipartRequest.getParameter("relevel"));
	String pageNum=multipartRequest.getParameter("pageNum");

	//사용자로부터 입력받아 전달된 값에 태그 관련 문자값이 존재할 경우 웹프로그램 실행시 문제 발생
	// => XSS(Cross Site Scripting) 공격 : 사용자가 악의적인 스크립트를 입력하여 페이지가 깨지거나
	//다른 사용자의 사용을 방해하거나 쿠키 및 기타 개인 정보를 특정 사이트로 전송하는 공격
	//String subject=multipartRequest.getParameter("subject");
	
	//XSS 공격을 방어하기 위해 전달값을 변환하여 필드값으로 저장
	//String subject=Utility.stripTag(multipartRequest.getParameter("subject"));//사용자가 입력한 태그 관련 문자열을 제거하여 저장
	String subject=Utility.escapeTag(multipartRequest.getParameter("subject"));//사용자가 입력한 태그를 문자열로 처리하여 저장
	int status=1;//전달값이 없는 경우 - 일반글
	if(multipartRequest.getParameter("secret")!=null) {//전달값이 있는 경우 - 비밀글
		status=Integer.parseInt(multipartRequest.getParameter("secret"));
	}
	String content=Utility.escapeTag(multipartRequest.getParameter("content"));
	//업로드 처리된 파일명을 반환받아 저장
	String reviewimg=multipartRequest.getFilesystemName("reviewimg");
	
	//REVIEW_SEQ 시퀸스의 다음값을 검색하여 반환하는 DAO 클래스의 메소드 호출
	// => 게시글의 글번호(NUM 컬럼값)로 저장
	// => 새글인 경우에는 게시글의 글그룹(REF 컬럼값)의 값으로 저장
	int num=ReviewDAO.getDAO().selectNextNum();
	
	//게시글을 작성한 클라이언트의 IP 주소를 반환받아 저장
	//request.getRemoteAddr() : JSP 문서를 요청한 클라이언트의 IP 주소를 반환하는 메소드
	// => 이클립스에 등록되어 동작되는 WAS 프로그램은 기본적으로 128Bit 형식(IPV6)의 IP 주소 제공
	//32Bit 형식(IPV4)의 IP 주소를 제공받을 수 있도록 이클립스의 Apache Tomcat 실행 환경 변경
	// => Run >> Run Configurations... >> Apache Tomcat >> 사용중인 Apache Tomcat 선택
	//    >> Arguments >> VM Arguments >> [-Djava.net.preferIPv4Stack=true] 추가 >> Apply
	String ip=request.getRemoteAddr();
	//System.out.println("ip = "+ip);
	
	//게시글을 새글과 답글로 구분하여 REVIEW 테이블의 컬럼값으로 저장될 변수값 변경
	// => [review_write.jsp] 문서에서 hidden 타입으로 전달된 값이 저장된 ref, restep, relevel
	//변수값 변경 - 새글 : 초기값(0), 답글 : 부모글로부터 전달된 값
	if(ref==0) {//새글인 경우
		//REVIEW 테이블의 REF 컬럼값에는 시퀸스의 다음값(num 변수값)을 저장하고 RESTEP 컬럼과
		//RELEVEL 컬럼에는 restep 변수값(0)과 relevel 변수값(0)을 저장
		ref=num;		
	} else {//답글인 경우
		//REVIEW 테이블에 저장된 기존 게시글에서 REF 컬럼값이 ref 변수값(부모글)과 같은 게시글 
		//중 RESTEP 컬럼값이 restep 변수값(부모글)보다 큰 모든 게시글의 RESTEP 컬럼값을 1 증가
		//되도록 변경 처리
		// => 새로운 답글이 기존 답글보다 먼저 검색되도록 기존 답글의 순서를 증가
		// => REVIEW 테이블에 저장된 게시글의 RESTEP 컬럼값을 변경하는 DAO 클래스의 호출
		ReviewDAO.getDAO().updateRestep(ref, restep);
		
		//REVIEW 테이블의 REF 컬럼값에는 ref 변수값(부모글)을 저장하고 RESTEP 컬럼과 RELEVEL
		//컬럼에는 restep 변수값(부모글)과 relevel 변수값(부모글)을 1 증가하여 저장
		restep++;
		relevel++;
	}
	
	//DTO 객체를 생성하고 전달값(변수값)으로 필드값 변경
	ReviewDTO review=new ReviewDTO();
	review.setNum(num);
	review.setReviewid(loginMember.getId());
	review.setSubject(subject);
	review.setContent(content);
	review.setReviewimg(reviewimg);
	review.setRef(ref);
	review.setRestep(restep);
	review.setRelevel(relevel);
	review.setIp(ip);
	review.setStatus(status);
	
	//게시글을 전달받아 REVIEW 테이블에 삽입하는 DAO 클래스의 메소드 호출
	ReviewDAO.getDAO().insertReview(review);
	
	//페이지 이동
	response.sendRedirect(request.getContextPath()+"/index.jsp?group=review&worker=review_list&pageNum="+pageNum);
%>



main>java> xyz.itwill > dao)
ReviewDAO. (수정)

package xyz.itwill.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import xyz.itwill.dto.ReviewDTO;

public class ReviewDAO extends JdbcDAO {
	private static ReviewDAO _dao;
	
	private ReviewDAO() {
		// TODO Auto-generated constructor stub
	}
	
	static {
		_dao=new ReviewDAO();		
	}
	
	public static ReviewDAO getDAO() {
		return _dao;
	}
	
	//게시글 검색 관련 정보를 전달받아 REVIEW 테이블에 저장된 게시글 중 검색 처리된 전체  
	//게시글의 갯수를 검색하여 반환하는 메소드
	public int selectReviewCount(String search, String keyword) {
		Connection con=null;
		PreparedStatement pstmt=null;
		ResultSet rs=null;
		int count=0;
		try {
			con=getConnection();
			
			//매개변수에 저장된 값을 비교하여 DBMS 서버에 다른 SQL 명령을 전달하여 실행
			// => 동적 SQL(DynamicSQL) 기능 
			if(keyword.equals("")) {//게시글 검색 기능을 사용하지 않은 경우
				//REVIEW 테이블에 저장된 전체 게시글의 갯수 검색
				String sql="select count(*) from review";
				pstmt=con.prepareStatement(sql);
			} else {//게시글 검색 기능을 사용한 경우
				//검색대상(컬럼명)에 검색단어가 포함한 게시글의 갯수 검색 - 삭제글 제외
				String sql="select count(*) from review join member on reviewid=id where "
						+search+" like '%'||?||'%' and status <> 0";
				pstmt=con.prepareStatement(sql);
				pstmt.setString(1, keyword);
			}
			
			rs=pstmt.executeQuery();
			
			if(rs.next()) {
				count=rs.getInt(1);
			}
		} catch (SQLException e) {
			System.out.println("[에러]selectReviewCount() 메소드의 SQL 오류 = "+e.getMessage());
		} finally {
			close(con, pstmt, rs);
		}
		return count;
	}
	
	//페이징 처리 관련 정보와 게시글 검색 기능 관련 정보를 전달하여 REVIEW 테이블에 저장된 
	//게시글 목록을 검색하여 List 객체로 반환하는 메소드
	public List<ReviewDTO> selectReviewList(int startRow, int endRow, String search, String keyword) {
		Connection con=null;
		PreparedStatement pstmt=null;
		ResultSet rs=null;
		List<ReviewDTO> reviewList=new ArrayList<>();
		try {
			con=getConnection();
			
			if(keyword.equals("")) {//게시글 검색 기능을 사용하지 않은 경우
				String sql="select * from (select rownum rn, temp.* from (select num, reviewid"
					+ ", name, subject, content, reviewimg, regdate, readcount, ref, restep"
					+ ", relevel,ip, status from review join member on reviewid=id order by"
					+ " ref desc, restep) temp) where rn between ? and ?";
				pstmt=con.prepareStatement(sql);
				pstmt.setInt(1, startRow);
				pstmt.setInt(2, endRow);
			} else {//게시글 검색 기능을 사용한 경우
				String sql="select * from (select rownum rn, temp.* from (select num, reviewid"
					+ ", name, subject, content, reviewimg, regdate, readcount, ref, restep"
					+ ", relevel,ip, status from review join member on reviewid=id where "
					+ search + " like '%'||?||'%' and status <> 0 order by ref desc, restep)"
					+ " temp) where rn between ? and ?";
				pstmt=con.prepareStatement(sql);
				pstmt.setString(1, keyword);
				pstmt.setInt(2, startRow);
				pstmt.setInt(3, endRow);
			}
			
			rs=pstmt.executeQuery();
			
			while(rs.next()) {
				ReviewDTO review=new ReviewDTO();
				review.setNum(rs.getInt("num"));
				review.setReviewid(rs.getString("reviewid"));
				review.setName(rs.getString("name"));
				review.setSubject(rs.getString("subject"));
				review.setContent(rs.getString("content"));
				review.setReviewimg(rs.getString("reviewimg"));
				review.setRegdate(rs.getString("regdate"));
				review.setReadcount(rs.getInt("readcount"));
				review.setRef(rs.getInt("ref"));
				review.setRestep(rs.getInt("restep"));
				review.setRelevel(rs.getInt("relevel"));
				review.setIp(rs.getString("ip"));
				review.setStatus(rs.getInt("status"));
				reviewList.add(review);
			}
		} catch (SQLException e) {
			System.out.println("[에러]selectReviewList() 메소드의 SQL 오류 = "+e.getMessage());
		} finally {
			close(con, pstmt, rs);
		}
		return reviewList;
	}
	
	//REVIEW_SEQ 시퀸스의 다음값을 검색하여 반환하는 메소드
	public int selectNextNum() {
		Connection con=null;
		PreparedStatement pstmt=null;
		ResultSet rs=null;
		int nextNum=0;
		try {
			con=getConnection();
			
			String sql="select review_seq.nextval from dual";
			pstmt=con.prepareStatement(sql);
			
			rs=pstmt.executeQuery();
			
			if(rs.next()) {
				nextNum=rs.getInt(1);
			}
		} catch (SQLException e) {
			System.out.println("[에러]selectNextNum() 메소드의 SQL 오류 = "+e.getMessage());
		} finally {
			close(con, pstmt, rs);
		}
		return nextNum;
	}
	
	//게시글정보를 전달받아 REVIEW 테이블에 삽입하고 삽입행의 갯수를 반환하는 메소드
	public int insertReview(ReviewDTO review) {
		Connection con=null;
		PreparedStatement pstmt=null;
		int rows=0;
		try {
			con=getConnection();
			
			String sql="insert into review values(?,?,?,?,?,sysdate,0,?,?,?,?,?)";
			pstmt=con.prepareStatement(sql);
			pstmt.setInt(1, review.getNum());
			pstmt.setString(2, review.getReviewid());
			pstmt.setString(3, review.getSubject());
			pstmt.setString(4, review.getContent());
			pstmt.setString(5, review.getReviewimg());
			pstmt.setInt(6, review.getRef());
			pstmt.setInt(7, review.getRestep());
			pstmt.setInt(8, review.getRelevel());
			pstmt.setString(9, review.getIp());
			pstmt.setInt(10, review.getStatus());
			
			rows=pstmt.executeUpdate();
		} catch (SQLException e) {
			System.out.println("[에러]insertReview() 메소드의 SQL 오류 = "+e.getMessage());
		} finally {
			close(con, pstmt);
		}
		return rows;
	}
	
	//부모글 관련 정보를 전달받아 REVIEW 테이블에 저장된 게시글에서 부모글의 글그룹과 같은
	//게시글 중 부모글의 글순서보다 큰 게시글의 RESTEP 컬럼값을 1 증가되도록 변경하고 
	//변경행의 갯수를 반환하는 메소드 
	public int updateRestep(int ref, int restep) {
		Connection con=null;
		PreparedStatement pstmt=null;
		int rows=0;
		try {
			con=getConnection();
			
			String sql="update review set restep=restep+1 where ref=? and restep>?";
			pstmt=con.prepareStatement(sql);
			pstmt.setInt(1, ref);
			pstmt.setInt(1, restep);
			
			rows=pstmt.executeUpdate();
		} catch (SQLException e) {
			System.out.println("[에러]updateRestep() 메소드의 SQL 오류 = "+e.getMessage());
		} finally {
			close(con, pstmt);
		}
		return rows;
	}
}

 


main>java> xyz.itwill >util)

Utllity.java          - XSS공격방어

package xyz.itwill.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;

//웹프로그램 작성에 필요한 기능을 제공하기 위한 클래스
public class Utility {
	//문자열을 전달받아 암호화 처리하여 반환하는 메소드
	public static String encrypt(String passwd) {
		String encryptPasswd="";//암호화 처리된 비밀번호를 저장하기 변수
		try {
			//MessageDigest.getInstance(String algorithm) : 매개변수로 전달받은 암호화
			//알고리즘이 저장된 MessageDigest 객체를 생성하여 반환하는 메소드
			// => MessageDigest 객체 : 암호화 처리 기능을 제공하기 위한 객체
			// => 매개변수에 잘못된 암호화 알고리즘을 전달할 경우 NoSuchAlgorithmException 발생
			//단방향 암호화 알고리즘(복호화 불가능) : MD5, SHA-1, SHA-256, SHA-512 등 
			//양방향 암호화 알고리즘(복호화 가능) : AES-123, RSA 등
			MessageDigest messageDigest=MessageDigest.getInstance("SHA-256");
					
			//MessageDigest.update(byte[] input) : MessageDigest 객체에 암호화 처리하기 위한
			//문자열을 byte 배열로 전달받아 저장하기 위한 메소드
			//String.getBytes() : String 객체에 저장된 문자열을 원시데이타(byte 배열)로 변환하여 반환하는 메소드
			messageDigest.update(passwd.getBytes());		
			
			//MessageDigest.digest() : MessageDigest 객체에 저장된 암호화 알고리즘을 사용하여
			//byte 배열의 값을 암호화 처리하여 byte 배열로 반환하는 메소드
			byte[] digest=messageDigest.digest();
			
			//암호화 처리된 byte 배열을 String 객체의 문자열로 변환하여 저장
			for(int i=0;i<digest.length;i++) {
				//Integer.toHexString(int i) : 매개변수로 전달받은 정수값을 16진수의 문자열로 변환하여 반환하는 메소드
				encryptPasswd+=Integer.toHexString(digest[i]&0xff);
			}
		} catch (NoSuchAlgorithmException e) {
			System.out.println("[에러]잘못된 암호화 알고리즘을 사용 하였습니다.");
		}
		return encryptPasswd;
	}
	
	//문자열을 전달받아 태그 관련 문자열을 모두 제거하여 반환하는 메소드
	public static String stripTag(String source) {
		//Pattern.compile(String regex) : 매개변수로 전달받은 정규표현식이 저장된 Pattern 객체를
		//생성하여 반환하는 메소드
		Pattern htmlTag=Pattern.compile("\\<.*?\\>");
		
		//Pattern.matcher(CharSequence input) : 매개변수에 입력값을 전달받아 Pattern 객체의 
		//정규표현식과 비교값이 저장된 Matcher 객체를 생성하여 반환하는 메소드
		// => Matcher 객체 : 정규표현식과 입력값을 비교하여 문자열의 검색,변경,삭제 기능을 제공하기 위한 객체
		//Matcher.replaceAll(String replacement) : 입력값에서 정규표현식과 동일한 패턴의
		//문자열을 모두 찾아 매개변수로 전달받은 문자열로 변경하는 메소드
		return htmlTag.matcher(source).replaceAll("");//문자열에서 HTML 태그를 삭제하여 반환
	}
	
	//문자열을 전달받아 태그 관련 문자를 회피문자로 변경하여 반환하는 메소드
	public static String escapeTag(String source) {
		return source.replace("<", "&lt;").replace(">", "&gt;");
	}
}

 


 

 





리뷰 사진 안없애고 싶으면, 폴더만들어서 직접 넣어줘야함