Study Web Development

3월 14일

이전으로

Spring

10. springPage

Interceptor

  1. 새 클래스 WriterCheckInterceptor 생성
package kr.spring.interceptor;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import kr.spring.board.service.BoardService;
import kr.spring.board.vo.BoardVO;

public class WriterCheckInterceptor extends HandlerInterceptorAdapter {
	private static final Logger logger = LoggerFactory.getLogger(WriterCheckInterceptor.class);
	
	@Autowired
	private BoardService boardService;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		logger.info("<<로그인한 회원 번호와 작성자 회원 번호 일치 여부 검사>>");
		
		// 작성자의 회원 번호 구하기
		int board_num = Integer.parseInt(request.getParameter("board_num"));
		BoardVO board = boardService.selectBoard(board_num);
		
		// 로그인한 회원 번호 구하기
		HttpSession session = request.getSession();
		Integer user_num = (Integer)session.getAttribute("user_num");
		
		// 로그인한 회원 번호와 작성자 회원 번호 일치 여부 검사
		if(user_num==null || user_num!=board.getMem_num()) {
			request.setAttribute("accessMsg", "로그인한 아이디와 작성자 아이디 불일치");
			request.setAttribute("accessUrl", request.getContextPath() + "/board/list.do");
			RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/common/notice.jsp");
			dispatcher.forward(request, response);
			return false;
		}
		
		return true;
	}
}

프로젝트 설정

XML
  1. servlet-context.xml에서 <interceptors> 태그 사이에 다음의 내용을 추가
		<interceptor>
			<mapping path="/board/update.do"/>
			<mapping path="/board/delete.do"/>
			<beans:bean class="kr.spring.interceptor.WriterCheckInterceptor"/>
		</interceptor>
CKEditor

https://ckeditor.com/

  1. src/main/webapp/resources 폴더 오른쪽 클릭하고 새 폴더 image_upload 생성
  2. ckeditor.js, ckeditor.js.map, uploadAdapter.js, videoAdapter.jsjs 폴더로 이동
CSS
  1. css 폴더의 style.css 하단에 다음의 내용을 추가
/* 글 상세 */
.detail-img {
	max-width: 500px;
}
div#reply_div {
	padding: 5px 10px 40px 10px;
	margin-top: 10px;
	background-color: #eeeeee;
}
form#re_form {
	width: 650px;
	border: none;
}
span.re-title {
	color: #000;
	font-size: 12pt;
	line-height: 200%;
}
span.letter-count {
	font-size: 10pt;
	color: #999;
}
textarea.rep-content {
	width: 90%;
	height: 50px;
	margin: 10px;
}
div#re_first,div#mre_first {
	float: left;
	width: 70%;
	padding-left: 15px;
	margin-bottom: 10px;
}
div#re_second,div#mre_second {
	float: left;
	width: 19%;
	margin-bottom: 10px;
}
div#loading {
	width: 100px;
	height: 50px;
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: flex;
	align-items: center;
	justify-content: center;
}
div.paging-button {
	text-align: right;
}
div#output {
	clear: both;
}
form#mre_form {
	border: none;
	margin: 5px;
}

/* CKEditor 사용시 등록, 수정 폼 */
form#register_form, form#update_form {
	width: 700px;
}
  1. ajax-loader.gifimages 폴더로 이동
SQL
  1. table.sql 하단에 다음의 내용을 추가
/* 댓글 */
CREATE TABLE spboard_reply(
	re_num NUMBER NOT NULL,
	re_content VARCHAR2(900) NOT NULL,
	re_date DATE DEFAULT SYSDATE NOT NULL,
	re_mdate DATE,
	re_ip VARCHAR2(40) NOT NULL,
	board_num NUMBER NOT NULL,
	mem_num NUMBER NOT NULL,
	CONSTRAINT spboard_reply_pk PRIMARY KEY (re_num),
	CONSTRAINT reply_spboard_fk1 FOREIGN KEY (board_num) REFERENCES spboard (board_num),
	CONSTRAINT reply_spmember_fk2 FOREIGN KEY (mem_num) REFERENCES spmember (mem_num)
);

CREATE SEQUENCE spreply_seq;
JavaScript
  1. js 폴더에 새 JavaScript 파일 board.reply.js 생성
$(function() {
	let currentPage;
	let count;
	let rowCount;
	
	// 댓글 목록
	function selectData(pageNum, board_num) {
		currentPage = pageNum;

		// 로딩 이미지 노출
		$('#loading').show();

		$.ajax({
			url:'listReply.do',
			type:'post',
			data:{pageNum:pageNum, board_num:board_num},
			dataType:'json',
			cache:false,
			timeout:30000,
			success:function(param) {
				// 로딩 이미지 감추기
				$('#loading').hide();

				count = param.count;
				rowCount = param.rowCount;

				if(pageNum==1) { // 처음 호출시에는 해당 ID의 div 내용물 제거
					$('#output').empty();
				}

				// 댓글 목록 작업
				$(param.list).each(function(index, item) {
					let output = '<div class="item">';
					output += '<h4>' + item.id + '</h4>';
					output += '<div class="sub-item">';
					output += '<p>' + item.re_content.replace(/\r\n/g, '<br>') + '</p>';

					if(item.re_mdate) {
						output += '<span class="modify-date">최근 수정일 : ' + item.re_mdate + '</span>';
					}
					else {
						output += '<span class="modify-date">등록일 : ' + item.re_date + '</span>'
					}

					if(param.user_num==item.mem_num) {
						output += '	<input type="button" data-num="' + item.re_num + '" value="수정" class="modify-btn">';
						output += '	<input type="button" data-num="' + item.re_num + '" value="삭제" class="delete-btn">';
					}

					output += '<hr size="1" noshade>';
					output += '</div>'
					output += '</div>'

					// 문서 객체에 추가
					$('#output').append(output);
				}); // end of each

				// paging button 처리
				if(currentPage>=Math.ceil(count/rowCount)) { // 다음 페이지가 없음
					$('.paging-button').hide();
				}
				else { // 다음 페이지가 존재
					$('.paging-button').show();
				}
			},
			error:function() {
				// 로딩 이미지 감추기
				$('#loading').hide();

				alert('네트워크 오류 발생!');
			}
		}); // end of ajax
	} // end of selectData

	// 다음 댓글 보기 버튼 클릭시 데이터 추가
	$('.paging-button input').click(function() {
		let pageNum = currentPage+1;
		selectData(pageNum, $('#board_num').val());
	})

	// 댓글 등록
	$('#re_form').submit(function(event) {
		if($('#re_content').val().trim()=='') {
			alert('내용을 입력하세요!');
			$('#re_content').val('').focus();
			return false;
		}

		// 폼에 입력한 데이터 반환
		let data = $(this).serialize();

		// 데이터 전송
		$.ajax({
			url:'writeReply.do',
			type:'post',
			data:data,
			dataType:'json',
			cache:false,
			timeout:30000,
			success:function(param) {
				if(param.result=='logout') {
					alert('로그인해야 작성할 수 있습니다!');
				}
				else if(param.result=='success') {
					// 폼 초기화
					initForm();
					// 댓글 작성이 성공하면 새로 삽입한 댓글을 포함해서 첫 번째 페이지의 댓글을 다시 호출
					selectData(1, $('#board_num').val());
				}
				else {
					alert('댓글 등록시 오류 발생!');
				}
			},
			error:function() {
				alert('네트워크 오류 발생!');
			}
		}); // end of ajax

		// 기본 이벤트 제거
		event.preventDefault();
	}); // end of submit

	// 댓글 작성 폼 초기화
	function initForm() {
		$('textarea').val('');
		$('#re_first .letter-count').text('300/300');
	} // end of initForm

	// textarea에 내용 입력시 글자 수 체크
	$(document).on('keyup', 'textarea', function() {
		// 남은 글자 수 구하기
		let inputLength = $(this).val().length;

		if(inputLength>300) { // 300자를 초과한 경우
			$(this).val($(this).val().substring(0, 300));
		}
		else { // 300자 이하인 경우
			let remain = 300 - inputLength;
			remain += '/300';
			if($(this).attr('id')=='re_content') { // 등록 폼인 경우
				$('#re_first .letter-count').text(remain);
			}
			else { // 수정 폼인 경우
				$('#mre_first .letter-count').text(remain);
			}
		}
	}); // end of on

	// 댓글 수정 버튼 클릭시 수정 폼 노출
	$(document).on('click', '.modify-btn', function() {
		// 댓글 글 번호
		let re_num = $(this).attr('data-num');
		// 댓글 내용
		let content = $(this).parent().find('p').html().replace(/<br>/gi, '\r\n');
		// 댓글 수정 폼 UI
		let modifyUI = '<form id="mre_form">';
		modifyUI += '<input type="hidden" name="re_num" id="mre_num" value="' + re_num + '">';
		modifyUI += '<textarea rows="3" cols="50" name="re_content" id="mre_content" class="rep-content">' + content + '</textarea>';
		modifyUI += '<div id="mre_first"><span class="letter-count">300/300</span></div>';
		modifyUI += '<div id="mre_second" class="align-right">';
		modifyUI += '<input type="submit" value="수정">';
		modifyUI += '<input type="button" value="취소" class="re-reset">';
		modifyUI += '</div>';
		modifyUI += '<hr size="1" width="96%" noshade>';
		modifyUI += '</form>';

		// 이전에 이미 수정 중인 댓글이 있을 경우 수정 버튼을 클릭하면 숨겨둔 sub-item을 노출시키고 수정 폼을 초기화
		initModifyForm();

		// 지금 클릭해서 수정하려는 데이터는 감추기
		$(this).parent().hide(); // 수정 버튼을 감싸고 있는 div

		// 수정 폼을 수정하고자 하는 데이터가 있는 div에 노출
		$(this).parents('.item').append(modifyUI);

		// 입력한 글자 수 세팅
		let inputLength = $('#mre_content').val().length;
		let remain = 300 - inputLength;
		remain += '/300';

		// 문서 객체에 반영
		$('#mre_first .letter-count').text(remain);
	}); // end of on

	// 수정 폼에서 취소 버튼 클릭시 수정 폼 초기화
	$(document).on('click', '.re-reset', function() {
		initModifyForm();
	}); // end of on

	// 댓글 수정 폼 초기화
	function initModifyForm() {
		$('.sub-item').show();
		$('#mre_form').remove();
	}

	// 댓글 수정
	$(document).on('submit', '#mre_form', function(event) {
		if($('#mre_content').val().trim()=='') {
			alert('내용을 입력하세요!');
			$('#mre_content').val('').focus();
			return false;
		}

		// 폼에 입력한 데이터 반환
		let data = $(this).serialize();

		// 데이터 전송
		$.ajax({
			url:'updateReply.do',
			type:'post',
			data:data,
			dataType:'json',
			cache:false,
			timeout:30000,
			success:function(param) {
				if(param.result=='logout') {
					alert('로그인해야 수정할 수 있습니다!');
				}
				else if(param.result=='success') {
					// 수정 데이터 표시
					$('#mre_form').parent().find('p').html($('#mre_content').val().replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\r\n/g, '<br>').replace(/\r/g, '<br>').replace(/\n/g, '<br>'));
					// 최근 수정일 처리
					$('#mre_form').parent().find('.modify-date').text('최근 수정일 : 5초 미만');

					// 수정 폼 초기화
					initModifyForm();
				}
				else if(param.result=='wrongAccess') {
					alert('다른 사람의 댓글은 수정할 수 없습니다!');
				}
				else {
					alert('댓글 수정시 오류 발생!');
				}
			},
			error:function() {
				alert('네트워크 오류 발생!');
			}
		}); // end of ajax

		// 기본 이벤트 제거
		event.preventDefault();
	}); // end of on

	// 댓글 삭제
	$(document).on('click', '.delete-btn', function() {
		// 댓글 번호
		let re_num = $(this).attr('data-num');

		$.ajax({
			url:'deleteReply.do',
			type:'post',
			data:{re_num:re_num},
			dataType:'json',
			cache:false,
			timeout:30000,
			success:function(param) {
				if(param.result=='logout') {
					alert('로그인해야 삭제할 수 있습니다!');
				}
				else if(param.result=='success') {
					alert('삭제 완료!');
					selectData(1, $('#board_num').val());
				}
				else if(param.result=='wrongAccess') {
					alert('다른 사람의 댓글은 삭제할 수 없습니다!');
				}
				else {
					alert('댓글 삭제시 오류 발생!');
				}
			},
			error:function() {
				alert('네트워크 오류 발생!');
			}
		}); // end of ajax
	}); // end of on

	// 초기 데이터(목록) 호출
	selectData(1, $('#board_num').val());
});

Main

Controller
  1. 새 클래스 CommonController 생성
package kr.spring.main.controller;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class CommonController {
	@RequestMapping("/common/imageUploader.do") // uploadAdapter.js와 통신하는 경로
	@ResponseBody
	public Map<String, Object> uploadImage(MultipartFile upload, HttpSession session, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 업로드할 폴더 경로
		String realFolder = session.getServletContext().getRealPath("/resources/image_upload");
		
		// 업로드할 파일명
		String org_filename = upload.getOriginalFilename();
		String str_filename = System.currentTimeMillis() + org_filename; // 파일명 중복 방지
		
		Integer user_num = (Integer)session.getAttribute("user_num");
		
		String filepath = realFolder + "\\" + user_num + "\\" + str_filename;
		
		File f = new File(filepath);
		if(!f.exists()) {
			f.mkdirs(); // mkdir()은 상위 경로가 없으면 에러, mkdirs()는 상위 경로가 없으면 경로 생성
		}
		upload.transferTo(f);
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("uploaded", true);
		map.put("url", request.getContextPath() + "/resources/image_upload/" + user_num + "/" + str_filename);
		
		return map;
	}
}

Board

JSP
  1. boardWrite.jsp 상단에 다음의 내용을 추가
<!-- 부트스트랩 라이브러리 -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<style>
.ck-editor__editable_inline {
	min-height: 250px;
}
</style>
<!-- CKEditor 라이브러리 -->
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/ckeditor.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/uploadAdapter.js"></script>
  1. boardWrite.jsp에서 <ul> 태그 사이의 내용을 다음처럼 수정
			<li>
				<b>내용</b>
			</li>
			<li>
				<form:textarea path="content"/>
				<form:errors path="content" cssClass="error-color"/>
				<script>
				 function MyCustomUploadAdapterPlugin(editor) {
					    editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
					        return new UploadAdapter(loader);
					    }
					}
				 
				 ClassicEditor
		            .create( document.querySelector( '#content' ),{
		            	extraPlugins: [MyCustomUploadAdapterPlugin]
		            })
		            .then( editor => {
						window.editor = editor;
					} )
		            .catch( error => {
		                console.error( error );
		            } );
			    </script>
			</li>
  1. boardView.jsp 상단에 다음의 내용을 추가
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/board.reply.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/resources/js/videoAdapter.js"></script>
  1. boardView.jsp에서 <div class="page-main"> 태그 사이에 다음의 내용을 추가
	<hr size="1" width="100%" noshade>
	<div id="reply_div">
		<span class="re-title">댓글 달기</span>
		<form id="re_form">
			<input type="hidden" name="board_num" value="${board.board_num}" id="board_num">
			<textarea rows="3" cols="50" name="re_content" id="re_content" class="rep_content" <c:if test="${empty user_num}">disabled</c:if>><c:if test="${empty user_num}">로그인해야 작성할 수 있습니다</c:if></textarea>
		<c:if test="${!empty user_num}">
		<div id="re_first">
			<span class="letter-count">300/300</span>
		</div>
		<div id="re_second" class="align-right">
			<input type="submit" value="전송">
		</div>
		</c:if>
		</form>
	</div>
	<!-- 댓글 목록 출력 -->
	<div id="output"></div>
	<div class="paging-button" style="display: none;">
		<input type="button" value="다음 댓글 보기">
	</div>
	<div id="loading" style="display: none;">
		<img src="${pageContext.request.contextPath}/resources/images/ajax-loader.gif">
	</div>
VO
  1. 새 클래스 BoardReplyVO 생성
package kr.spring.board.vo;

import kr.spring.util.DurationFromNow;

public class BoardReplyVO {
	private int re_num;
	private String re_content;
	private String re_date;
	private String re_mdate;
	private String re_ip;
	private int board_num; // 부모글 번호
	private int mem_num; // 댓글 작성자 회원 번호
	private String id; // 댓글 작성자 아이디
	
	public int getRe_num() {
		return re_num;
	}
	public void setRe_num(int re_num) {
		this.re_num = re_num;
	}
	public String getRe_content() {
		return re_content;
	}
	public void setRe_content(String re_content) {
		this.re_content = re_content;
	}
	public String getRe_date() {
		return re_date;
	}
	public void setRe_date(String re_date) {
		this.re_date = DurationFromNow.getTimeDiffLabel(re_date);
	}
	public String getRe_mdate() {
		return re_mdate;
	}
	public void setRe_mdate(String re_mdate) {
		this.re_mdate = DurationFromNow.getTimeDiffLabel(re_mdate);
	}
	public String getRe_ip() {
		return re_ip;
	}
	public void setRe_ip(String re_ip) {
		this.re_ip = re_ip;
	}
	public int getBoard_num() {
		return board_num;
	}
	public void setBoard_num(int board_num) {
		this.board_num = board_num;
	}
	public int getMem_num() {
		return mem_num;
	}
	public void setMem_num(int mem_num) {
		this.mem_num = mem_num;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	
	@Override
	public String toString() {
		return "BoardReplyVO [re_num=" + re_num + ", re_content=" + re_content + ", re_date=" + re_date + ", re_mdate="
				+ re_mdate + ", re_ip=" + re_ip + ", board_num=" + board_num + ", mem_num=" + mem_num + ", id=" + id
				+ "]";
	}
}
DAO
  1. BoardMapper 인터페이스에 다음의 메서드를 추가
	// 댓글
	public List<BoardReplyVO> selectListReply(Map<String, Object> map);
	@Select("SELECT COUNT(*) FROM spboard_reply JOIN spmember USING(mem_num) WHERE board_num=#{board_num}")
	public int selectRowCountReply(Map<String, Object> map);
	@Select("SELECT * FROM spboard_reply WHERE re_num=#{re_num}")
	public BoardReplyVO selectReply(Integer re_num);
	@Insert("INSERT INTO spboard_reply (re_num, re_content, re_ip, board_num, mem_num) "
			+ "VALUES (spreply_seq.NEXTVAL, #{re_content}, #{re_ip}, #{board_num}, #{mem_num})")
	public void insertReply(BoardReplyVO boardReply);
	@Update("UPDATE spboard_reply SET re_content=#{re_content}, re_ip=#{re_ip}, re_mdate=SYSDATE WHERE re_num=#{re_num}")
	public void updateReply(BoardReplyVO boardReply);
	@Delete("DELETE FROM spboard_reply WHERE re_num=#{re_num}")
	public void deleteReply(Integer re_num);	
	// 부모글 삭제시 댓글이 존재하면 부모글 삭제 전 댓글 삭제
	@Delete("DELETE FROM spboard_reply WHERE board_num=#{board_num}")
	public void deleteReplyByBoardNum(Integer board_num);
Service
  1. BoardService 인터페이스에 다음의 내용을 추가
import kr.spring.board.vo.BoardReplyVO;
  1. BoardService 인터페이스에 다음의 메서드를 추가
	// 댓글
	public List<BoardReplyVO> selectListReply(Map<String, Object> map);
	public int selectRowCountReply(Map<String, Object> map);
	public BoardReplyVO selectReply(Integer re_num);
	public void insertReply(BoardReplyVO boardReply);
	public void updateReply(BoardReplyVO boardReply);
	public void deleteReply(Integer re_num);
  1. BoardServiceImpl 클래스에 다음의 내용을 추가
import kr.spring.board.vo.BoardReplyVO;
  1. BoardServiceImpl 클래스에서 deleteBoard 메서드를 다음처럼 수정
	@Override
	public void deleteBoard(Integer board_num) {
		// 댓글이 존재하면 댓글을 우선 삭제하고 부모글을 삭제
		boardMapper.deleteReplyByBoardNum(board_num);
		boardMapper.deleteBoard(board_num);
	}
  1. BoardServiceImpl 클래스에 다음의 메서드를 추가
	@Override
	public List<BoardReplyVO> selectListReply(Map<String, Object> map) {
		return boardMapper.selectListReply(map);
	}

	@Override
	public int selectRowCountReply(Map<String, Object> map) {
		return boardMapper.selectRowCountReply(map);
	}

	@Override
	public BoardReplyVO selectReply(Integer re_num) {
		return boardMapper.selectReply(re_num);
	}

	@Override
	public void insertReply(BoardReplyVO boardReply) {
		boardMapper.insertReply(boardReply);
	}

	@Override
	public void updateReply(BoardReplyVO boardReply) {
		boardMapper.updateReply(boardReply);
	}

	@Override
	public void deleteReply(Integer re_num) {
		boardMapper.deleteReply(re_num);
	}

Util

  1. kr.spring.util 패키지에 새 클래스 DurationFromNow 생성
package kr.spring.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class DurationFromNow {
	/**
	 * 현재부터 "yyyyMMddHHmmss" 포맷의 날짜 차이 레이블
	 * @param date1
	 * @return String
	 */
	public static String getTimeDiffLabel(String date1) {
		try {
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			
			return getTimeDiffLabel(sdf.parse(date1), new Date());
		} catch (ParseException e) {
			return "-";
		}
	}

	/**
	 * 현재부터 Date 포맷의 날짜 차이 레이블
	 * @param d1
	 * @return String
	 */
	public static String getTimeDiffLabel(Date d1) {
		return getTimeDiffLabel(d1, new Date());
	}

	/**
	 * "yyyyMMddHHmmss" 포맷의 날짜 차이 레이블
	 * @param date1
	 * @param date2
	 * @return String
	 */
	public static String getTimeDiffLabel(String date1, String date2) {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd- HH:mm:ss");
		try {
			return getTimeDiffLabel(sdf.parse(date1), sdf.parse(date2));
		} catch (ParseException e) {
			return "-";
		}
	}

	/**
	 * java.util.Date 포맷의 날짜 차이 레이블
	 * @param d1
	 * @param d2
	 * @return String
	 */
	public static String getTimeDiffLabel(Date d1, Date d2) {
		long diff = d2.getTime() - d1.getTime();
		int sec = (int)(diff / 1000);
		if (sec < 5) return "5초미만";
		if (sec < 60) return sec + "초 전";

		int min = (int)(sec / 60);
		if (min < 60) return min + "분 전";

		int hour = (int)(min / 60);
		if (hour < 24) return hour + "시간 전";

		Calendar c1 = Calendar.getInstance();
		Calendar c2 = (Calendar) c1.clone();
		c1.setTime(d1);
		c2.setTime(d2);

		int day = c2.get(Calendar.DATE) - c1.get(Calendar.DATE);
		if (day <= 0) {
			day = hour / 24;
		}

		if (hour/24 < 30) {
			if (day == 1) return "어제";
			if (day == 2) return "2일전";
			return day + "일전";
		}

		int month = hour / 24 / 30;
		if (month == 1) return "한 달전";
		if (month == 2) return "두 달전";
		if (month < 12) return month + "달전";

		int year = month / 12;
		if (year == 1) return "작년";
		return year + "년전";

	}
}

다음으로