개주 훈련일지/🏋️ 전집중 호흡 훈련

게시글 상세보기 리뉴얼: 신고 버튼 + 댓글 프로필 + 수정일 표시

lshfood2 2026. 2. 17. 17:45

[ 게시글 상세보기 기능 추가 ]

중프 때 만든 게시글 상세보기는
중프 설계 기준으로는 정상적으로 돌아갔다.

 

하지만 커뮤니티 운영 관점에서
꼭 필요한 요소들이 빠져 있었다.

 

신고 기능이 없어서

문제 게시글을 수집할 방법이 없었고,

 

댓글 작성자 정보도

닉네임 텍스트 정도만 보여줘서

커뮤니티 느낌이 약했다.


또 작성/수정일이 명확하지 않아서

‘수정된 글인지’가 UX로 전달되지 않았다.

 

이번 작업에서는 이 3가지를 한 번에

보강하면서, 페이지 구조도 함께 정리했다.

  • 신고 버튼 + 신고 모달
  • 댓글 작성자 프로필(사진/꾸밈) 노출
  • 게시글/댓글 작성일·수정일 표시 규칙 적용
  • 그리고 유지보수를 위해 JSP / CSS / JS 분리

1. 구조 분리

중프 버전은 JSP 안에 CSS/JS가 섞여 있어서

수정할 때마다 찾기 힘들고 충돌도 자주 났다.

최종 버전에서는 전용 파일 3개로 분리했다.

 

전 → 후

전(중프: 한 파일에 몰아넣음)

<style>
  /* board detail css ... */
</style>

<script>
  /* board detail js ... */
</script>

 

후(최종: 파일 분리)

<link rel='stylesheet' href='${ctx}/css/boarddetail.css' />
<script src='${ctx}/js/boarddetail.js'></script>

2. ctx 스코프

헤더를 jsp:include로 붙이는 구조에서는

include 내부에서도 ctx를 쓰는 경우가 많다.

그래서 ctx는 request scope로 올려서

include에서도 안전하게 참조하도록 바꿨다.

 

전 → 후

<c:set var='ctx' value='${pageContext.request.contextPath}' />

 

<c:set var='ctx' value='${pageContext.request.contextPath}' scope='request' />

3. 신고 버튼

중프에는 신고 버튼 자체가 없었다.
최종에서는 '로그인+본인글 아님+아직 신고 안 함'일 때만

버튼이 렌더링되도록 했다.

 

전 → 후

(없음)

 

후(JSTL로 노출 조건 구성)

<c:set var='isReportedFlag'
       value='${isReported == true or isReported == 1 or isReported == "1"}' />

<c:set var='canReportPost'
       value='${isLogin and (sessionMemberId ne boardData.memberId) and (not isReportedFlag)}' />

<c:if test='${canReportPost}'>
  <button id='btnReport' type='button' class='bd-btn danger'>신고</button>
</c:if>

 

신고는 모달로 받고 접수 성공 시

버튼을 즉시 숨겨 UX를 마무리했다.

// 성공 시
if ($btnReport) $btnReport.style.display = 'none';

4. 작성일·수정일

중프는 작성일/수정일을

제대로 분리해서 보여주지 않았다.

최종에서는 수정된 경우에만 수정일 칩을

추가로 노출하는 규칙을 넣었다.

 

전 → 후

전(작성자/조회수 정도만)

작성자: ${boardData.writerNickname}
조회수: ${boardData.boardViews}

 

후(작성일은 항상, 수정일은 수정된 경우만)

<c:set var='createdAt' value='${boardData.boardCreatedAt}' />
<c:set var='updatedAt' value='${boardData.boardUpdatedAt}' />

<c:set var='boardIsEdited'
       value='${boardData.isEdited == 1 or boardData.isEdited == true or boardData.isEdited == "1"
               or (not empty updatedAt and updatedAt ne createdAt)}' />

<span class='meta-chip'>작성일 ${createdAt}</span>

<c:if test='${boardIsEdited and not empty updatedAt}'>
  <span class='meta-chip'>수정일 ${updatedAt}</span>
</c:if>

5. CKEditor 본문 안정화

중프는 본문을 pre-wrap으로 고정해서,

CKEditor HTML(문단/리스트/표)이 들어오면

레이아웃이 깨질 수 있었다.


최종은 기본은 normal로 살리고,

code/pre만 스크롤을 허용하는 방식으로 정리했다.

 

전 → 후

.bd-content-box .bd-content {
  white-space: pre-wrap;
}

 

.board-detail-page .bd-content {
  white-space: normal;
  overflow-wrap: anywhere;
}

.board-detail-page .bd-content pre {
  white-space: pre;
  overflow: auto;
}

 

표/이미지도 깨짐 방지 처리를 추가했다.

.board-detail-page .bd-content table {
  display: block;
  overflow-x: auto;
  width: 100%;
}

.board-detail-page .bd-content img,
.board-detail-page .bd-content iframe {
  max-width: 100% !important;
}

6. 댓글 렌더 방식

중프는 댓글을 서버 렌더링(c:forEach)으로 찍었다.
최종은 정렬/갱신을 위해

댓글을 비동기로 다시 렌더링한다.

 

전 → 후

전(서버 렌더링)

<c:forEach var='r' items='${replyList}'>
  <div class='reply-item'>
    <span class='reply-nick'>${r.writerNickname}</span>
    <div class='reply-content'>${r.replyContent}</div>
  </div>
</c:forEach>

 

후(비동기 렌더링 자리만 남김)

<div id='replyList' class='reply-list'>
  <div class='reply-empty' id='replyEmpty' style='display:none;'>
    등록된 댓글이 없습니다.
  </div>
</div>

 

그리고 JS에서 목록을 받아서

renderReplyItem()로 DOM을 구성한다.

function loadReplies(condition) {
  // GET 호출 → list 받아서 → renderReplyItem 반복
}

7. 댓글 프로필

중프 댓글은 닉네임 텍스트만 보여줬다.
최종에서는 댓글 렌더링 시

프로필 이미지/색/꾸밈 클래스까지 반영한다.

 

전 → 후

<span class='reply-nick'>${r.writerNickname}</span>

 

후(JS 렌더에서 아바타 + 꾸밈 클래스 포함)

return (
  "<div class='reply-item'>" +
    "<div class='reply-top'>" +
      "<div style='display:flex; gap:10px;'>" +
        avatarHtml +
        "<div class='reply-writer " + decoClass + "'>" + nickname + "</div>" +
      "</div>" +
    "</div>" +
  "</div>"
);

8. danger 버튼 톤

신고 버튼은 ‘경고 느낌’이 확실해야 해서

danger만 컬러/그라데이션을 강화했다.


또 disabled 공통 처리 때문에

danger가 평범해지는 걸 막기 위해

danger disabled hover는 예외 처리했다.

 

전 → 후

전(기본 danger)

.btn-danger2 {
  background: linear-gradient(...);
}

 

후(danger 강화 + disabled hover 예외)

.board-detail-page .bd-btn.danger {
  background: linear-gradient(135deg, rgba(255, 70, 95, 0.55), rgba(0, 0, 0, 0.18));
  border-color: rgba(255, 90, 120, 0.55);
}

.board-detail-page .bd-btn:disabled:hover {
  background: var(--btn-bg);
  border-color: var(--btn-border);
}

.board-detail-page .bd-btn.danger:disabled:hover {
  background: linear-gradient(135deg, rgba(255, 70, 95, 0.55), rgba(0, 0, 0, 0.18));
  border-color: rgba(255, 90, 120, 0.55);
}

마무리

정리하면 이번 보드 디테일 개선은

기능 3종 추가에서 끝난 게 아니라,
그 기능들이 자연스럽게 유지보수되도록

구조까지 함께 정리한 작업이었다.

  1. 신고 UX를 추가하면서
    권한/노출 조건을 JSP에서 확정
  2. 댓글은 비동기 렌더로 전환해
    정렬/갱신 구조를 단순화
  3. 프로필/꾸밈/수정일 같은
    운영 요소를 UI로 드러나게 개선
  4. CSS/JS 분리 + 스코프 적용으로
    페이지 단위 유지보수 가능
변경 포인트 요약 체크리스트

[ 구조 ]
> JSP / CSS / JS 분리 CSS 스코프를
body.board-detail-page로 제한

> ctx를 request scope로 올려
include(header)에서도 사용 가능하게 정리

[ 게시글 영역 ]
> 작성일/수정일 표시 규칙 적용
(수정된 경우만 수정일 노출)

> 본문은 CKEditor HTML 렌더 기준으로
스타일 리셋(white-space normal)

> 표/코드/이미지/iframe
오버플로 대응(모바일 포함)

[ 신고 기능 ]
> 신고 버튼 노출 조건 추가
(로그인 + 본인글 아님 + 미신고)

> 신고 모달 UI 추가
+ 접수 성공 시 버튼 즉시 숨김 처리

> danger 버튼 톤 강화
(hover/active/focus/disabled 예외 포함)

[ 댓글 영역 ]
> 댓글 목록을 비동기
렌더링 구조로 전환(정렬/갱신용)

> 댓글 작성자 프로필(사진/색/꾸밈 클래스) 노출

> 댓글 작성일/수정일 표기
+ 수정 여부 판단 로직 적용

> 댓글 권한 규칙 적용
(작성자 수정, 작성자/관리자 삭제)

[ 좋아요 ]
> 좋아요 토글 UI/카운트 동기화

> 좋아요 누른 사람 목록 모달/알림 처리