각 회원들의 개인 일정을 설정하는 페이지입니다. 일정 관리에 필요한 기본적인 기능들이 포함되어 있습니다.
- 일정 생성, 조회, 수정, 삭제를 통하여 자신의 일정을 관리하고 다른사람의 일정을 조회해볼 수 있습니다.
- 로그인한 사람에 한해서만 페이지에 접근할 수 있게 구현하였습니다.
- 다른사람의 정보에 접근할 수 없도록 프론트단과 백엔드 단에서 벨리데이션 체크를 하여 잘못된 접근을 방지하였습니다.
풀캘린더 화면과 데이터 파싱을 사용하는 페이지들은 모두 한 스크립트와 jsp를 사용하였습니다.
- jsp 페이지를 인클루딩 해오는 방식으로 하나의 풀캘린더를 구현 후 인클루딩하여 여러 페이지에서 사용하였습니다
- main.js 파일에 풀캘린더 div에 데이터를 파싱해주는 메서드를 정의하여 사용하였습니다.
댓글과 대댓글을 등록하고 목록을 보여주는 화면을 일정 상세정보 창에 보여준다.
- 댓글과 대댓글 조회, 등록, 수정, 삭제를 할 수 있고 댓글과 대댓글은 depth 칼럼으로 구분한다.
- 대댓글에는 답글을 달 수 없게 하였다.
- 삭제와 수정의 경우에는 글쓴이와 로그인한 사용자의 정보가 일치할 경우에만 수정 삭제 버튼이 보이고 컨트롤러에서 사용자와 글쓴이의 일치관계를 한번 더 재확인한다.
// 세션에 저장된 사용자의 pk
int sessionMember = Integer.parseInt(session.getAttribute("member").toString());
// 파라미터로 넘어온 글쓴이의 pk
int memberNo = Integer.parseInt(requestParam.get("memberNo").toString());
// 사용자와 글쓴이의 pk가 일치하지 않을경우
if (sessionMember != memberNo) throw new NullPointerException("잘못된 접근입니다.");
/*
* 댓글 조회 쿼리문
* GROUP_NO = 댓글의 순서이다 GROUP_NO가 같은 수일 경우 하나의 댓글과 하나 이상의 대댓글이다.
* DEPTH = 0이면 댓글 1이면 대댓글이다.
*/
<select id="replyList" parameterType="int" resultType="hashMap">
select SEQ, m.NAME, SAMPLE_SEQ, COMMENT, CRE_DATE, DEPTH, PARENT_REPLYS_NO, GROUP_NO, MEMBER_NO
from REPLYS as r
left join MEMBER as m
on r.MEMBER_NO = m.NO
where SAMPLE_SEQ = #{seq, jdbcType=INTEGER}
and DELETE_AT = 'N'
order by GROUP_NO, DEPTH
</select>
/*
* 댓글일 경우와 대댓글일 경우 댓글 등록시 등록해야 하는 값이 다르므로 depth의 값에따라 다른 쿼리문을 처리한다.
*/
<insert id="addReply" parameterType="hashMap">
insert into REPLYS(MEMBER_NO, SAMPLE_SEQ, COMMENT, CRE_DATE, DEPTH, PARENT_REPLYS_NO, GROUP_NO, DELETE_AT)
<choose>
<when test="depth == 0">
values (#{memberNo}, #{sample_seq}, #{comment}, now(), #{depth}, null, (select ifnull(max(r.GROUP_NO),0) +1 from REPLYS as r), 'N')
</when>
<otherwise>
values (#{memberNo}, #{sample_seq}, #{comment}, now(), #{depth}, #{parentNo}, #{groupNo}, 'N')
</otherwise>
</choose>
</insert>
회원가입시 해야하는 기본적인 벨리데이션 체크를 하였습니다.
- 이메일, 이름, 비밀번호, 비밀번호 확인, 성별, 부서, 직급 정보를 입력받고 해당 데이터가 입력되지 않았을 경우 해당 태그에 포커싱되게 하고 회원가입이 되지 않게 하였습니다.
- 이메일은 ex) sattlub123@gmail.com 처럼 @ 앞에 아이디 뒤에 메일주소를 입력하지 않으면 회원가입이 되지 않도록 하였습니다.
- 회원정보 등록시 db에서 이메일의 중복 여부를 체크하여 중복되어있을 경우 중복된 이메일이라는 경고창과 함께 회원가입이 되지 않도록 하였습니다.
- 이름은 2~10글자 이내로 한글 및 영문만 입력하게 하였습니다.
- 비밀번호와 비밀번호 확인은 영문+숫자+특수문자 조합으로 8~16이내의 정보를 입력하지 않거나 둘의 값이 다를 경우 회원가입이 되지 않도록 하였습니다.
//공백체크 및 비밀번호 & 비밀번호 확인 일치여부 확인
var email = $$("email").getValue();
// 이메일 공백확인
if (spaceCheck(email, "email", "이메일을 입력하세요.") == 0) return;
var name = $$("name").getValue();
// 이름 공백 확인
if (spaceCheck(name, "name", "이름을 입력하세요.") == 0) return;
var pwd = $$("pwd").getValue();
var pwdCheck = $$("pwdCheck").getValue();
// 비밀번호 공백 확인
if (spaceCheck(pwd, "pwd", "비밀번호를 입력하세요") == 0) return;
// 비밀번호확인 공백 확인
if (spaceCheck(pwdCheck, "pwdCheck", "비밀번호 확인을 입력하세요.") == 0) return;
// 비밀번호와 비밀번호 확인이 일치하는지
if (pwd != pwdCheck) {
alert("비밀번호와 비밀번호 확인이 일치하지 않습니다.");
$$("pwd").focus();
$$("pwdCheck").focus();
return;
}
var dept = $$("dept").getValue();
// 부서 선택여부 확인
if (spaceCheck(dept, "dept", "부서를 선택하세요.") == 0);
var classes = $$("classes").getValue();
// 직급 선택여부 확인
if (spaceCheck(classes, "classes", "직급을 선택하세요.") == 0);
var gender = $$("gender").getValue();
// 성별 선택여부 확인
if (spaceCheck(gender, "gender", "성별을 선택하세요.") == 0);
// 벨리데이션 체크 함수
if (memberValidation(param)) {
return;
}
위의 코드에 정의된 spaceCheck와 memberValidation 메서드
// 공백여부 체크
function spaceCheck(obj, objName, comment) {
if(obj == null || obj == "" || obj.length == 0){
alert(comment);
$$(objName).focus();
return 0;
}
return 1;
}
// 회원정보 벨리데이션 체크
function memberValidation (param) {
/*
* member param 정보
param.name = name;
param.email = email;
param.pwd = pwd;
param.gender = gender;
param.dept = dept;
param.classes = classes;
*/
// 이메일, 이름, 비밀번호의 벨리데이션 체크에 필요한 정규식 표현들
var emailRegex = RegExp("^[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*\\.[a-zA-Z]{2,3}$");
var nameRegex = RegExp("^[가-힣a-zA-Z]{2,10}$");
var pwdRegex = RegExp("^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!~`@#&$%^*()\\-_=+\\\\\\|\\[\\]{};:\\'\"<>\\/?]).{8,16}$"); // 영문 + 숫자 + 특수문자(_ 는 제외) 조합으로 8자 이상 16자 이하로 조합하여야 한다
if (regexCheck(emailRegex, param.email, "이메일 형식이 올바르지 않습니다.") == 0) return true;
if (regexCheck(nameRegex, param.name, "이름은 2~10글자 내외로 한글, 영문만 입력가능합니다.") == 0) return true;
if (regexCheck(pwdRegex, param.pwd, "비밀번호는 8~16글자 내외로 문자, 숫자, 특수문자를 조합하여 등록하세요.") == 0) return true;
return false;
}
// 정규식 표현 체크를 위한 메서드
function regexCheck (regex, obj, comment) {
if (!regex.test(obj)) {
alert (comment);
return 0;
}
return 1;
}
이메일과 비밀번호 조회결과를 체크합니다.
- 아이디, 비밀번호를 입력하였는지 공백여부를 체크합니다.
- 로그인 정보가 db정보와 일치하지 않는다면 아이디가 틀렸는지 비밀번호가 틀렸는지의 경우를 따로 알려줍니다.
- 로그인을 하지 않을경우 로그인 페이지와 회원가입 페이지 이외의 서비스를 이용할 수 없게 하였습니다.
등록된 일정들을 조회설정대로 검색하여 조회 결과를 보여줍니다.
- 화면에서 설정한 타입별로 데이터를 조회하도록 마이바티스에서 쿼리를 정의하였습니다.
<select id="searchSample" resultType="hashMap" parameterType="hashMap">
select SEQ as id,
SEQ,
MEMBER_NO,
TITLE,
CONTENT,
TYPE,
START_DATE,
END_DATE,
M.NAME
from SAMPLE as S
left join MEMBER as M
on S.MEMBER_NO = M.NO
where DELETE_AT = 'N'
<if test="_parameter != null" >
<if test="LIKE_TITLE != null">
<if test="TYPE_SELECT == 1">
and upper(M.NAME) like #{LIKE_TITLE,jdbcType=VARCHAR}
</if>
<if test="TYPE_SELECT == 2">
and upper(TITLE) like #{LIKE_TITLE,jdbcType=VARCHAR}
</if>
</if>
</if>
<if test="CALENDAR_SELECT != 0">
and TYPE = #{CALENDAR_SELECT}
</if>
order by SEQ desc;
</select>
자신의 일정에 관한 정보들을 입력받습니다.
- 일정명, 내용, 일정 종류, 시작일, 종료일, 일정을 같이하는 멤버를 등록합니다.
- 정보 입력이 누락될 경우를 대비하여 일정을 같이하는 멤버의 정보 이외의 정보들은 공백일 경우 일정등록을 못하게 하였습니다.
// 추가된 멤버들을 담을 배열
var addMemberArr = new Array();
var title = trim("TITLE");
if (spaceCheck(title, "TITLE", "제목을 입력하세요.") == 0) return;
var content = trim("CONTENT");
if (spaceCheck(content, "CONTENT", "내용을 입력하세요.") == 0) return;
var type = $$("TYPE").getValue();
if (spaceCheck(type, "TYPE", "일정 종류를 체크하세요.") == 0) return;
var startDate = dateFormat($$("STARTDATE").getValue());
if (spaceCheck(startDate, "STARTDATE", "일정 시작일을 입력하세요") == 0) return;
var endDate = dateFormat($$("ENDDATE").getValue());
if (spaceCheck(endDate, "ENDDATE", "일정 종료일을 입력하세요") == 0) return;
if (dateCheck(startDate, endDate) == 0) return;
if (title.length > 20 || title.length < 2) {
alert("글 제목은 2글자 이상 20글자 이하로 작성하세요.");
return;
}
if (content.length > 50 || content.length < 2) {
alert("글 내용은 2글자 이상 50글자 이하로 입력하세요.");
return ;
}
// webix의 id명을 매개변수로 입력받아서 객체값의 좌우 여백을 없애주는 메서드
function trim(objName) {
return $.trim($$(objName).getValue());
}
- 일정에 관한 데이터를 받아올 때 초기 멤버배열, 추가 멤버배열, 삭제 멤버배열을 생성하여 기존에 등록되있던 회원 이외의 추가 멤버들만 데이터 테이블에 추가한다.
// 초기 멤버배열
var initializedMember = new Array();
// 추가 멤버배열
var addMemberArr = new Array();
// 삭제 멤버배열
var deleteMemberArr = new Array();
// Submit
function doSubmit() {
var title = trim("TITLE");
if (spaceCheck (title, "TITLE", "제목을 입력하세요.") == 0) return;
var content = trim("CONTENT");
if (spaceCheck (content, "CONTENT", "내용을 입력하세요.") == 0) return;
var startDate = dateFormat($$("STARTDATE").getValue());
if (spaceCheck (startDate, "STARTDATE", "시작일을 입력하세요.") == 0) return;
var endDate = dateFormat($$("ENDDATE").getValue());
if (spaceCheck (endDate, "ENDDATE", "종료일을 입력하세요.") == 0) return;
if (dateCheck(startDate, endDate) == 0) return;
// param에 글 내용 저장
var param = {};
// 추가해야할 멤버만 골라낸다
for (var i= 0; i < initializedMember.length; i++) {
if (addMemberArr.indexOf(initializedMember[i]) != -1) {
addMemberArr.splice(addMemberArr.indexOf(initializedMember[i]), 1);
}
}
param.ADDMEMBERLIST = addMemberArr;
param.DELETEMEMBERLIST = deleteMemberArr;
param.SEQ = $$("SEQ").getValue();
param.MEMBERNO = $$("MEMBERNO").getValue();
param.TITLE = title;
param.CONTENT = content;
param.TYPE = $$("TYPE").getValue();
param.STARTDATE = startDate;
param.ENDDATE = endDate;
// 수정 정보 전송
$.ajax({
type:"POST",
url:"${pageContext.request.contextPath}/sample/updateSample.do",
contentType: "application/json",
data:JSON.stringify(param),
dataType:"json",
success:function(data, status, xhr){
if(data.result == "SUCCESS"){
goBack("${pageContext.request.contextPath}/sample/sampleListView", "수정되었습니다.");
}else{
alert(data.errorMsg);
}
},
error:function(data, status, xhr){
alert(data.responseText);
}
});
}
// 일정 정보를 가져온다
function sampleOne () {
var param = new Object();
param.SEQ = ${param.seq};
param.RESULT = "UPDATE";
$.ajax({
type:"POST",
url:"${pageContext.request.contextPath}/sample/selectSample.do",
async:false,
data:param,
dataType:"json",
success:function(data, status, xhr){
// 날짜 형식 변환
let startDate = new Date(data.sampleList[0].start);
let endDate = new Date(data.sampleList[0].end);
$$("SEQ").setValue(data.sampleList[0].id);
$$("MEMBERNO").setValue(data.sampleList[0].memberNo);
$$("TITLE").setValue(data.sampleList[0].title);
$$("CONTENT").setValue(data.sampleList[0].content);
$$("TYPE").setValue(data.sampleList[0].type);
$$("STARTDATE").setValue(startDate);
$$("ENDDATE").setValue(endDate);
},
error:function(data, status, xhr){
alert(data.responseText);
}
});
}
// 해당 글에 해당된 회원 조회
function selectPartyMember () {
var memberList = new Array();
var no = ${param.seq};
$.ajax({
url: "${pageContext.request.contextPath}/sample/partyMember.do",
type: "POST",
dataType: "json",
data: {"no": no},
success: function(data, status, xhr){
memberList = data;
for (var i=0; i < memberList.length; i++) {
let addMember = memberList[i];
$("#addMember").append(deleteMemberDiv(addMember));
var member = $(".member"+ addMember.NO);
member.data("no", memberList[i].NO);
deleteEvent (member, addMemberArr, deleteMemberArr);
addMemberArr.push(addMember.NO);
initializedMember.push(addMember.NO);
}
},
error: function(data, status, xhr) {
alert(data.responseText);
}
});
}
//회원 조회
function searchName () {
let memberList = new Array();
let param = {};
param.name = $(".likeName").val();
param.type = $(".type").val();
param.writer = ${member};
// 부서명이나 직급을 검색할 경우 index값으로 치환
if (param.type == "dept") param.name = deptList.indexOf(param.name);
if (param.type == "classes") param.name = classesList.indexOf(param.name);
$.ajax({
url: "${pageContext.request.contextPath}/member/searchMember",
type: "GET",
data: param,
success: function (data, status, xhr) {
console.log(data);
if (data.memberList[0] == null) {
// 검색결과가 없음
$("#main").text("검색결과가 없습니다.");
} else {
$("#main").empty();
// 검색결과가 있음
memberList = data.memberList;
for (var i= 0; i < memberList.length; i++) {
if (addMemberArr.indexOf(memberList[i].NO) == -1) {
$("#main").append(createMemberDiv(memberList[i]));
var member = $(".addMember"+ memberList[i].NO);
inputElementData (member, memberList[i]);
memberClickEvent (member, addMemberArr, deleteMemberArr);
}
}
}
},
error: function (data, status, xhr) {
alert(data.responseText);
}
});
}
일정 정보를 보여준다
- 일정 기간과 일정명을 달력에 표출하고 클릭할 경우 상세 데이터를 나타낸다.
- 자기 자신의 일정일 경우에만 수정페이지로 이동하는 버튼과 삭제버튼을 보여준다
// 서블릿에서 풀캘린더가 바로 파싱할 수 있도록 키와 값을 설정한다
// 게시글의 정보를 풀캘린더에서 바로 읽어들일 수 있도록 세팅한다
private void sampleSettings (HashMap<String, Object> map, String contextPath) {
// 풀캘린더에서 데이터를 읽어들일수 있게 값 세팅
map.put("id", map.get("SEQ"));
map.put("memberNo", map.get("MEMBER_NO"));
map.put("url", contextPath+ "/sample/sampleUpdateView?seq=" + map.get("SEQ"));
map.put("textColor", "white");
map.put("title", map.get("TITLE"));
map.put("content", map.get("CONTENT"));
map.put("start", map.get("START_DATE"));
map.put("end", map.get("END_DATE"));
map.put("name", map.get("NAME"));
map.put("type", map.get("TYPE"));
// 키 삭제
map.remove("SEQ");
map.remove("MEMBER_NO");
map.remove("TITLE");
map.remove("CONTENT");
map.remove("START_DATE");
map.remove("END_DATE");
map.remove("NAME");
map.remove("TYPE");
if (map.get("type") != null) {
int type = Integer.parseInt(map.get("type").toString());
if (type == 1) {
// 휴가
map.put("color", "#00bfff");
} else if (type == 2) {
// 회의
map.put("color", "#32cd32");
} else if (type == 3) {
// 교육
map.put("color", "#088A68");
} else if (type == 4) {
// 야근
map.put("color", "#7401DF");
}
}
}
// url 프로퍼티를 eventUrl의 href 속성으로 등록하여 클릭할 경우 해당 글의 수정페이지로 이동하게 한다.
$('#eventUrl').attr('href',event.url);
// 일정 삭제 메서드
function scheduleDelete (seq, contextPath, memberNo) {
var param = {};
param.seq = seq;
param.memberNo = memberNo;
$.ajax({
url: contextPath+ "/sample/deleteSample.do",
type: "POST",
data: param,
dataType: "json",
success: function(data) {
if (data.result == "SUCCESS") {
alert("해당 일정이 삭제되었습니다.");
window.location.href= contextPath+ "/sample/sampleListView";
} else {
alert("삭제되지 않았습니다.");
}
},
error: function(data) {
alert(data.responseText);
}
});
}
일정 pk를 서버에 전달하여 데이터를 삭제한다
- 일정 데이터의 pk와 등록자 pk를 서버에 전송하여 컨트롤러에서 현재 로그인중인 멤버의 pk와 비교하여 일치할 경우에만 삭제를 한다
- 데이터를 삭제시키는게 아닌 일정 테이블의 DELETE_AT 컬럼을 'Y'값으로 변경한다.
// 게시물 삭제
@ResponseBody
@RequestMapping(value = "/sample/deleteSample.do", method = RequestMethod.POST)
public HashMap<String, Object> deleteSample(@RequestParam HashMap<String, Object> requestParam) {
HashMap<String, Object> resultMap = new HashMap<String, Object>();
try{
if (session.getAttribute("member") == null) throw new Exception("비로그인 상태");
int memberNo = Integer.parseInt(requestParam.get("memberNo").toString());
int sessionMemberNo = Integer.parseInt(session.getAttribute("member").toString());
if (memberNo != sessionMemberNo) throw new Exception("사용자 정보가 다름");
sampleService.deleteSample(requestParam);
resultMap.put("result", "SUCCESS");
}catch(Exception e){
resultMap.put("result", "FAIL");
resultMap.put("errorMsg", e.getMessage());
}
return resultMap;
}
// 쿼리 부분
<update id="deleteSample" parameterType="hashMap">
update SAMPLE
set DELETE_AT = 'Y'
where SEQ = #{seq,jdbcType=INTEGER}
</update>
사이드바에서 캘린더 메뉴로 들어가면 모든 일정 조회를 캘린더 화면으로 받아볼 수 있다
- 일정 목록 조회조건이랑 똑같으며 일정 데이터를 달력 화면에서 조회해서 볼 수 있다.
내 일정만 조회해서 화면에 달력형태로 데이터를 보여준다
- 내가 등록한 일정이랑 타인의 일정중 내가 참여하는 일정 데이터를 달력화면으로 보여준다
select s.SEQ, m2.NAME, s.TITLE, m.NO, s.CONTENT, s.TYPE, s.START_DATE, s.END_DATE
from SAMPLE s
join SAMPLEPARTY sp on s.seq = sp.sample_seq
join MEMBER m on sp.member_no = m.no
join MEMBER m2 on s.member_no = m2.no
where m.no = #{memberNo}
and s.delete_at = 'N'
union
select s.SEQ, m.NAME,s.TITLE, m.NO, s.CONTENT, s.TYPE, s.START_DATE, s.END_DATE
from sample s
join MEMBER m on s.member_no = m.no
where m.no = #{memberNo}
and s.delete_at = 'N'