...
This commit is contained in:
parent
51b97e2422
commit
d62a4a3c15
@ -75,17 +75,22 @@ class SecurityConfig(
|
||||
"/user/login.bjx", "/user/joinUser.bjx","/tlg/repotToMe.bjx",
|
||||
"/blog/post/imageUpload.bjx", "/blog/post.bjx",
|
||||
// "/blog/post/images/**", // WebSecurityCustomizer에서 이미 ignoring 처리됨
|
||||
"/puzzle/**","/puzzle/play/**",
|
||||
"/puzzle/**","/puzzle/play/**","/bums/save/**",
|
||||
"/rank/**","/spider/**",
|
||||
"/sudoku/**",
|
||||
)
|
||||
}.authorizeHttpRequests { auth ->
|
||||
auth
|
||||
// [정상 유지] 이 두 엔드포인트는 인증(로그인)이 필요하며 CSRF 보호를 받아야 합니다.
|
||||
.requestMatchers(HttpMethod.GET, "/blog/comments/{commentId}/replies.bjx").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/blog/posts/{postId}/comments.bjx").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/blog/posts/{postId}/comments.bjx").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/blog/post/{postId}/like.bjx").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/blog/post/{postId}/unlike.bjx").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/bums/save/loc.api").permitAll()
|
||||
// permitAll() 목록
|
||||
.requestMatchers(
|
||||
|
||||
"/",
|
||||
"/home.bs",
|
||||
"/bums/where.bs" ,
|
||||
|
||||
@ -118,6 +118,7 @@ class BlogController(private val commentService : CommentService) {
|
||||
try {
|
||||
val authentication = SecurityContextHolder.getContext().authentication
|
||||
val username = (authentication.principal as? UserDetails)?.username ?: authentication.name
|
||||
logService.log(username)
|
||||
target.writer = username
|
||||
} catch (e: Exception) {
|
||||
target.writer = "Anonymous" // 인증 정보 가져오기 실패 시
|
||||
@ -144,12 +145,23 @@ class BlogController(private val commentService : CommentService) {
|
||||
} else {
|
||||
// === B. 기존 게시물 수정 (새 버전 생성) 로직 ===
|
||||
// (글쓴이는 client-sent 'target' 객체에 이미 포함되어 있으므로 별도 설정 필요 없음)
|
||||
|
||||
if (target.writer.isNullOrEmpty()) {
|
||||
try {
|
||||
val authentication = SecurityContextHolder.getContext().authentication
|
||||
val username = (authentication.principal as? UserDetails)?.username ?: authentication.name
|
||||
logService.log(username)
|
||||
target.writer = username
|
||||
} catch (e: Exception) {
|
||||
target.writer = "Anonymous" // 인증 정보 가져오기 실패 시
|
||||
}
|
||||
}
|
||||
// 3. (정상 로직) 이 객체에 "수정 시간"을 설정합니다.
|
||||
target.modifyTime = System.currentTimeMillis()
|
||||
|
||||
// 4. 이 게시물이 "버전 기록용 사본"임을 설정합니다.
|
||||
target.originId = target.id // 원본 마스터 ID를 originId 필드에 저장
|
||||
if (target.originId == null) {
|
||||
target.originId = target.id
|
||||
}
|
||||
target.id = null // Mongo가 새 ID를 생성하도록 ID를 null로 변경
|
||||
|
||||
// 5. 모든 데이터(새 카테고리, 태그, modifyTime 포함)가 담긴 "새 버전 문서"를 저장합니다.
|
||||
@ -613,30 +625,66 @@ class BlogController(private val commentService : CommentService) {
|
||||
@PostMapping("posts/{postId}/comments.bjx")
|
||||
fun addComment(@PathVariable postId: String, @RequestBody jsonString: String): Mono<ResponseEntity<ResponceResult>> {
|
||||
try {
|
||||
// 1. Decode the Base64 string to get the envelope JSON
|
||||
val decodedBytes: ByteArray = Base64.getDecoder().decode(jsonString)
|
||||
val requestData = String(decodedBytes)
|
||||
val comment = Gson().fromJson(requestData, Comment::class.java).apply {
|
||||
this.postId = postId
|
||||
this.writeTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
return commentService.addComment(comment)
|
||||
.map {
|
||||
ResponseEntity.ok().body(ResponceResult().apply {
|
||||
resultCode = 0
|
||||
resultMsg = "Comment submitted successfully"
|
||||
})
|
||||
// 2. Parse the ENVELOPE (RequestModel), just like in your "post.bjx" endpoint
|
||||
Gson().fromJson(String(decodedBytes), RequestModel::class.java)?.let { model ->
|
||||
model.data?.let { innerJsonString ->
|
||||
try {
|
||||
// 3. COPY the EXACT decryption/un-formatting logic from "post.bjx"
|
||||
val reqString = innerJsonString.split(GlobalEnvironment.padding(model.getKeyword()))
|
||||
val nb = arrayListOf<String>()
|
||||
val na = arrayListOf<String>()
|
||||
reqString[0].replace(GlobalEnvironment.padding(model.getKeyword()),"").split("").toList().let { na.addAll(it) }
|
||||
reqString[1].replace(GlobalEnvironment.padding(model.getKeyword()),"").split("").toList().let { nb.addAll(it) }
|
||||
var max = nb.size + na.size
|
||||
var fullData = arrayListOf<String>()
|
||||
for (idx in 0..max) { if (idx % 2 == 0) { if (nb.size > 0) { fullData.add(nb.removeLast()) } } else { if (na.size > 0) { fullData.add(na.removeLast()) } } }
|
||||
|
||||
val jsonFromClient = fullData.joinToString("") // This is the REAL comment JSON
|
||||
|
||||
// 4. NOW, parse the decrypted JSON into the Comment object
|
||||
val comment = Gson().fromJson(jsonFromClient, Comment::class.java).apply {
|
||||
this.postId = postId
|
||||
this.writeTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
// 5. Save the valid comment object
|
||||
return commentService.addComment(comment)
|
||||
.map {
|
||||
ResponseEntity.ok().body(ResponceResult().apply {
|
||||
resultCode = 0
|
||||
resultMsg = "Comment submitted successfully"
|
||||
})
|
||||
}
|
||||
.onErrorResume { e ->
|
||||
Mono.just(ResponseEntity.status(500).body(ResponceResult().apply {
|
||||
resultCode = 500
|
||||
resultMsg = "Error submitting comment: ${e.message}"
|
||||
}))
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
// This catches decryption errors
|
||||
return Mono.just(ResponseEntity.status(400).body(ResponceResult().apply {
|
||||
resultCode = 400
|
||||
resultMsg = "Invalid request data (decryption fail)"
|
||||
}))
|
||||
}
|
||||
}
|
||||
.onErrorResume { e ->
|
||||
Mono.just(ResponseEntity.status(500).body(ResponceResult().apply {
|
||||
resultCode = 500
|
||||
resultMsg = "Error submitting comment: ${e.message}"
|
||||
}))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
// This catches envelope parsing errors
|
||||
return Mono.just(ResponseEntity.status(400).body(ResponceResult().apply {
|
||||
resultCode = 400
|
||||
resultMsg = "Invalid request data"
|
||||
resultMsg = "Invalid request data (model fail)"
|
||||
}))
|
||||
|
||||
} catch (e: Exception) {
|
||||
// This catches Base64 decoding errors
|
||||
return Mono.just(ResponseEntity.status(400).body(ResponceResult().apply {
|
||||
resultCode = 400
|
||||
resultMsg = "Invalid request data (base64 fail)"
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -462,4 +462,77 @@ a.btn_layerClose:hover {
|
||||
}
|
||||
#selected-category-area i, #selected-hashtags-area i {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* === 대댓글 기능 CSS (신규 추가) === */
|
||||
|
||||
/* 1. 대댓글 목록 컨테이너 (들여쓰기) */
|
||||
.reply-list {
|
||||
margin-left: 40px;
|
||||
padding-left: 15px;
|
||||
border-left: 2px solid #eee;
|
||||
}
|
||||
|
||||
/* 2. 댓글 헤더 (작성자/날짜/답글 버튼) */
|
||||
.comment-header {
|
||||
border-bottom: 1px dotted #ddd;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
/* Flexbox를 사용해 요소를 양쪽으로 정렬 */
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment-author-info {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.comment-author-info strong {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.comment-author-info .comment-date {
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 3. 답글 달기 버튼 */
|
||||
.btn-reply {
|
||||
font-size: 0.8em;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
color: #555;
|
||||
}
|
||||
.btn-reply:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/* 4. 답글 상태 표시줄 (숨겨진 UI) */
|
||||
#reply-status-bar {
|
||||
display: none; /* JS로 제어 */
|
||||
background: #f0f8ff;
|
||||
border: 1px solid #bde0ff;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
#reply-status-text {
|
||||
font-size: 0.9em;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
#btn-cancel-reply {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #E63946; /* 빨간색 계열 */
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -10,23 +10,31 @@
|
||||
|
||||
var stagedCategory = 'none';
|
||||
var stagedHashtags = []; // 해시태그는 배열로 관리
|
||||
|
||||
var currentReplyParentId = null;
|
||||
// 전역 변수: Quill 에디터 인스턴스와 게시물 기본 데이터를 저장합니다.
|
||||
var quill = null;
|
||||
var currentLat = 0.0;
|
||||
var currentLon = 0.0;
|
||||
var baseData = {
|
||||
'id': "",
|
||||
'originId': "",
|
||||
'title': "",
|
||||
'content': "",
|
||||
'category': "none",
|
||||
'tags': "",
|
||||
'writeTime': 0,
|
||||
'modifyTime': 0,
|
||||
'firstPostLat': 0.0,
|
||||
'firstPostLon': 0.0,
|
||||
'firstAddress': "",
|
||||
'modifyLat': 0.0,
|
||||
'modifyLon': 0.0,
|
||||
'originId': "",
|
||||
'writeTime': 0,
|
||||
'modifyAddress': "",
|
||||
'writer': "",
|
||||
'posting': false,
|
||||
'readCount': 0,
|
||||
'voteCount': 0,
|
||||
'unlikeCount': 0
|
||||
};
|
||||
|
||||
// jQuery를 사용하여 문서가 완전히 로드된 후에 함수를 실행합니다.
|
||||
@ -59,7 +67,11 @@ $(document).ready(function() {
|
||||
openPopup(this);
|
||||
});
|
||||
/* === (대규모 수정) 팝업 입력/적용/취소 버튼 로직 === */
|
||||
|
||||
/* === (신규 추가) 댓글 등록 버튼 이벤트 리스너 === */
|
||||
$('#submit-comment').on('click', function(e) {
|
||||
e.preventDefault(); // 기본 버튼 동작 방지
|
||||
submitComment(); // 아래에 정의된 새 함수 호출
|
||||
});
|
||||
// --- 1. Category Popup Logic ---
|
||||
const categoryInput = document.getElementById('category-input');
|
||||
const addCategoryBtn = document.getElementById('add-category-btn');
|
||||
@ -159,6 +171,19 @@ function initEditor(useEditor = false) {
|
||||
baseData.firstPostLon = serverData.firstPostLon;
|
||||
baseData.writeTime = serverData.writeTime;
|
||||
baseData.originId = serverData.originId;
|
||||
|
||||
// === [버그 수정] 누락된 메타데이터 복사 로직 추가 ===
|
||||
baseData.modifyTime = serverData.modifyTime;
|
||||
baseData.firstAddress = serverData.firstAddress;
|
||||
baseData.modifyLat = serverData.modifyLat;
|
||||
baseData.modifyLon = serverData.modifyLon;
|
||||
baseData.modifyAddress = serverData.modifyAddress;
|
||||
baseData.writer = serverData.writer;
|
||||
baseData.posting = serverData.posting;
|
||||
baseData.readCount = serverData.readCount;
|
||||
baseData.voteCount = serverData.voteCount;
|
||||
baseData.unlikeCount = serverData.unlikeCount;
|
||||
// ===============================================
|
||||
}
|
||||
|
||||
getLocation();
|
||||
@ -718,9 +743,26 @@ function post(target, type, data, key, callBackResult) {
|
||||
};
|
||||
httpRequest.open('POST', target, true);
|
||||
httpRequest.setRequestHeader("Content-Type", "text/plain");
|
||||
httpRequest.send(btoa(JSON.stringify({
|
||||
|
||||
const csrfMeta = document.querySelector('meta[name="_csrf"]');
|
||||
if (csrfMeta) {
|
||||
const csrfToken = csrfMeta.getAttribute('content');
|
||||
if (csrfToken) {
|
||||
// Spring Security는 이 헤더를 확인합니다.
|
||||
httpRequest.setRequestHeader("X-CSRF-TOKEN", csrfToken);
|
||||
}
|
||||
}
|
||||
// 1. Create the complete JSON string payload
|
||||
const jsonPayloadString = JSON.stringify({
|
||||
'data': unformat(type, data, key), 'key': key, 'type': type,
|
||||
})));
|
||||
});
|
||||
|
||||
// 2. [FIX] Convert the Unicode JSON string (which may contain Korean) into
|
||||
// a UTF-8 byte stream that btoa() can safely handle.
|
||||
const utf8SafePayload = unescape(encodeURIComponent(jsonPayloadString));
|
||||
|
||||
// 3. Send the result after running btoa() on the safe string.
|
||||
httpRequest.send(btoa(utf8SafePayload));
|
||||
}
|
||||
|
||||
function postLogin(target, type, data, key, callBackResult) {
|
||||
@ -924,4 +966,189 @@ function handleVote(buttonElement, voteType) {
|
||||
controls.querySelectorAll('button').forEach(btn => btn.disabled = false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* [수정됨] 댓글 등록 처리 함수 (대댓글 지원)
|
||||
*/
|
||||
function submitComment() {
|
||||
|
||||
const commentInput = document.getElementById('comment-input');
|
||||
if (!commentInput) return;
|
||||
|
||||
const content = commentInput.value.trim();
|
||||
|
||||
if (content.length === 0) {
|
||||
alert('댓글 내용을 입력하세요.');
|
||||
commentInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// [수정] parentId를 하드코딩(null)하는 대신 전역 변수에서 읽어옴
|
||||
const commentData = {
|
||||
content: content,
|
||||
writer: null,
|
||||
parentId: currentReplyParentId, // currentReplyParentId 값 (null 또는 댓글ID) 사용
|
||||
postId: null,
|
||||
id: null
|
||||
};
|
||||
|
||||
const postId = serverData.id;
|
||||
if (!postId) {
|
||||
alert("게시물 ID를 찾을 수 없어 댓글을 등록할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadUrl = `${getMainPath()}/blog/posts/${postId}/comments.bjx`;
|
||||
const encType = serverData.enc;
|
||||
const keyword = serverData.keyword;
|
||||
|
||||
try {
|
||||
post(uploadUrl, encType, JSON.stringify(commentData), keyword, function(resultData) {
|
||||
try {
|
||||
const response = JSON.parse(resultData);
|
||||
|
||||
if (response.resultCode === 0) {
|
||||
alert('댓글이 성공적으로 등록되었습니다.');
|
||||
commentInput.value = ''; // 입력창 초기화
|
||||
|
||||
cancelReply(); // [신규 추가] 답글 상태 초기화
|
||||
|
||||
fetchComments(postId); // 목록 새로고침
|
||||
} else {
|
||||
alert('댓글 등록 실패: ' + (response.resultMsg || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse comment submission response:', e, resultData);
|
||||
alert('댓글 등록 후 서버 응답을 처리하는 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error during comment submission:', err);
|
||||
alert('댓글 전송 중 예외가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchComments(postId) {
|
||||
if (!postId) return;
|
||||
|
||||
const commentsListContainer = document.getElementById('comments-list');
|
||||
if (!commentsListContainer) return;
|
||||
|
||||
commentsListContainer.innerHTML = '<p style="text-align: center; color: #888;">댓글 목록을 불러오는 중...</p>';
|
||||
|
||||
try {
|
||||
// 1. 최상위 댓글 목록 (ParentId = null)을 먼저 가져옵니다.
|
||||
const parentResponse = await fetch(`${getMainPath()}/blog/posts/${postId}/comments.bjx`);
|
||||
const parentData = await parentResponse.json();
|
||||
|
||||
if (parentData.resultCode !== 0 || !parentData.comments) {
|
||||
throw new Error(parentData.resultMsg || 'Failed to load parent comments');
|
||||
}
|
||||
|
||||
if (parentData.comments.length === 0) {
|
||||
commentsListContainer.innerHTML = '<p style="text-align: center; color: #888;">아직 댓글이 없습니다. 첫 댓글을 작성해보세요.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 컨테이너 비우기
|
||||
commentsListContainer.innerHTML = '';
|
||||
|
||||
// 2. 각 최상위 댓글을 순회합니다 (for...of 루프 사용 필수)
|
||||
for (const parentComment of parentData.comments) {
|
||||
|
||||
// 2-1. 최상위 댓글 HTML 생성 및 DOM에 추가
|
||||
const parentElement = document.createElement('article');
|
||||
parentElement.className = 'comment-item';
|
||||
parentElement.id = `comment-${parentComment.id}`;
|
||||
parentElement.innerHTML = createCommentHTML(parentComment); // 헬퍼 함수 사용
|
||||
commentsListContainer.appendChild(parentElement);
|
||||
|
||||
// 2-2. 해당 댓글의 "답글 API"를 호출합니다.
|
||||
const replyResponse = await fetch(`${getMainPath()}/blog/comments/${parentComment.id}/replies.bjx`);
|
||||
const replyData = await replyResponse.json();
|
||||
|
||||
if (replyData.resultCode === 0 && replyData.comments && replyData.comments.length > 0) {
|
||||
// 2-3. 답글 컨테이너(div) 생성
|
||||
const replyListContainer = document.createElement('div');
|
||||
replyListContainer.className = 'reply-list'; // CSS 들여쓰기 적용
|
||||
|
||||
// 2-4. 모든 답글(대댓글)을 순회하며 HTML 추가
|
||||
replyData.comments.forEach(childComment => {
|
||||
replyListContainer.innerHTML += createCommentHTML(childComment, true); // 헬퍼 함수 사용 (isReply=true)
|
||||
});
|
||||
|
||||
// 2-5. 답글 컨테이너를 부모 댓글(parentElement)의 자식으로 삽입
|
||||
parentElement.appendChild(replyListContainer);
|
||||
}
|
||||
} // end for loop
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch comments hierarchicaly:', err);
|
||||
commentsListContainer.innerHTML = '<p>댓글 로딩 중 오류가 발생했습니다.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [신규 추가] 댓글 객체를 받아 HTML 문자열을 생성하는 헬퍼 함수
|
||||
* @param {object} comment - 댓글 객체
|
||||
* @param {boolean} isReply - 대댓글 여부 (대댓글에는 "답글달기" 버튼 숨김. 원한다면 true로 변경)
|
||||
* @returns {string} - 완성된 HTML 문자열
|
||||
*/
|
||||
function createCommentHTML(comment, isReply = false) {
|
||||
const writerName = comment.writer || 'Anonymous';
|
||||
const date = new Date(comment.writeTime);
|
||||
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
// JS에서 XSS를 방지하기 위해 특수문자를 HTML 엔티티로 치환 (간단 버전)
|
||||
const safeContent = String(comment.content).replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br>");
|
||||
const writerNameForReply = String(writerName).replace(/'/g, "\\'"); // ' 문자가 JS를 깨트리는 것을 방지
|
||||
|
||||
// 참고: 현재 로직은 대댓글의 대댓글(3단계)은 지원하지 않습니다. (isReply = true이면 답글 버튼 생성 안 함)
|
||||
// 3단계 이상을 지원하려면 isReply 체크를 제거하고, 답글 API가 대댓글도 정상적으로 가져오는지 확인해야 합니다.
|
||||
const replyButtonHTML = !isReply
|
||||
? `<button class="btn-reply" onclick="setReplyTarget('${comment.id}', '${writerNameForReply}')">답글</button>`
|
||||
: ''; // 대댓글에는 "답글" 버튼 표시 안 함
|
||||
|
||||
return `
|
||||
<div class="comment-header">
|
||||
<div class="comment-author-info">
|
||||
<strong>${writerName}</strong>
|
||||
<span class="comment-date">${formattedDate}</span>
|
||||
</div>
|
||||
${replyButtonHTML}
|
||||
</div>
|
||||
<p style="padding: 5px 0 15px 5px; min-height: 2em;">${safeContent}</p>
|
||||
`;
|
||||
}
|
||||
/**
|
||||
* [신규 추가] "답글 달기" 버튼 클릭 시 호출되는 헬퍼 함수
|
||||
* @param {string} commentId - 부모가 될 댓글의 ID
|
||||
* @param {string} writerName - 부모 댓글 작성자명
|
||||
*/
|
||||
function setReplyTarget(commentId, writerName) {
|
||||
currentReplyParentId = commentId; // 전역 변수(상태) 설정
|
||||
|
||||
// UI 업데이트
|
||||
const statusBar = document.getElementById('reply-status-bar');
|
||||
const statusText = document.getElementById('reply-status-text');
|
||||
const commentInput = document.getElementById('comment-input');
|
||||
|
||||
if (statusBar && statusText) {
|
||||
statusText.innerText = `@${writerName} 님에게 답글 다는 중...`;
|
||||
statusBar.style.display = 'flex'; // 숨겨둔 상태바 표시
|
||||
}
|
||||
commentInput.focus(); // 입력창으로 포커스 이동
|
||||
}
|
||||
|
||||
/**
|
||||
* [신규 추가] 답글 달기 "취소" 시 호출되는 헬퍼 함수
|
||||
*/
|
||||
function cancelReply() {
|
||||
currentReplyParentId = null; // 상태 초기화
|
||||
const statusBar = document.getElementById('reply-status-bar');
|
||||
if (statusBar) {
|
||||
statusBar.style.display = 'none'; // 상태바 숨기기
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================= */
|
||||
|
||||
@ -9,7 +9,10 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.js"></script>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {initEditor(false)});</script>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {
|
||||
initEditor(false)
|
||||
fetchComments(serverData.id);
|
||||
});</script>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="content" id="content">
|
||||
@ -85,6 +88,10 @@
|
||||
<section class="comment-section">
|
||||
<h2>Comments</h2>
|
||||
<div id="comment-form-container">
|
||||
<div id="reply-status-bar" style="display: none;">
|
||||
<span id="reply-status-text"></span>
|
||||
<button id="btn-cancel-reply" onclick="cancelReply()">X 취소</button>
|
||||
</div>
|
||||
<textarea id="comment-input" placeholder="댓글을 입력하세요..."></textarea>
|
||||
<button id="submit-comment" class="button">등록</button>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user