Spring/스프링 프로젝트

테이블 부분 무한스크롤 구현

어굴애 2021. 7. 22. 03:31

 

해당 글은 스프링 프로젝트를 처음으로 구현하는 입장에서

어떻게 각각의 기능들을 구현했는지를 정리하기 위해 작성한 글로

잘못된 정보나 불필요하거나 비효율적인 코드를 포함할 수 있음

 

 

 

 Point 

1. 페이지를 리로드하지 않고 ajax로 데이터를 추가로 가져온 뒤 기존의 태그에 붙인다.

    무한스크롤은 여러 가지 구현 방식이 있겠지만 내가 시도한 방법은 스크롤이 최하단에 닿을 때,

    숨겨진 페이지 번호를 증가시켜 해당 페이지 번호를 파라미터로 넘겨주어 그에 맞는 데이터를 가져온다.

    이때, 페이지를 리로드하는 mvc방식이 아니라, ajax를 통해 가져온 데이터만 부분적으로 갱신해 추가한다.

    한번에 데이터를 불러오지 않고 무한스크롤을 구현하는 이유는, 데이터가 많아졌을 때 로딩하는 시간을 줄이기 위해서다.

 

    무한스크롤 페이징 처리 방식을 사용하는 예를 들어보자.

    총 470개의 데이터가 존재한다고 가정한다.

    50개 단위로 데이터를 가져오면 총 10페이지가 존재하고 마지막 페이지에는 20개의 데이터만 존재한다.

    해당 페이지가 로드되었을 때 먼저 50개의 데이터를 가져온 뒤 스크롤이 최하단에 닿으면 자바스크립트로 인식한다.

    1~50번의 자료를 건너뛰고 그 다음 페이지 51~100번의 자료를 가져와 tbody html 태그 하단에 붙인다.

    마지막 페이지 20개의 데이터까지 모두 불러온다면 스크롤이 바닥에 닿아도 아무런 이벤트가 발생하지 않는다.

 

2. ajax를 사용하여 서버에 페이지번호를 넘겨주면, 실질적인 페이징을 위해 필요한 데이터 연산은 Page VO에서 담당한다.

    사실 서버단의 코드는 Ajax로 넘어온 데이터를 변환하는 부분을 제외하고는 일반적인 페이징 방식을 채택할 때와 차이가 없다. 

 

 

    서버단의 코드 설명은 이전 포스팅으로 대신함

2021.07.21 - [Spring/스프링 프로젝트] - MySQL이용 시의 페이징2 [VO를 통한 구현]

 

 

3. view단(jsp, javascript 파일)에 무한스크롤을 구현하기 위한 코드가 집중된다.

    내가 무한스크롤을 구현한 과정은 다음과 같이 설명할 수 있는데, 4번을 제외한 단계들은 클라이언트 쪽 코드에 해당한다.

 

4. 전체 무한스크롤이 아닌 테이블 부분 무한스크롤을 구현할 때,

     thead(컬럼)는 고정한 채로 tbody(데이터) 내부에 스크롤이 생기도록 할 필요가 있다.

    이를 위해 css에 몇 가지 설정을 해야 한다.

    

 무한 스크롤 구현 단계 

① View : js제어를 위한 view(jsp&css)파일 셋팅

② JS : 테이블 영역 스크롤 이벤트 

JS : ajax로 페이지 넘겨주기

④ controller - service - dao - mapper : ajax로 파라미터 받아 페이지 객체 생성 후 새로운 리스트 가져오기

⑤ JS : ajax 성공 후 리턴되는 데이터를 통해 페이징을 처리하는 html코드 작성

⑥ View : thead 고정에 따른 너비 맞추기 (jsp,css 파일 수정)

 

 

 


각각의 코드를 제시하기 앞서, 해당 코드는 실시간 다중 검색을 위한 코드를 포함하고 있다는 점을 명시한다. 

jsp파일에서 search-tr이라는 클래스 이름을 가진 tr부분, js에서 search란 이름으로 데이터를 매핑하고 보내주는 부분이 그렇다.

이에 대한 설명은 아래 링크의 글에서 포함하고 있다.

 

2021.07.10 - [Spring/스프링 프로젝트] - 스프링에서 Ajax를 통해 JSON 데이터 주고받기

 

 

① View : js제어 및 테이블 형태를 고정하기 위한 view(jsp, css)파일 셋팅

<table class="tbl-ex emp-tbl">
	<summary>총 직원수 : ${cnt} &nbsp; <span id="totRows"></span></summary>
	<thead>
    <!--
		<tr class="search-tr">
			<th class="gname"><input type="text" class="search" id="org_groupname" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="tname"><input type="text" class="search" id="org_teamname" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="eid"><input type="text" class="search" id="emp_id" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="ename"><input type="text" class="search" id="emp_name" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="eposi"><input type="text" class="search" id="emp_posi" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="etel"><input type="text" class="search" id="emp_tel" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="email"><input type="text" class="search" id="emp_email" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="elevel"><input type="text" class="search" id="emp_level" onkeyup="getMap(1)" placeholder="admin/member"></th>
		</tr>
        -->
		<tr class="title-tr">
			<th class="gname">본부</th>
			<th class="tname">부서</th>
			<th class="eid">사번</th>
			<th class="ename">이름</th>
			<th class="eposi">직급</th>
			<th class="etel">내선번호</th>
			<th class="email">이메일</th>
			<th class="elevel">액세스권한</th>
		</tr>
	</thead>
	<tbody id="ajax">

	</tbody>
</table>

 

@charset "UTF-8";
table {	display: block;}
tbody tr { height: 5vh; }
thead {	display: inline-block;	width: 62vw;	padding-right:18px;	box-sizing: border-box; }
tbody {	max-height: 500px;	display: inline-block; 	width: 62vw; overflow-y: auto; }
td, th { box-sizing:border-box;}

/*
.gname, .tname, .eid, .ename, .eposi, .elevel{	width:10.5% !important; }
.etel{	width:13.33% !important; }
.email{	width:24% !importatnt; }
input#emp_level { font-size: 12px; width: 100px; }

*/

 

1. JSP : tbody에 id를 부여하고, 내용은 비워둔다.

    tbody 내부 html 코드는 ajax를 통해 데이터를 가져온 후에 제이쿼리를 이용해 작성할 예정이다.

    tbody 요소를 선택해 그 안의 html 코드를 채워주기 위해 id를 미리 부여한다.

 

 

2. CSS : thead 고정과 tbody 영역 내부 스크롤 만들기

  • thead 고정
    • table의 display : block
    • thead의 display : inline-block 혹은 block
    • tbody의 display : inline-block 혹은 block
    • 정확히는 모르겠지만 포인트는 tbody의 display를 block으로 지정하는 것이다.
      내 경우엔 thead를 굳이 블럭태그로 만들어주지 않아도 헤더가 고정되고 내부 스크롤이 생겨나지만,
      구글링하면 thead, tbody 모두를 블럭태그로 만들기를 권장하고 있다.
  • tbody 스크롤 자동생성
    • max-height: 500px
      overflow-y: auto
    • tbody의 최대 높이를 지정해 content의 길이가 이보다 크다면 scroll이 생기도록 overflow-y를 auto로 설정한다.

 

참고1 : https://blogpack.tistory.com/810

참고2

 

 

 

 

mvc를 전혀 사용하지 않고 ajax를 통해 초기 데이터를 뿌려주려고 할 때,
사실 서버(controller, service, dao, mapper)의  코드까지 완성되어야 스크롤이 제대로 생기고 있는지,
스크롤을 내려도 헤더가 고정되고 있는지 확인가능하다.

그렇지 않고는 일단은 정적인 임의의 값들을 td안에 인위적으로 넣어준 후 확인해야 할 것이다.

 

 


 

② JS : 테이블 영역 스크롤 이벤트 [JavaScript 상단부]

 

0. var page = 1; var last; var html;
1. $(document).ready(function () { $("#ajax").on('scroll',function() {

2.	 if (page == last) return;
	 
3. 	var innerHeight = $(this).innerHeight(); //tbody의 padding값 포함한 높이 (border, margin은 미포함)
4.	 var scroll = $(this).scrollTop() + $(this).innerHeight(); //스크롤을 내린만큼의 길이 + tbody 높이 
5.	 var height = $(this)[0].scrollHeight; //tbody 컨텐츠의 전체 길이(스크롤 전체 길이)

6.	 if (scroll >= height) { //scroll = height 시점은 스크롤이 바닥에 닿았을 때
7.	 page++;
8.	 getMap(page);
	}
	});
});

 

 

순수 자바스크립트만을 사용해서도 구현 가능하지만, ajax 사용 시에 제이쿼리 코드가 들어가기 때문에 제이쿼리로 통일해주었다.

 

0. page(현재 페이지), last(마지막 페이지), html(tbody안에 들어갈 html태그) 변수는 가장 먼저 전역변수로 선언

1. 문서가 준비되었을 때, ajax란 id를 가진 요소(tbody)에 scroll이 움직인다면 익명구현메소드를 실행한다.

2. 현재 페이지가 마지막 페이지라면 스크롤에 따른 이벤트가 발생하지 않는다. (더이상 불러올 자료가 없기 때문에)

3. innerHeight : padding값을 포함한 요소의 높이로, border과 margin은 제외된다.

4. scrollTop : 스크롤을 내릴때마다 증가한다.

                          스크롤을 바닥까지 내렸을 때 높이와 해당 요소의 높이를 더하면 전체 컨텐츠의 길이와 같다.

5. [0].scrollHeight : 요소가 가진 content의 길이이자, 스크롤의 전체 높이라고 할 수 있다.

6. 스크롤이 바닥에  닿았을 때 조건 처리

    사실 이걸 바닥에 닿기 전에 (즉, 바닥에서 5~60px 남았을 때) 미리 로드해 사용자가 기다리지 않게 수정하고 싶었는데..

    콘솔에 찍어 테스트해봤을 때 스크롤을 한번만 내려도 내부적으로 scrollTop은 서서히 증가하기 때문에

    스크롤 이동 한번에 해당 조건문이 여러번 실행되어 페이지가 계속해서 증가하는 문제를 해결하지 못했다.

    일단은 50개 데이터 가져오는 것은 아주 찰나에 불과하기 때문에 그냥 스크롤이 최하단에 닿았을 때로 두었다.

7. 페이지 번호를 증가시킨다.

8. ajax를 통해 새로운 페이지에 해당하는 데이터를 새로 가져오는 getMap을 실행한다.

    이때 요청하려는 페이지 번호를 파라미터로 넘겨준다.

 

 

innerHeight, scrollTop, scrollHeight에 대한 이해를 돕기 위해 이미지를 첨부한다.

사실 티스토리 작성 화면을 캡처했기 때문에 이미지에서 보이는 스크롤은 전체 스크롤로 조금 다른 개념이지만,

글쓰기 화면을 하나의 div 혹은 테이블 내부의 부분적인 요소로 가정하고 이해하도록 하자.

화면을 띄웠을 때 보이는 고정 높이InnerHeight에 해당한다.

스크롤을 하나도 내리지 않아도 해당 높이 만큼은 보여지고 있다는 걸 염두에 두고, 스크롤을 바닥까지 이동시켰을 때

스크롤이 이동한 거리(ScrollTop)스크롤의 전체 길이(ScrollHeight)에서 InnerHeight을 뺀 값과 일치한다.

 

이를 이용하여 ScrollTop+InnerHeight이 ScrollHeight과 같아지는 지점에서 사용자가 데이터를 모두 읽었다고 전제하여

그 뒤의 데이터를 새로 불러오기 위한 getMap 메소드를 실행하는 것이다.

 

 

 

 

 

 


 

 

③ JS : ajax로 요청하려는 페이지 번호 controller로 넘겨주기 [JavaScript 하단부]

 

function getMap(p){
	/*
	let search = $('.search');
	search = [];
	
	$('.search').each(function(){
		let s ={
			field : $(this).attr('id'),
			keyword : $(this).val()
		};
		search.push(s);
	});
	var jsonData = JSON.stringify(search); */
	$.ajax({
		url : "searchlist",
		type:"post",
		data : {
			"search":jsonData,
			"page": p
			},
		success : function(data){
			//반환되는 데이터로 html태그추가
			}
			$("#ajax").html(html);
			
		},
		error : function(xhr){
			console.log(xhr.status);
			console.log(xhr.responseText);
		}
	});
}


window.onload = function(){
    getMap();
}

 

ajax 기술을 통해 서버쪽으로 데이터를 전달한다.

- ajax는 제이쿼리 기반으로 작동하기 때문에 제이쿼리를 사용하기 위한 link는 jsp 헤드에 포함되어야 한다

- ajax를 통해 데이터를 전송하기 위해 주입되어야할 의존성이나 설정들은 앞서 제시한 글에서 다루고 있다.

 

1. url : 컨트롤러에 매핑할 url과 일치시킨다. 상대경로를 이용했다.

2. type : 데이터를 안정적으로 암호화해서 전송하기 위해 post방식을 사용

3. data : p라는 이름으로 전달받은 파라미터 값을 "page"란 이름의 변수에 담아 ajax로 전송해줄 것이다.

  • p는 단순 정수값이기 때문에 변환을 거치지 않고 제이슨 문자열로 바로 전달할 수 있다.
  • search는 주석처리한 코드에서 만들어진 객체로, 다중 검색에 필요한 값들을 배열에 담아준 뒤 json 문자열로 변환했다.

4. success : ajax를 통해 에러 없이 데이터 전송이 이루어졌을 때 수행할 코드를 적는다. 아래에서 자세히 다룰 예정

5. error : 중간에 어떤 이유로든 에러가 발생한다면 수행해야할 코드를 작성한다.

  • xhr.status : 에러코드
  • xhr.responseText : 자세한 에러 내용

window.onload =function() { getMap(); }

 

브라우저가 해당 페이지를 처음 load했을 때 기본 데이터(1페이지의 데이터)를 뿌려주기 위해 바로 getMap 메소드를 실행시킨다.

이때 p값을 전달하지 않고 있는데, 컨트롤러에서 p가 빈값을 가진다면 1페이지로 기본값을 설정하도록 처리하고 있다.

 

 


 

④ controller - PageVO - service - dao - mapper
: ajax로 파라미터 받아 페이지 객체 생성 후 새로운 리스트 가져오기

 

service, page객체, dao, mapper 및 controller에 대한 자세한 설명은 아래 글에서 이미 다루고 있기 때문에 생략한다.

 

2021.07.21 - [Spring/스프링 프로젝트] - MySQL이용 시의 페이징2 [VO를 통한 구현]

 

 

@RestController("MemberInfoController") // @ResponseBody를 하지 않아도 알아서 제이슨 데이터타입으로 변환(제이슨 문자열)
@RequestMapping("/admin/empManagement/")
public class MemberInfoController {
	
	@Autowired
	private MemberService service;
	
	//ResponseBody -- 바로 제이슨 문자열로 받고 보내줌
	@PostMapping("searchlist")
	public Map<String,Object> searchResult(@RequestParam(value = "search") String search, @RequestParam(value="page", defaultValue = "1") int p) throws JsonMappingException, JsonProcessingException {
		/*
        ObjectMapper mapper = new ObjectMapper();
		List<Map<String, String>> mapList = mapper.readValue(search, new TypeReference<List<Map<String, String>>>(){});
        */
		double totRows = service.cntRows(mapList);
		Page page = new Page(p, totRows);
		page.setScalePerPage(13);
		List<MemberList> member =service.searchList(mapList, page);
		Map<String,Object> data = new HashMap();
		data.put("member", member);
		data.put("totRows",totRows);
		data.put("lastNum", page.getLastNum());
        return data;
	}

 

추가된 내용은

@RequestParam을 이용해서 page라는 이름의 파라미터 변수를 받아 int page에 넣어주고 있다는 점이다.

사실 이 내용도 이미 이전 글에서 다루고 있는데, 그때는 뷰단에서 get요청으로 넘어온 파라미터였고,

여기서는 js에서 post요청으로 넘어온 파라미터라는 점이다.

컨트롤러에서 단순 [숫자로 변환 가능한] 문자열을 받는 방식은 똑같다.

 

 

무한스크롤을 구현할 때에는 사실 페이징과 관련하여 필수적인 요소는 마지막 페이지 번호와 현재 페이지 번호 뿐이다. 

현재 페이지 번호는 이미 js측에서 결정되었기 때문에 새로 값을 넘겨줄 필요가 없고, 

마지막 페이지 번호(lastNum)는 Page객체를 생성할 때 연산된 값으로 ajax를 통해 그대로 넘겨준다.

총 자료의 개수(totRows)는 사용자에게 보여줄 부가적인 정보로, 넘겨주지 않아도 무방하다.

요청하는 페이지(다음 페이지)에 해당하는 자료의 리스트는 member 이라는 이름의 변수에 담아 주었다.

 

 

주목할 점은 다시 ajax를 통해 클라이언트 쪽으로 여러 개의 파라미터를 넘겨주기 위해서,

파라미터들을 하나의 변수(객체 혹은 컬렉션, 배열)에 담아주어야한다는 점이다.

여기서는 각각의 key값으로 원하는 데이터를 바로 꺼내기 위해 HashMap을 사용하고 있다.

 

 

 

 


 

⑤ JS : ajax 성공 후 리턴되는 데이터를 통해 페이징을 처리하는 html코드 작성

 

function getMap(p){
	/*
	let search = $('.search');
	search = [];
	
	$('.search').each(function(){
		let s ={
			field : $(this).attr('id'),
			keyword : $(this).val()
		};
		search.push(s);
	});
	var jsonData = JSON.stringify(search); */
	$.ajax({
		url : "searchlist",
		type:"post",
//		dateType:'json', @ResponseBody를 담아놓았기 때문에 자동 처리
		data : {
			"search":jsonData,
			"page": p
			},
		success : function(data){
			var member = data.member;
			var cnt = data.totRows;
			var cntRows = $('#totRows');
			last = data.lastNum;
			if(p==1) {html=''; page=1; cntRows.html('검색 결과 : ' +cnt+ '건');}
			for(var i=0; i<member.length; i++){
				html += "<tr>"+
					"<td class='gname'>"+member[i].org_groupname+"</td>"+
					"<td class='tname'>"+member[i].emp_dept+"</td>" +
					"<td class='eid'>"+member[i].emp_id+"</td>" +
					"<td class='ename'>"+member[i].emp_name+"</td>" +
					"<td class='eposi'>"+member[i].emp_posi+"</td>" +
					"<td class='etel'>"+member[i].emp_tel+"</td>" +
					"<td class='email'>"+member[i].emp_email+"</td>";
				if(member[i].emp_level == "ROLE_ADMIN")
					html +="<td class='elevel'>관리자</td>";
				if(member[i].emp_level == "ROLE_MEMBER")
					html +="<td class='elevel'>직원</td>";
					html+="</tr>";
			}
			$("#ajax").html(html);
			
		},
		error : function(xhr){
			console.log(xhr.status);
			console.log(xhr.responseText);
//			alert("Error code : " + xhr.status);
		}
	});
}


window.onload = function(){
    getMap();
}

 

여러가지 파라미터들을 HashMap에 담아 리턴해주었고, 이를 data란 이름으로 받고 있다 : function의 파라미터

자바스크립트에서 해쉬맵.key값으로 매핑된 value를 얻을 수 있다.

 

var member = data.member;          member -> 쿼리 실행을 통해 얻은 자료 list이다.
var cnt = data.totRows;                      totRows -> 전체 자료의 개수로, 사용자에게 추가정보로 제공하기 위해 받아왔다.
var cntRows = $('#totRows');            jsp파일에서 테이블 summary안의 span태그를 선택해 cntRows라는 변수에 저장
last = data.lastNum;                            lastNum ->마지막 페이지번호로, (2번 js 작성 참고)
                                                                    현재 페이지가 마지막 페이지에 해당하면 더이상 getMap을 호출하지 않기 위해 필요

if(p==1) {                                                  사용자가 검색을 시도할때, p가 1이라는 값을 가지게 된다.  (무시해도 됨)
html=''; page=1;                                    검색 결과 데이터가 새롭게 리턴되면 페이징 또한 초기화되어야 하므로 필요한 코드
cntRows.html('검색 결과 : ' +cnt+ '건');     span태그 내부에  검색 결과 자료의 수를 알려주는 텍스트 삽입

}       

 

JavaScrirpt 최상단에서 html 변수를 선언했다. 선언 당시 초기화하지 않았기 때문에 해당 변수는 빈값을 가질 것이다.

이제 html변수에 추가해주면서 데이터를 뿌린다.

 

ajax를 통해 가져온 데이터의 리스트를 반복문을 돌며 뿌려야하는데,

일단은 html 태그 형식으로 작성하되, 문자열이 되게 한다.

 

가장 먼저 페이지가 로드되었을 때 다음 코드(2단계에서 설명함)에 의해 getMap이 실행되는데,

 

window.onload =function() { getMap(); }

 

이때 빈값이었던 html변수에 1페이지의 데이터를 불러와 다음과 같이 추가한다.

 

cf.

member은 자바에서 넘겨줄 당시 List<HashMap<String, Object>>의 데이터 타입을 가지는데,

클라이언트에서 뽑아 쓸때 배열처럼 인덱스를 넣고, 그 안의 value는 각각의 key값으로 꺼낼 수 있다.

 

 

 

for(var i=0; i<member.length; i++){ 
html += "<tr>"+
     "<td class='gname'>"+member[i].org_groupname+"</td>"+
     "<td class='tname'>"+member[i].emp_dept+"</td>" +
     "<td class='eid'>"+member[i].emp_id+"</td>" +
     "<td class='ename'>"+member[i].emp_name+"</td>" +
     "<td class='eposi'>"+member[i].emp_posi+"</td>" +
     "<td class='etel'>"+member[i].emp_tel+"</td>" +
     "<td class='email'>"+member[i].emp_email+"</td>";
     if(member[i].emp_level == "ROLE_ADMIN")
     html +="<td class='elevel'>관리자</td>";
     if(member[i].emp_level == "ROLE_MEMBER")
     html +="<td class='elevel'>직원</td>";
     html+="</tr>";
}

 

문자열 형태로 작성된 html태그를 html이라는 제이쿼리 메소드를 통해 선택한 요소(비워두었던 tbody) 내부에 넣어 뷰단을 완성한다.

$("#ajax").html(html);

 

[ 2단계에서 작성한 JS 참고 ]

스크롤이 바닥에 닿으면 스크롤 이벤트에 의해 페이지 번호를 저장하는 변수가 증가하고(page++;)

getMap(page)이 실행된 후 위의 반복문을 거쳐 html태그가 점점 추가되는 방식으로 작동한다.

 

 


 

⑥ View : thead 고정에 따른 너비 맞추기 (jsp,css 파일 수정)

데이터를 불러와 뷰단에 뿌리는 데 성공했다면, 위의 이미지에서처럼 이상한 점이 하나 보인다.

상단의 컬럼명과 데이터들 간의 정렬이 맞지 않다.

이는 thead가 고정된 후 thead와 tbody는 각각의 독립된 영역으로 분리돼 th와 td가 각기 다른 너비를 갖기 때문이다.

 

td의 너비는 기본적으로 content에 따라 잡히는데,

tbody와 thead에 별다른 설정을 하지 않았을 때는 th가 td의 너비를 따라갔지만

thead가 고정되면서 각 컬럼별로 th와 td의 너비를 일치시켜야할 필요가 생겼다.

 

<table class="tbl-ex emp-tbl">
	<summary>총 직원수 : ${cnt} &nbsp; <span id="totRows"></span></summary>
	<thead>
    <!--
		<tr class="search-tr">
			<th class="gname"><input type="text" class="search" id="org_groupname" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="tname"><input type="text" class="search" id="org_teamname" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="eid"><input type="text" class="search" id="emp_id" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="ename"><input type="text" class="search" id="emp_name" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="eposi"><input type="text" class="search" id="emp_posi" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="etel"><input type="text" class="search" id="emp_tel" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="email"><input type="text" class="search" id="emp_email" onkeyup="getMap(1)" placeholder="검색.."></th>
			<th class="elevel"><input type="text" class="search" id="emp_level" onkeyup="getMap(1)" placeholder="admin/member"></th>
		</tr>
        -->
		<tr class="title-tr">
			<th class="gname">본부</th>
			<th class="tname">부서</th>
			<th class="eid">사번</th>
			<th class="ename">이름</th>
			<th class="eposi">직급</th>
			<th class="etel">내선번호</th>
			<th class="email">이메일</th>
			<th class="elevel">액세스권한</th>
		</tr>
	</thead>
	<tbody id="ajax">

	</tbody>
</table>

 

먼저 jsp 파일을 다시보자.

각각의 th들이 다다른 이름의 class를 가지고 있다.

 

for(var i=0; i<member.length; i++){ 
html += "<tr>"+
     "<td class='gname'>"+member[i].org_groupname+"</td>"+
     "<td class='tname'>"+member[i].emp_dept+"</td>" +
     "<td class='eid'>"+member[i].emp_id+"</td>" +
     "<td class='ename'>"+member[i].emp_name+"</td>" +
     "<td class='eposi'>"+member[i].emp_posi+"</td>" +
     "<td class='etel'>"+member[i].emp_tel+"</td>" +
     "<td class='email'>"+member[i].emp_email+"</td>";
     if(member[i].emp_level == "ROLE_ADMIN")
     html +="<td class='elevel'>관리자</td>";
     if(member[i].emp_level == "ROLE_MEMBER")
     html +="<td class='elevel'>직원</td>";
     html+="</tr>";
}

 

JS파일을 다시 본다면 마찬가지로 html 태그를 만드는 반복문에서 td가 각기 다른 이름의 class를 가지고 있다.

즉 th와 td를 열에 따라 같은 너비를 주기 위해 클래스이름을 부여하고 있다.

 

 

@charset "UTF-8";
table {	display: block;}
tbody tr { height: 5vh; }
thead {	display: inline-block;	width: 62vw;	padding-right:18px;	box-sizing: border-box; }
tbody {	max-height: 500px;	display: inline-block; 	width: 62vw; overflow-y: auto; }
td, th { box-sizing:border-box;}


.gname, .tname, .eid, .ename, .eposi, .elevel{	width:10.5% !important; }
.etel{	width:13.33% !important; }
.email{	width:24% !importatnt; }

input#emp_level { font-size: 12px; width: 100px; }

 

CSS파일을 마지막으로 수정한다.

 

1. 클래스 이름별로 너비를 설정한다.

데이터가 이미 불러와진 상태이기 때문에 각각의 컬럼이 어느 정도의 너비를 가져야할지 쉽게 조정할 수 있을 것이다.

 

2. tbody와 thead의 너비값을 미세 조정

tbody의 우측에는 스크롤이 존재하고, thead에는 존재하지 않기 때문에

tbody와 thead에 같은 너비값을 주었음에도 thead의 컬럼들이 살짝 우측으로 밀려있을 수 있다.

 

width를 어떻게 주는지에 따라 다른 방식으로도 해결 가능하겠지만,

나는 그냥 box-sizing을 border-box로 설정한 뒤 thead에는 스크롤의 너비만큼 padding-right을 설정했다.

 

귀찮아서 스크롤이 존재하지 않을 때의 조건처리는 생략했다.