| 강좌 목록 - 강좌 상세페이지 구현
- 오늘 수업에서는 강좌목록과 강좌 상세페이지를 구현했다.
- 방법은 카테고리 목록 및 상세페이지 구현을 할 때 쓴 MVC모델과 동일.
- MVC 모델 : HTTP request <--> Controller <--> Service <--> [JPA/MyBatis <--> DB]
- 새롭게 배운 내용만 별도로 정리하기로 했다.
구분 | 새롭게 배운 내용 | ||
Client | Back | ||
C | 강좌 등록 | 등록 및 수정 상태에 따른 버튼 노출 | 등록과 수정을 동시에 처리하기 (상태값 필요) |
R | 강좌 조회 | select - option 에 thymeleaf로 반복문 돌리기 | CollectionsUtil로 세련된 예외처리하기 MyBatis XML 공통영역 <sql id = "">로 빼기 |
U | 강좌 수정 | (리마인드 - @Transactional) | 스마트 에디터 사용하기 |
D | 강좌 삭제 | 목록 체크박스 전체/하나 선택 | 자바스크립트에서 List 문자열로 받아 파싱하기(생략) |
1. Back
(1) Controller에서 수정과 등록을 동시처리하기 - request.getRequestURI().contains("")
- 실제 현장에서는 매 요청사항 마다 별도의 페이지를 쓰지 않는다고 한다.
* 수정페이지와 등록페이지를 하나로 해결하면, 아무래도 디자인/퍼블/개발 작업이 더 줄거라 생각된다..
- 따라서 Controller에서도 하나의 메소드로 두 개의 다른 요청에 대한 처리를 해준다.
- 그러면 하나의 메소드 내에서, 어떻게 두 개의 다른 기능을 구분할 수 있을까?
>> 그건 클라이언트가 요청한 URI 주소에 특정 문자열이 있는지를 통해 할 수 있다.
>> HttpServletRequest의 reqeust.getRequestURI().contains() 를 쓰면 이를 구현할 수 있다.
- 이렇게 하게되면 아무래도 개발자 입장에서 좀 더 전체적으로 코드를 보며 이해하기 쉽지 않을까 싶다.
- 잘 기억해두자...!!
/* 강좌 등록 및 수정 */
@PostMapping(value = {"/admin/course/add.do", "/admin/course/edit.do"})
public String addSubmit(Model model, HttpServletRequest request,
CourseInput parameter) {
boolean editMode = request.getRequestURI().contains("/edit.do");
if (editMode) {
long id = parameter.getId();
CourseDto existCourse = courseService.getById(id);
if (existCourse == null) {
// error 처리
model.addAttribute("message", "강좌정보가 존재하지 않습니다.");
return "common/error";
}
boolean result = courseService.set(parameter);
} else {
boolean result = courseService.add(parameter);
}
return "redirect:/admin/course/list.do";
}
(2) CollectionsUtil를 통해 세련되게 List의 null, size==0 인 상황 처리하기
// 기존 방식
List<CourseDto> courses = courseService.selectList();
if (courses != null && courses.size() > 0){
// 처리
}
// CollectionUtils 방식
List<CourseDto> courses = courseService.selectList();
if (!CollectionUtils.isEmpty(courses)){
// 처리
}
※ DAO/ DTO/ VO에 대해서.. : 위에서 DTO란 말이 나왔는데 이 개념이 항상 헷갈려서 잠시 정리하고 가려한다.
- 출처 : https://gmlwjd9405.github.io/2018/12/25/difference-dao-dto-entity.html
내용 | 예시 | 특징 | |
DTO | 계층간 데이터 교환을 위한 객체 | MemberDto | - DB의 데이터를 Service/Controller로 보낼 때나 - Client -> Presentation Logic Tier로 데이터가 넘어올 때, Request/Response용으로 사용 - 로직을 갖지 않은 순수한 데이터 객체 - 주로 getter/setter만 갖는다. - 제약사항이나 정규식 패턴 등을 보통 여기서 걸어주며, 데이터 validation |
DAO | 실제로 DB에 접근하는 객체 | MemberRepository | - 실제로 DB에 접근하는 객체 - 가변적이다. |
VO | DAO와 동일하나, 불변인 객체 | - | - DAO와 동일 - read only (불변) 객체 |
Entity | 말 그대로 엔터티 그 자체 | Member | - 엔터티 그 자체 |
- 이전에 배운 수업에서는 Request와 Response를 전부 DTO로 묶어서 보내주었었는데,
이번에 배운 수업에서는 Request를 받을 때, XXXInput 또는 XXXParam이라는 명칭으로 별도로 받은 후,
DB에서 받은 내용을 Service나 Controller로 전달할 때 DTO를 썼었다.
- 어떤게 좋은 지는 현장에 나가봐야 알 것 같다.
다만, 두 방식 모두 DTO에서 팩토리 메소드를 사용해서 Entity를 Dto로 변환했고,
데이터를 생성할 때 builder를 사용해서 save했다.
- 이는 이펙티브 자바에서도 권장하는 내용이기도 하다.
(3) Class의 공통된 영역은 Base 클래스로 묶기
- 본 수업에서 눈여겨 보았던 점은 서로 연관은 있으나 다른 Member, Course Controller에 대해, Util Class를 사용한 영역만 별도로 BaseController로 빼주었다는 점이었다.
- 좀 신기하면서도 의아했는데, 가만 생각해보면, 이게 Util Class를 사용하는 부분만 뺀 거라 사실 스마트한 방식이라 생각이 들었다. Util 영역을 바꾸는게 나중에 쉬워질 것이기 때문에. 그런데 좀 더 고민은 해봐야할 것 같다. (아직도 내 맴은 상속이 좀 걸려서.. 이 부분이 조금 부자연스럽게 느껴진다.)
MemberController, CourseController의 목록 페이징 처리 영역을 BaseController로 처리 MemberParam, CourseParam (Http reqeust dao) |
(4) MyBatis의 Mapping XML 의 공통 영역 처리
- MyBatis에서 공통영역은 <sql id = "">로 묶어주고, 각 쿼리문에서 <include refid ="">로 가져올 수 있다.
- 신박하면서도 꼭 기억해야할 부분 같아 정리해두었다.
<sql id="selectListWhere">
// 공통 영역
</sql>
<select id="selectListCount" resultType="long">
select
count(*)
from course
where 1 = 1
<include refid="selectListWhere"></include>
</select>
2. Client
(1) 등록 및 수정 상태에 따른 버튼 노출
- 서버단에서 등록과 수정을 동시에 처리하면서, Response 값으로 등록/수정 상태값이 넘어왔다.
- 이에 따라, 강좌 상세페이지 하나로, [등록] [수정] 버튼을 다르게 노출하였는데,
결과적으로 수정페이지와 등록페이지가 하나로 해결되게 되었다.
<div class = "buttons">
<button th:if="${!editMode}" type = "submit">강좌 등록 하기</button>
<button th:if="${editMode}" type = "submit">강좌 수정 하기</button>
<a href = "/admin/course/list.do">목록으로</a>
</div>
(2) select - option 에 thymeleaf로 반복문 돌리기
- 이 부분은 나중에 다시 보기 위해 적어둔 내용이다.
- <tr> 안에 for문을 쓴 건 이해가 잘 되었지만, <option>의 경우에는 다소 생소했다.
- 잘 기억해두었다가 써먹어보자.
<select name = "categoryId" required>
<option value = "">카테고리 선택</option>
<option th:each="x : ${category}" th:value="${x.id}"
th:text="${x.categoryName}" th:selected="${detail.categoryId == x.id}"></option>
</select>
(3) (리마인드 - @Transactional)
- 리마인드용...
- 오늘도 @Service, CourseServiceImpl에 @Transactional 메소드를 쓰지 않음에 따라, Update 시, repo.save()를 했다.
- 이 부분을 정리를 해야하는데... 휴우 시간이 없어 리마인드로 적어두었다.
(4) 목록 체크박스 전체/하나 선택
- 이 부분도 노트용으로 적어두었다.
- 다시 구현을 하려면 꽤 까다로운 내용일 것 같아 기억해두는게 좋을 것 같다.
<script src="https://code.jquery.com/jquery-3.6.1.min.js"
integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous">
</script>
<script>
$(document).ready(function(){
// checkbox 전체 선택
$('#selectAll').on('click', function(){
let checked = $(this).is(":checked");
$('#dataList input[type=checkbox]').each(function(k, v){
$(this).prop('checked', checked);
});
});
// checkbox 한 개 선택
$('#deleteBtn').on('click', function(){
let $checked = $('#dataList input[type = checkbox]:checked');
if ($checked.length < 1) {
alert('삭제할 데이터를 선택해주세요');
return false;
}
if (!confirm('선택한 데이터를 삭제하시겠습니까?')) {
return false;
}
let idList = [];
$.each($checked, function(k, v){
idList.push($(this).val());
});
// 문자열로 전달
let $deleteForm = $('form[name=deleteForm]');
$deleteForm.find('input[name=idList]').val(idList.join(','));
$deleteForm.submit();
})
});
</script>
--------------------HTML 영역-------------------------
<tr th:each="x : ${list}">
<td>
<input th:value="${x.id}" type = "checkbox">
</td>
<td th:text="${x.seq}">1</td>
<td>
<p>
<a th:href="'edit.do?id=' + ${x.id}"
th:text = "${x.subject}">강좌명</a>
</p>
</td>
<td th:text="${x.registeredAt}">2021.08.20</td>
</tr>
| 스마트 에디터 적용하기
- 예전에 어떤 웹 에이전시 회사에서 에디터로 CK 에디터, 네이버 스마트 에디터를 쓰고 있는 걸 본 적이 있다.
- 생각해보면 하이브리드 웹 환경에서도 공지사항 게시판 같은 곳에서 에디터를 썼던 것 같은데, CK, 스마트에디터도 적용할 수 있을지 문득 궁금해진다.
- 일단 오늘 배운 네이버 스마트 에디터를 적용하는 법을 정리하려 한다.
https://github.com/naver/smarteditor2
- 위 링크로 들어가서, Demo로 들어가면 한 번 사용해볼 수 있다.
- 위 링크에 들어가면 [사용자 가이드] 링크가 있는데, 들어가면 자세한 사용법이 나와 있다.
1. 공식 링크의 사용자 가이드 > 다운로드 에서 오픈소스를 다운 받는다.
- 다운 받은 압축파일을 풀면 아래와 같은데, 여기의 [dist] 폴더가 실질적인 에디터이다.
2. 내 어플리케이션의 [resource]-[static]-[res]-[se2] 에 [dist] 폴더 안의 내용을 넣어준다.
- 왜 [static]에 넣어줄까? 이 static 폴더는 정적 리소스(쉽게 정적 페이지라고 하자)를 넣는 곳이다.
- [static] 폴더는 application.yml을 통해 다른 곳으로 지정할 수 있으며,
// applicaton.yml 에서 static 폴더 위치를 다른 곳으로 지정할 수 있다.
spring.resources.static-locations=classpath:/static-loc/
- [static] 안에 html 페이지를 넣어주면, 수정 후 어플리케이션을 재실행 하지 않고도, 변경 사항을 확인 할 수 있다.
- 일반적으로 [resource]-[static] 내부의 폴더, 파일은 path를 "/" - 최상단- 부터 시작한다.
ex. [resource]-[static]-[res]-[se2]의 경우, "/res/se2"
- 웹 에디터를 열어보면 html/css/js로 구성되어 있는데, 우린 이걸 자원으로 사용하며, 동적처리는 하지 않는다.
- ※ 참고 : https://atoz-develop.tistory.com/entry/spring-boot-web-mvc-static-resources
3. smart editor를 사용할 html 페이지 안에 <script>로 경로를 지정해준다.
- 본인 어플리케이션 smart editor 리로스 경로 위치와 일치해야한다.
<script type="text/javascript" src="/res/se2/js/service/HuskyEZCreator.js" charset="utf-8"></script>
4. textarea의 id에 이름을 설정한다.
<textarea name="ir1" id="ir1" rows="10" cols="100">에디터에 기본으로 삽입할 글(수정 모드)이 없다면 이 value 값을 지정하지 않으시면 됩니다.</textarea>
5. 가이드에 따라 추가 스크립트를 작성한다.
- elPlaceHolder는 위에서 지정한 textArea id로 써주어야한다.
<script type="text/javascript">
var oEditors = [];
nhn.husky.EZCreator.createInIFrame({
oAppRef: oEditors,
elPlaceHolder: "ir1",
sSkinURI: "/res/se2/SmartEditor2Skin.html",
fCreator: "createSEditor2"
});
</script>
6. Spring Securiy의 http Securiy를 통해 X-Frame-Option을 sameOrigin으로 설정
- 5번을 한 상태에서, 어플리케이션을 동작시키면 아래와 같이 에러가 나타난다.
- X-Frame-Option이 deny로 되어 있어서 그렇다며, cross-origin 다른 곳으로 access가 차단된 상태란다.
- 해결방법은 Spring Securiy의 Http Securiy를 통해 X-Frame-Option을 변경해주는 것이다.
※ 참고 : https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/X-Frame-Options
/**
* Http Security
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 아래 사항을 추가한다.
http.headers().frameOptions().sameOrigin();
}
.- 참고로 위 코드의 의미는 아파치에서 앞으로 사용하는 모든 Http header에
X-Frame-Option을 sameOrigin으로 설정하겠다는 뜻이라고 한다.
7. textarea에 UPDATE_CONTENTS_FIELD 메세지를 호출
- 6번 상태에서, 어플리케이션을 재가동하면 [수정하기]를 통해 서버에 전송하려할 시, 사이트에서 나가겠냐 물어본다.
- 이에 대해서 사용자 가이드에서 말하길..
에디터는 편집 내용을 서버에 전달해 주는 역할만 수행하며,
에디터에서 편집한 내용을 저장하는 작업은 서버 측에서 이루어진다.
에디터는 편집 내용을 전송하기 위해서 먼저 3번 과정에서 추가한
textarea의 value에 편집 내용을 적용한 후에 서버 측 URL에 form을 전송한다.
textarea의 value에 편집 내용을 적용하려면 UPDATE_CONTENTS_FIELD 메시지를 호출해야 한다.
이 부분은 다음 write.html 파일의 코드를 참고한다
---------------------------------------------------------------
// ‘저장’ 버튼을 누르는 등 저장을 위한 액션을 했을 때 submitContents가 호출된다고 가정한다.
function submitContents(elClickedObj) {
// 에디터의 내용이 textarea에 적용된다.
oEditors.getById["ir1"].exec("UPDATE_CONTENTS_FIELD", []);
// 에디터의 내용에 대한 값 검증은 이곳에서
// document.getElementById("ir1").value를 이용해서 처리한다.
try {
elClickedObj.form.submit();
} catch(e) {}
- 수업에서는 서버에 전송할 form에 id를 준 다음 아래와 같이 제이쿼리로 작성을 했다.
<script src="https://code.jquery.com/jquery-3.6.1.min.js"
integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous">
</script>
<script>
$(document).ready(function(){
$('#submitForm').on('submit', function(){
oEditors.getById["ir1"].exec("UPDATE_CONTENTS_FIELD", []);
});
});
</script>
- 이 상태에서 다시 실행을 했더니, 아래와 같은 오류가 발생했다.
- 이것은 textarea에 required 속성이 있어서인데, 에디터를 사용할 땐, 이처럼 required를 넣을 수 없는 듯 하다.
required만 지우면 잘 써진다.
[ 출처 및 참조 ]
부트캠프 수업을 들은 후 정리한 내용입니다.
'Framework > 프로젝트로 스프링 이해하기' 카테고리의 다른 글
[LMS 만들기] 회원정보 수정 - 우편번호 찾기, ajax와 Rest API (0) | 2022.10.10 |
---|---|
[LMS 만들기] @Controller 와 @RestController 정확히 알기, AJAX로 Json 데이터 전송하기, Principle(로그인 정보) (0) | 2022.10.08 |
[LMS 만들기] 카테고리 수정, 삭제, 정렬 (0) | 2022.10.06 |
[LMS만들기] 회원 상태 변경 및 비밀번호 초기화 (0) | 2022.10.05 |
[LMS만들기] 페이징 처리하기 (with MyBatis) (0) | 2022.10.05 |