티스토리 뷰
2-13 CSS 적용하기
스태틱 디렉터리
기본적으로 스프링부트 프로젝트 생성 시, src/main/resources 아래 static 폴더가 있다.
이 폴더에는 css, js, 이미지 파일을 올려서 관리한다.
1) style.css 라는 파일을 만들고 아래와 같이 작성한다.
textarea {
width:100%;
}
input[type=submit] {
margin-top:10px;
}
2) 템플릿에 스타일을 적용한다.
html 파일에 아래와 같이 link 를 작성하면 된다.
static 폴더를 루트 디렉터리로 인식하므로, /style.css 로만 작성하면 된다.
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
2-14 부트스트랩 적용하기
1) 아래 링크에서 부트스트랩을 설치한다.
https://getbootstrap.com/docs/5.3/getting-started/download/
부트스트랩에는 다양한 버전이 존재하고, 메이저 (3,4,5) 버전에 따라 사용법이 다르다.
이 책은 부트스트랩 버전 5 기준으로 실습을 진행했다.
내가 설치할 시점에는 bootstrap-5.3.3 으로 설치했다.
우측 상단에 버전 선택 후, Download
2) 다운로드 한 zip 파일을 압축해제 한다.
나는 c드라이브 밑에 tools 폴더 아래 압축해제 해두었다.
3) 압축해제한 폴더 안에 bootstrap.min.css 파일과 bootstrap.min.js 파일을 static 폴더 아래에 복사-> 붙여넣기 해준다.
4) 템플릿에 부트스트랩을 적용한다. 아래와 같이 link 를 작성하면 된다.
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
* link 를 작성한 코드 예제 (점프 투 스프링부트 발췌)
class="container my-3" , class="table" , class="table-dark" 와 같은 class 들은
부트스트랩에 이미 정의되어 있는 클래스들로, 간격을 조정하고 테이블에 스타일을 지정하는 용도이다.
* 나머지 노란 부분은 설명 생략
부트 스트랩의 자세한 내용은 아래 링크를 참고한다.
https://getbootstrap.com/docs/5.3/getting-started/introduction/
questionDetail.html 파일에서 부트스트랩의 card 컴포넌트를 이용하여 디자인 해주었다.
<html layout:decorate="~{common/layout}">
<div layout:fragment="content" class="container my-3">
<h1 class="border-bottom py-2" th:text="${question.subject}"></h1>
<!--질문-->
<div class="card my-3">
<div class="card-body">
<!-- 마크다운 적용 위해 주석
<div class="card-text" style="white-space:pre-line;" th:text="${question.content}"></div>
-->
<!--마크다운표시.. 줄바꿈을 위해 사용한 기존 style 삭제후, 마크다운 적용,
th:text가 아닌 th:utext 사용
th:text 사용시 html 태그들이 escape 처리되어 화면에 그대로 보이게 됨.
마크다운으로 변환된 html문서를 제대로 표시하기 위해선 utext써야함
-->
<div class="card-text" th:utext="${@commonUtil.markdown(question.content)}"></div>
<div class="d-flex justify-content-end">
<div th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
<div class="mb-2">modified at</div>
<div th:text="${#temporals.format(question.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
<div class="badge bg-light text-dark p-2 text-start">
<div class="mb-2">
<span th:if="${question.user != null}" th:text="${question.user.username}"></span>
</div>
<div th:text="${#temporals.format(question.writeDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
<div class="my-3">
<a href="javascript:void(0);" th:data-uri="@{|/question/vote/${question.id}|}"
class="recommend btn btn-sm btn-outline-secondary">
추천
<span class="badge rounded-pill bg-success" th:text="${#lists.size(question.voter)}"></span>
</a>
<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.user != null and #authentication.getPrincipal().getUsername() == question.user.username}"
th:text="수정"></a>
<!--삭제버튼 클릭시 alert 문구를 위해 href에 바로 url을 넣지 않음
javascript:void(0) 설정,
삭제를 실행할 url을 얻기 위해 th:data-uri라는 속성에 url 추가
-->
<a href="javascript:void(0);" th:data-uri="@{|/question/delete/${question.id}|}"
class="delete btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.user != null and #authentication.getPrincipal().getUsername() == question.user.username}"
th:text="삭제"></a>
</div>
</div>
</div>
<!--답변갯수-->
<h5 class="border-bottom my-3 py-2" th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<!--답변반복-->
<!--div 바로 아래 a 태그 : 답변수정,추천 후 화면 리프레쉬될때 이 위치로 돌아오도록 함. redirect경로에 a태그id값 함께 보냄-->
<div class="card my-3" th:each="answer : ${question.answerList}">
<a th:id="|answer_${answer.id}|"></a>
<div class="card-body">
<!--마크다운 적용 위해 주석
<div class="card-text" style="white-space:pre-line;" th:text="${answer.content}"></div>
-->
<div class="card-text" th:utext="${@commonUtil.markdown(answer.content)}"></div>
<div class="d-flex justify-content-end">
<div th:if="${answer.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
<div class="mb-2">modified at</div>
<div th:text="${#temporals.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
<div class="badge bg-light text-dark p-2 text-start">
<div class="mb-2">
<span th:if="${answer.user != null}" th:text="${answer.user.username}"></span>
</div>
<div th:text="${#temporals.format(answer.writeDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
<div class="my-3">
<a href="javascript:void(0);" th:data-uri="@{|/answer/vote/${answer.id}|}"
class="recommend btn btn-sm btn-outline-secondary">
추천
<span class="badge rounded-pill bg-success" th:text="${#lists.size(answer.voter)}"></span>
</a>
<a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${answer.user != null and #authentication.getPrincipal().getUsername() == answer.user.username}"
th:text="수정"></a>
<a href="javascript:void(0);" th:data-uri="@{|/answer/delete/${answer.id}|}"
class="delete btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${answer.user != null and #authentication.getPrincipal().getUsername() == answer.user.username}"
th:text="삭제"></a>
</div>
</div>
</div>
<!--답변작성-->
<!-- 필수입력체크를 위해 AnswerForm.java 작성했고, th:object 선언을 해준다 -->
<form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">
<!--post 호출 후 에러 발생시 문구 표출란 -->
<div th:replace="~{common/formErrors :: formErrorsFragment}"></div>
<!-- sec:authorize="isAnonymous()" : 로그인안했을 때 textarea 보이게 -->
<textarea sec:authorize="isAnonymous()" th:field="*{content}" rows="10" class="form-control" disabled></textarea>
<!-- sec:authorize="isAuthenticated()" : 로그인했을 때 textarea 보이게-->
<textarea sec:authorize="isAuthenticated()" th:field="*{content}" rows="10" class="form-control"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
</div>
<script layout:fragment="script" type='text/javascript'>
const delete_elements = document.getElementsByClassName("delete");
Array.from(delete_elements).forEach(function(element) {
// console.log(delete_elements)
// console.log(element)
//class="delete" 에 이벤트 추가
element.addEventListener('click', function() {
if(confirm("정말로 삭제하시겠습니까?")) {
location.href = this.dataset.uri;
};
});
});
const recommend_elements = document.getElementsByClassName("recommend");
Array.from(recommend_elements).forEach(function(element) {
element.addEventListener('click', function() {
if(confirm("정말로 추천하시겠습니까?")) {
location.href = this.dataset.uri;
};
});
});
</script>
</html>
위 코드에서
card, card-body, card-text 등이 card 컴포넌트이고
그 외에도 다양한 클래스를 사용했다.
부트스트랩의 card 컴포넌트에 대한 설명은 아래 링크를 참고한다.
https://getbootstrap.com/docs/5.3/components/card/