Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ dependencies {
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

implementation 'org.commonmark:commonmark:0.21.0'
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.20.0' // 테이블 관련 추가 설정
implementation 'org.commonmark:commonmark-ext-gfm-strikethrough:0.21.0' // 취소선 관련 추가 설정
implementation 'com.atlassian.commonmark:commonmark-ext-task-list-items:0.15.0' // task 관련 추가 설정

implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20211018.2'

implementation 'org.springframework.boot:spring-boot-starter-mail'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
package com.ll.spring_additional.base.security;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.commonmark.Extension;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;

import org.commonmark.ext.task.list.items.TaskListItemsExtension;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
Expand All @@ -8,7 +19,6 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration // 스프링 환경설정 파일
@EnableWebSecurity // 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 -> 내부적으로 시큐리티 필터체인 동작
Expand Down Expand Up @@ -42,4 +52,28 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// 마크다운 렌더링 렌더러 및 파서 빈 등록
@Bean
HtmlRenderer htmlRenderer(List<Extension> extensions) {
return HtmlRenderer.builder()
.extensions(extensions)
.build();
}

@Bean
Parser parser(List<Extension> extensions) {
return Parser.builder()
.extensions(extensions)
.build();
}

@Bean
List<Extension> markdownExtensions() {
return Arrays.asList(
TablesExtension.create(),
StrikethroughExtension.create(),
TaskListItemsExtension.create()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.util.List;
import java.util.Set;

import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
Expand Down Expand Up @@ -66,5 +68,6 @@ public void prePersist() {

@OneToMany(mappedBy = "answer", cascade = {CascadeType.REMOVE})
@ToString.Exclude
@LazyCollection(LazyCollectionOption.EXTRA) // commentList.size(); 함수가 실행될 때 SELECT COUNT 실행
private List<Comment> comments = new ArrayList<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public String getCategoryAsString() {

@OneToMany(mappedBy = "question", cascade = {CascadeType.REMOVE})
@ToString.Exclude
@LazyCollection(LazyCollectionOption.EXTRA) // commentList.size(); 함수가 실행될 때 SELECT COUNT 실행
private List<Comment> comments = new ArrayList<>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,37 @@
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class CommonUtil {
private final Parser parser;
private final HtmlRenderer renderer;

public String markdown(String markdown) {
Parser parser = Parser.builder().build();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
String html = renderer.render(document);

System.out.println("Converted HTML: " + html);

// Sanitize HTML
PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("h1", "h2", "h3", "p", "b", "i", "em", "strong", "img", "a", "ul", "ol", "li", "table", "thead", "tbody", "tr", "th", "td", "del", "blockquote", "code", "pre", "input", "hr")
.allowUrlProtocols("https", "http")
.allowAttributes("href", "target").onElements("a")
.allowAttributes("src", "alt").onElements("img")
.allowAttributes("type", "checked", "disabled").onElements("input")
.allowAttributes("border", "cellspacing", "cellpadding").onElements("table")
.requireRelNofollowOnLinks()
.toFactory();

String safeHtml = policy.sanitize(html);
System.out.println("After sanitize: " + safeHtml);
return safeHtml; // Return the sanitized HTML
}
}
36 changes: 36 additions & 0 deletions src/main/resources/static/markdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
table {
border-collapse: collapse; /* 테두리 */>
}

thead {
background-color: #91c0fd;
font-weight: bold;
color: white;
padding: 12px;
text-align: center;
}

th, td {
padding: 12px;
border: 3px solid #91c0fd;
}
/* 인용 */
blockquote {
/* 왼쪽 경계선 */
border-left: 4px solid #cccccc;
/* 들여 쓰기와 여백 */
padding-left: 20px;
margin-left: 0;
/* 글꼴 속성 (기울어짐, 색상 등) */
font-style: italic;
color: #666666;
}

a {
text-decoration: none;
color: inherit;
}

img {
max-width: 100%;
}
38 changes: 12 additions & 26 deletions src/main/resources/templates/comment/answer_comment.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,24 @@
<!-- 댓글 개수 갱신용 -->
<div id="new_total_answer_comments" th:if="${totalCount !=null}" th:text="${totalCount}" style="display: none;"></div>
<div th:if="${totalCount ==null}" th:text="${totalCount}"style="display: none;">0</div>
<ul>
<li class="nav-link d-flex flex-column gap-2" th:each="comment : ${answerCommentPaging}" th:if="${comment.parent == null}">

<div class="nav-link d-flex flex-column gap-2" th:each="comment : ${answerCommentPaging}" th:if="${comment.parent == null}">
<div class="card-body bg-white rounded shadow-lg">
<a th:id="|answer_comment_${comment.id}|"></a>
<div class="d-flex align-items-baseline gap-2">
<p th:text="${comment.writer.username}" class="text-primary"></p>
<p th:text="${comment.writer.username}" class="text-primary" style="white-space: pre-wrap;"></p>
<!-- 비밀댓글 표시 자물쇠-->
<span class="badge text-bg-success" th:if="${comment.secret}">
<i class="fa-solid fa-lock" style="color: #ffffff;"></i>
</span>
</div>
<div class="d-flex justify-content-between">
<p th:text="${comment.content}" th:if="${!comment.deleted and !comment.secret}"></p>
<p th:text="${comment.content}" style="white-space: pre-wrap;" th:if="${!comment.deleted and !comment.secret}"></p>
<p class="text-body-tertiary" th:if="${comment.deleted}"> 삭제된 댓글입니다.</p>
<!-- 비밀 댓글 분기 시작, 위 : 로그인 시 댓글 작성자 or 질문 작성자 or 관리자일 경우 확인 가능 / 아래 : 로그인 안하면 아에 안보이게-->
<div sec:authorize="isAuthenticated()">
<p class="text-lg font-bold" th:if="${comment.secret and ((comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}"
th:text="${comment.content}"></p>
th:text="${comment.content}" style="white-space: pre-wrap;" ></p>
<p class="text-lg font-bold"
th:if="${comment.secret and !((comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}">
비밀 댓글입니다.</p>
Expand Down Expand Up @@ -133,8 +133,8 @@
</div>

<!-- 대댓글 출력-->
<ul class="ml-8 space-y-2">
<li th:each="childComment, childIndex : ${comment.children}">
<div class="ml-8 space-y-2">
<div th:each="childComment, childIndex : ${comment.children}">
<div class="p-4">
<a th:id="|answer_comment_${childComment.id}|"></a>
<div class="d-flex align-items-baseline gap-2">
Expand All @@ -145,12 +145,12 @@
</span>
</div>
<div class="d-flex justify-content-between">
<p th:text="${childComment.content}" th:if="${!childComment.deleted and !childComment.secret}"></p>
<p th:text="${childComment.content}" style="white-space: pre-wrap;" th:if="${!childComment.deleted and !childComment.secret}"></p>
<p class="text-body-tertiary" th:if="${childComment.deleted}"> 삭제된 댓글입니다.</p>
<!-- 비밀 댓글 분기 시작, 위 : 로그인 시 대댓글 작성자 or 댓글 작성자 or 질문 작성자 or 관리자일 경우 확인 가능 / 아래 : 로그인 안하면 아에 안보이게-->
<div sec:authorize="isAuthenticated()">
<p class="text-lg font-bold" th:if="${childComment.secret and ((childComment.writer.username == #authentication.getPrincipal().getUsername()) or (comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}"
th:text="${childComment.content}"></p>
th:text="${childComment.content}" style="white-space: pre-wrap;" ></p>
<p class="text-lg font-bold"
th:if="${childComment.secret and !((childComment.writer.username == #authentication.getPrincipal().getUsername()) or (comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}">
비밀 댓글입니다.</p>
Expand Down Expand Up @@ -218,10 +218,9 @@
</button>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<!-- 댓글 페이징처리 시작 -->
<div th:if="${!answerCommentPaging.isEmpty()}">
<ul class="pagination justify-content-center">
Expand Down Expand Up @@ -633,19 +632,6 @@
}
});
}

function Answer_CommentForm__submit(form) {
// username 이(가) 올바른지 체크

form.commentContents.value = form.commentContents.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

if (form.commentContents.value.length === 0) {
alert('내용을 입력해주세요');
form.commentContents.focus();
return;
}
form.submit(); // 폼 발송
}
</script>

</main>
36 changes: 11 additions & 25 deletions src/main/resources/templates/comment/question_comment.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
<div id="new_total_question_comments" th:if="${totalCount !=null}" th:text="${totalCount}" style="display: none;"></div>
<div th:if="${totalCount ==null}" th:text="${totalCount}"style="display: none;">0</div>
<!-- 댓글 출력 부분-->
<ul>
<li class="nav-link d-flex flex-column gap-2" th:each="comment : ${questionCommentPaging}" th:if="${comment.parent == null}">
<div class="nav-link d-flex flex-column gap-2" th:each="comment : ${questionCommentPaging}" th:if="${comment.parent == null}">
<div class="card-body bg-white rounded shadow-lg">
<a th:id="|question_comment_${comment.id}|"></a>
<div class="d-flex align-items-baseline gap-2">
Expand All @@ -39,12 +38,12 @@
</span>
</div>
<div class="d-flex justify-content-between">
<p th:text="${comment.content}" th:if="${!comment.deleted and !comment.secret}"></p>
<p th:text="${comment.content}" style="white-space: pre-wrap;" th:if="${!comment.deleted and !comment.secret}"></p>
<p class="text-body-tertiary" th:if="${comment.deleted}"> 삭제된 댓글입니다.</p>
<!-- 비밀 댓글 분기 시작, 위 : 로그인 시 댓글 작성자 or 질문 작성자 or 관리자일 경우 확인 가능 / 아래 : 로그인 안하면 아에 안보이게-->
<div sec:authorize="isAuthenticated()">
<p class="text-lg font-bold" th:if="${comment.secret and ((comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}"
th:text="${comment.content}"></p>
th:text="${comment.content}" style="white-space: pre-wrap;" ></p>
<p class="text-lg font-bold"
th:if="${comment.secret and !((comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}">
비밀 댓글입니다.</p>
Expand Down Expand Up @@ -133,8 +132,8 @@
</div>

<!-- 대댓글 출력-->
<ul class="ml-8 space-y-2">
<li th:each="childComment, childIndex : ${comment.children}">
<div class="ml-8 space-y-2">
<div th:each="childComment, childIndex : ${comment.children}">
<div class="p-4">
<a th:id="|comment_${childComment.id}|"></a>
<div class="d-flex align-items-baseline gap-2">
Expand All @@ -145,12 +144,12 @@
</span>
</div>
<div class="d-flex justify-content-between">
<p th:text="${childComment.content}" th:if="${!childComment.deleted and !childComment.secret}"></p>
<p th:text="${childComment.content}" style="white-space: pre-wrap;" th:if="${!childComment.deleted and !childComment.secret}"></p>
<p class="text-body-tertiary" th:if="${childComment.deleted}"> 삭제된 댓글입니다.</p>
<!-- 비밀 댓글 분기 시작, 위 : 로그인 시 대댓글 작성자 or 댓글 작성자 or 질문 작성자 or 관리자일 경우 확인 가능 / 아래 : 로그인 안하면 아에 안보이게-->
<div sec:authorize="isAuthenticated()">
<p class="text-lg font-bold" th:if="${childComment.secret and ((childComment.writer.username == #authentication.getPrincipal().getUsername()) or (comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}"
th:text="${childComment.content}"></p>
th:text="${childComment.content}" style="white-space: pre-wrap;" ></p>
<p class="text-lg font-bold"
th:if="${childComment.secret and !((childComment.writer.username == #authentication.getPrincipal().getUsername()) or (comment.writer.username == #authentication.getPrincipal().getUsername()) or (question.author.username == #authentication.getPrincipal().getUsername()) or (#authentication.principal.username eq 'admin'))}">
비밀 댓글입니다.</p>
Expand Down Expand Up @@ -218,10 +217,10 @@
</button>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>

<!-- 댓글 페이징처리 시작 -->
<div th:if="${!questionCommentPaging.isEmpty()}">
<ul class="pagination justify-content-center">
Expand Down Expand Up @@ -622,19 +621,6 @@
});
}


function CommentForm__submit(form) {
// username 이(가) 올바른지 체크

form.commentContents.value = form.commentContents.value.trim(); // 입력란의 입력값에 있을지 모르는 좌우공백제거

if (form.commentContents.value.length === 0) {
alert('내용을 입력해주세요');
form.commentContents.focus();
return;
}
form.submit(); // 폼 발송
}
</script>

</main>
10 changes: 0 additions & 10 deletions src/main/resources/templates/common/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,6 @@
color:inherit;
text-decoration:inherit;
}

ul, li {
/* 앞에 점 없애기 */
list-style:none;
/* 안쪽 여백 제거 */
padding:0;
/* 바깥쪽 여백 제거 */
margin:0;
}

</style>


Expand Down
Loading