파이널 프로젝트: 협업툴 Safari 회고

메밀·2022년 11월 1일
0

협업툴 Safari 회고

목록 보기
1/2

메인 페이지

배포 링크
깃헙 링크

1. Safari

파이널 프로젝트의 주제는 협업툴이다. 우리 조는 나를 포함하여 총 네 명이었는데, 나는 프로젝트, 프로젝트 통계, 업무 피드백, 채팅을 구현하기로 했다.


설계를 하고 선생님께 검사를 맡으며 두근거리던 감정이 아직도 선하다.
그리고 신나게 나눴지만… 나중에 반정규화하고 싶었다.

여담

Safari라는 프로젝트명은 ‘우리 넷 다 맥북 쓰니까 사파리 해!’로 3초만에 지어졌다. 심지어 이 시점엔 프로젝트명도 아니고 팀명이었다. 솔직하게는 ‘이렇게 지어도 되는거야?’라고 생각했지만… 자기도 고유명사라고 이제는 Safari가 아닌 다른 이름은 상상하기 어렵게 되었다.




2. 구현기능 1) 프로젝트

사파리의 주요기능 흐름은 다음과 같다.

  1. 로그인을 한 유저는 워크스페이스에 진입한다.
  2. 그곳엔 해당 워크스페이스에 속한 프로젝트 목록이 있다.
  3. 프로젝트를 클릭하면 해당 프로젝트의 업무 리스트와 스케줄 탭을 확인할 수 있다.

이 중 내가 맡은 프로젝트 기능에서 하나의 프로젝트를 추가하기 위해선 프로젝트 이름, 시작일, 마감일, 프로젝트 관리자, 프로젝트 멤버 등의 정보가 필요하다. 프로젝트 최초 생성 시 만든이가 프로젝트 관리자로 등록되고 만든이는 워크스페이스에 속한 이들 중 프로젝트 멤버를 선택할 수 있다. 결론적으로 CRUD 중 Create은 꽤나 순조로웠다. 그런데……



시련: 프로젝트 관리자, 프로젝트 멤버 수정

이 시점에서 내가 만들어야 했던 로직은 다음과 같다.

  1. 프로젝트 관리자와 프로젝트 멤버는 모두 복수 배정 가능하다.
  2. 해당 워크스페이스에 속한 멤버만이 프로젝트 관리자/멤버가 될 수 있다.
  3. 동일인이 프로젝트 관리자이자 멤버일 수는 없다.

결론: 모르겠다. 어떻게 하지?

조장이었던 구구는 늘 update가 빌런이라는 명언을 남긴 바 있다. 내가 맞닥뜨린 상황과 몹시 부합했다. 그러나 이것은 내가 가장 사랑하는 ‘내 머리론 안 될 것 같은데’ 상황이었다.



ver 1. 서비스 레이어에서 차집합 구현

A, B, C가 속해있던 프로젝트의 멤버가 A, B, D로 수정되었다고 가정하자. 나는 project_member 테이블에서 C의 active 값을 N로 수정하고, D를 새로이 추가해야 한다. 이를 위해 서비스레이어에서 차집합을 구현하기로 했다.

@Transactional
@Override
public boolean modifyProject(Map<String, Object> map) {
		boolean result = false;
        
        int row = projectMapper.updateProject(map);
        
        ////// projectMember 수정 시작
		log.debug(TeamColor.CSK + "projectMember 수정 시작");
		
        // 프로젝트 멤버에 변동 사항이 없을 경우 프로젝트멤버 테이블 업데이트 X
		if("".equals(tmp)) {
			return true;
		}
        
		// 문자열로 받은 프로젝트 멤버 수정 정보를 배열로 변환
		String[] tmpArr = tmp.split(",");
		//	tmpArr를 List로 변환			1) 리스트로 변환		2) 스트림	3) 형변환			4) 최종연산
		List<Integer> newProjectMemberList = Arrays.asList(tmpArr).stream().map(Integer::parseInt).collect(Collectors.toList());
		log.debug(TeamColor.CSK + "newProjectMemberList: " + newProjectMemberList);

		// map에서 뽑아낸 projectNo를 가공
		int projectNo = Integer.parseInt(String.valueOf(map.get("projectNo")));

		// 수정 전 프로젝트 멤버리스트
		List<Map<String, Object>> list = projectMemberMapper.selectProjectMemberList(projectNo);

		// 대입해서 복사하면 얕은 복사 -> 복사한 객체가 변경되면 기존 객체도 변경됨 -> 깊은 복사 필요
		List<Integer> deleteProjectMemberList = new ArrayList<>(); // workMemberNo만 추출하여 담을 list
		List<Integer> prevProjectMemberList = new ArrayList<>(); // 깊은 복사를 위한 list

		for(Map<String, Object> m : list) {
			// 기존 프로젝트 멤버 리스트에서 workMemberNo만 추출, 
			// 메소드 실행 후 데이터 유실을 막기 위해 두 개의 리스트에 저장
			deleteProjectMemberList.add((int)m.get("workMemberNo"));
			prevProjectMemberList.add((int)m.get("workMemberNo"));
		}

		log.debug(TeamColor.CSK + "prevProjectMemberList: " + prevProjectMemberList);

		deleteProjectMemberList.removeAll(newProjectMemberList); // 차집합 - 프로젝트에서 삭제된 멤버
		newProjectMemberList.removeAll(prevProjectMemberList); // 차집합 - 프로젝트에 새로 추가된 멤버

		log.debug(TeamColor.CSK + "삭제할 멤버: " + deleteProjectMemberList);
		log.debug(TeamColor.CSK + "추가할 멤버: " + newProjectMemberList);
        
        // vo 세팅
		ProjectMember projectMember = new ProjectMember();
        projectMember.setProjectNo(projectNo);

		// 프로젝트 멤버의 active 값을 N으로
		for(int workMemberNo : deleteProjectMemberList) {
			projectMember.setWorkMemberNo(workMemberNo); // 해당 멤버의 workMemberNo 세팅
			projectMemberMapper.updateProjectMemberActive(projectMember);
			// update project_member set active = 'N' where work_member_no = #{workMemberNo} and project_no = #{projectNo};
		}
        
        // 프로젝트에 새롭게 추가 메소드
		for(int workMemberNo : newProjectMemberList) {
			projectMember.setWorkMemberNo(workMemberNo); // 해당 멤버의 workMemberNo 세팅
			projectMemberMapper.insertProjectMember(projectMember);
		}

		result = true;
		return result;

고군분투의 흔적...이다. 나름대로 자랑스럽다.

그러나 차집합을 굳이 서비스 레이어에서 구할 필요가 없었다. 또한 이 시점에서 나는 프로젝트 수정을 동기 방식으로 진행하고 있었던 탓에 구성원 한 명을 수정할 때마다 새로운 멤버리스트를 받아오기 위해 리로드를 해야했다. 그건 너무나... 멋지지 않았다.

ver 2. 앞단에서 차집합 구현, 비동기 수정 메소드로 전환

이에 차집합 구현부를 자바스크립트 메소드로 보내고, RestController를 통해 비동기 방식으로 바뀐 데이터를 받아오기로 결정하였다.

// 프로젝트 수정폼을 띄우는 ajax
 
 $(document).ready(function(){
    	let prevProjectManagerArr = new Array(); // 기존 관리자들의 번호를 저장해놓을 배열
		let prevProjectMemberArr = new Array(); // 기존 멤버들의 번호를 저장해놓을 배열
		let projectKeep = null;
		let prevProjectName = "";

    	$.ajax({
    		type : 'get',
    		url : '/member/restModifyProject',
    		data : {projectNo : $("#projectNo").val()},
    		success : function(json){
				console.log(json);
    			$(json).each(function(index, item){
    				$('#projectName').val(item.project.projectName);
    				prevProjectName = item.project.projectName;
    				$('#projectExpl').val(item.project.projectExpl);
    				$('#projectAuth').val(item.project.projectAuth);
    				$('#date1').val(item.project.projectStart);
    				$('#date2').val(item.project.projectDeadline);
    				$('#date3').val(item.project.projectEnd);
    				projectKeep = item.project.projectKeep;
					
    				// 프로젝트 멤버 리스트 반복문
    				for(let i = 0; i < item.projectMemberList.length; i++){
						if(item.projectMemberList[i].projectMemberAuth == null){
							$("#projectMemberList").append("<option value='" + item.projectMemberList[i].workMemberNo + "'>" + item.projectMemberList[i].workMemberName + "</option>");
							$("#projectManagerList").append("<option value='" + item.projectMemberList[i].workMemberNo + "'>" + item.projectMemberList[i].workMemberName + "</option>");
						} else if(item.projectMemberList[i].projectMemberAuth == 'Y'){
							$("#projectManagerList").append("<option value='" + item.projectMemberList[i].workMemberNo + "' selected>" + item.projectMemberList[i].workMemberName + "</option>");
							prevProjectManagerArr.push(String(item.projectMemberList[i].workMemberNo));
						} else {
							$("#projectMemberList").append("<option value='" + item.projectMemberList[i].workMemberNo + "'selected>" + item.projectMemberList[i].workMemberName + "</option>");
							prevProjectMemberArr.push(String(item.projectMemberList[i].workMemberNo));
						}
					}
					
		// ... 중간 생략 ...
                  
		// 프로젝트 관리자
		$("#projectManagerList").change(function(){
			
			if($("#projectManagerList").val() == ""){
				alert("최소 한 명의 프로젝트 관리자가 필요합니다.");
				return;
			}
			
			const select = $("#projectManagerList").val();
			const newManager = $(select).not(prevProjectManagerArr).get();
			const deleteManager = $(prevProjectManagerArr).not(select).get();

          // boolean -> true면 매니저 delete, false면 매니저 update
			const inOrOut = newManager.length == 0;
			console.log(inOrOut ? "delete" : "update");
			
			$.ajax({
				type : 'put',
				url : '/member/modifyMember',
				data : {projectNo : $("#projectNo").val(),
						workMemberNo: (inOrOut) ? deleteManager[0] : newManager[0],
						projectMemberAuth: "Y",
						active : (inOrOut) ? "N" : "Y"},
				success : function(json){
					prevProjectManagerArr = new Array(); // 관리자 배열 초기화
						
					$("#projectMemberList").empty();
					$("#projectManagerList").empty();
					
					$(json).each(function(index, item){
						if(item.projectMemberAuth == null){
							$("#projectMemberList").append("<option value='" + item.workMemberNo + "'>" + item.workMemberName + "</option>");
							$("#projectManagerList").append("<option value='" + item.workMemberNo + "'>" + item.workMemberName + "</option>");
						} else if(item.projectMemberAuth == 'Y'){
							$("#projectManagerList").append("<option value='" + item.workMemberNo + "' selected>" + item.workMemberName + "</option>");
							prevProjectManagerArr.push(String(item.workMemberNo));
						} else {
							$("#projectMemberList").append("<option value='" + item.workMemberNo + "'selected>" + item.workMemberName + "</option>");
						}
					})
				}, // end for success call back function
				error : function(error){
					console.log("error!");
				}
			}); // end 
		}); // end for projectManager change
		
		// 프로젝트 멤버 메소드도 로직 동일

    })

이에 프로젝트 멤버 수정 메소드의 로직도 함께 개선하였다.

	@Override
	public List<Map<String, Object>> modifyProjectMember(int workNo, ProjectMember projectMember) {
		// 일단 UPDATE
		int row = projectMemberMapper.updateProjectMember(projectMember);
		
		if(row == 0) {
			// projectNo와 workMemberNo 조건으로 업데이트할 항목이 없을 때,
			// 즉 미리 속해있지 않은 멤버라서 INSERT의 대상일 때
			projectMemberMapper.insertProjectMember(projectMember);
		}
		
		return projectMemberMapper.selectPossibleProjectMemberListByWorkNoAndProjectNo(workNo, (int)projectMember.getProjectNo());
	}

이미 자바스크립트로 해당 멤버의 workMemberNo와 추가/삭제 여부를 받아왔으므로, 무작정 UPDATE를 실행한 뒤 영향받은 row가 없으면 INSERT를 진행한다. Safari 설계 과정에서 우리는 프로젝트에서 프로젝트 멤버가 삭제되어도 그가 업로드한 자료를 보존하기 위해 멤버를 삭제하지 않고 active 컬럼의 값(Y/N)으로 포함 여부를 구분하기로 결정했다. UPDATE를 우선 실행하였을 때의 장점은 다음과 같다.

1) 삭제했던 멤버를 다시 추가했을 때 과거 자신이 만든 자료의 권한을 되돌려줄 수 있고,
2) 무의미한 데이터의 증가를 막을 수 있다. (이 방법이 아닐 시 만약 'A'라는 멤버가 '추가 - 삭제 - 재추가 - 삭제 - 재추가' 과정을 거친다면, 한 프로젝트의 같은 멤버에 대해 다섯 줄의 데이터가 쌓이게 된다)




cf) 프로젝트 멤버리스트 쿼리

워크스페이스에 속한 멤버리스트를 받아오는 SELECT문에 프로젝트에 이미 속한 멤버 리스트를 반환하는 SELECT문을 LEFT JOIN 한다. 이후 projectMemberAuth가 'Y'면 프로젝트 관리자에 <option selected>로, 'N'이면 프로젝트 멤버에 <option selected>로, null이면 둘 모두에 <option>으로 append한다.

SELECT
	w.work_member_no workMemberNo
	, w.work_member_name workMemberName
	, m.project_no projectNo
	, m.project_member_auth projectMemberAuth
FROM
	workspace_member w
LEFT JOIN
	(SELECT 
		work_member_no
		, project_no
		, project_member_auth 
	FROM
		project_member 
	WHERE 
		project_no = #{projectNo} AND active = 'Y') m
ON 
	w.work_member_no = m.work_member_no
WHERE 
	w.work_no = #{workNo}
AND
	w.active = 'Y'



2편으로 이어집니다.

0개의 댓글