@@ -48,6 +48,8 @@ export default function PostDetail() {
4848 // 댓글 작성 상태
4949 const [ newComment , setNewComment ] = useState ( "" ) ;
5050 const [ posting , setPosting ] = useState ( false ) ;
51+ const [ deletingPost , setDeletingPost ] = useState ( false ) ;
52+ const [ deletingCommentId , setDeletingCommentId ] = useState ( null ) ;
5153
5254 // 대댓글 작성 상태
5355 const [ replyTarget , setReplyTarget ] = useState ( null ) ; // 대댓글을 달 댓글 ID
@@ -58,6 +60,19 @@ export default function PostDetail() {
5860 const authHeader = tokenRaw
5961 ? tokenRaw . startsWith ( "Bearer " ) ? tokenRaw : `Bearer ${ tokenRaw } `
6062 : null ;
63+ const currentUserId = useMemo ( ( ) => localStorage . getItem ( "userId" ) || "" , [ ] ) ;
64+ const currentUsername = useMemo ( ( ) => localStorage . getItem ( "username" ) || "" , [ ] ) ;
65+ const currentRole = useMemo ( ( ) => ( localStorage . getItem ( "role" ) || "" ) . toUpperCase ( ) , [ ] ) ;
66+ const hasManageRole = useMemo ( ( ) => [ "ADMIN" , "MANAGER" , "ROLE_ADMIN" , "ROLE_MANAGER" ] . includes ( currentRole ) , [ currentRole ] ) ;
67+ const matchesCurrentUser = useCallback ( ( writerName , writerId ) => {
68+ if ( writerId && currentUserId ) return String ( writerId ) === String ( currentUserId ) ;
69+ if ( writerName && currentUsername ) return writerName === currentUsername ;
70+ return false ;
71+ } , [ currentUserId , currentUsername ] ) ;
72+ const canManageRecord = useCallback ( ( writerName , writerId ) => {
73+ if ( hasManageRole ) return true ;
74+ return matchesCurrentUser ( writerName , writerId ) ;
75+ } , [ hasManageRole , matchesCurrentUser ] ) ;
6176
6277 // 좋아요 수 및 내 상태 재조회
6378 const refreshLikeStatus = async ( ) => {
@@ -113,7 +128,8 @@ export default function PostDetail() {
113128 id : data . id ,
114129 title : data . title ,
115130 content : data . content || "" ,
116- author : data . writer || "익명" ,
131+ author : data . writer || data . author || "익명" ,
132+ authorId : data . writerId ?? data . authorId ?? data . userId ?? null ,
117133 date : data . createdAt ? new Date ( data . createdAt ) . toLocaleString ( ) : "" ,
118134 tags : Array . isArray ( data . tags ) ? data . tags : [ ] ,
119135 } ) ;
@@ -176,6 +192,19 @@ export default function PostDetail() {
176192 fetchComments ( ) ;
177193 } , [ id , authHeader , fetchComments ] ) ;
178194
195+ const canDeletePost = useMemo ( ( ) => {
196+ if ( hasManageRole ) return true ;
197+ if ( ! post ) return false ;
198+ return matchesCurrentUser ( post . author , post . authorId ) ;
199+ } , [ hasManageRole , post , matchesCurrentUser ] ) ;
200+
201+ const canDeleteComment = useCallback ( ( comment ) => {
202+ if ( ! comment ) return false ;
203+ const writerName = comment . writer ?? comment . author ?? comment . nickname ;
204+ const writerId = comment . writerId ?? comment . authorId ?? comment . userId ;
205+ return canManageRecord ( writerName , writerId ) ;
206+ } , [ canManageRecord ] ) ;
207+
179208
180209 // 좋아요 토글
181210 const handleToggleLike = async ( ) => {
@@ -304,6 +333,67 @@ export default function PostDetail() {
304333 }
305334 } ;
306335
336+ const handleDeletePost = async ( ) => {
337+ if ( ! authHeader ) {
338+ promptLogin ( ) ;
339+ return ;
340+ }
341+ if ( deletingPost ) return ;
342+ if ( ! window . confirm ( "게시글을 삭제하시겠습니까?" ) ) return ;
343+
344+ try {
345+ setDeletingPost ( true ) ;
346+ const res = await fetch ( `${ config . API_BASE_URL } /api/posts/${ id } ` , {
347+ method : "DELETE" ,
348+ headers : { Authorization : authHeader , Accept : "application/json" } ,
349+ } ) ;
350+
351+ if ( ! res . ok ) {
352+ const text = await res . text ( ) ;
353+ throw new Error ( text || `게시글 삭제 실패 (${ res . status } )` ) ;
354+ }
355+
356+ alert ( "게시글이 삭제되었습니다." ) ;
357+ navigate ( "/community" ) ;
358+ } catch ( e ) {
359+ alert ( e . message || "게시글 삭제에 실패했습니다." ) ;
360+ } finally {
361+ setDeletingPost ( false ) ;
362+ }
363+ } ;
364+
365+ const handleDeleteComment = async ( commentId ) => {
366+ if ( ! authHeader ) {
367+ promptLogin ( ) ;
368+ return ;
369+ }
370+ if ( ! commentId || deletingCommentId === commentId ) return ;
371+ if ( ! window . confirm ( "삭제하시겠습니까?" ) ) return ;
372+
373+ try {
374+ setDeletingCommentId ( commentId ) ;
375+ const res = await fetch ( `${ config . API_BASE_URL } /api/comments/${ commentId } ` , {
376+ method : "DELETE" ,
377+ headers : { Authorization : authHeader , Accept : "application/json" } ,
378+ } ) ;
379+
380+ if ( ! res . ok ) {
381+ const text = await res . text ( ) ;
382+ throw new Error ( text || `댓글 삭제 실패 (${ res . status } )` ) ;
383+ }
384+
385+ if ( replyTarget === commentId ) {
386+ setReplyTarget ( null ) ;
387+ setReplyContent ( "" ) ;
388+ }
389+ await fetchComments ( ) ;
390+ } catch ( e ) {
391+ alert ( e . message || "댓글 삭제에 실패했습니다." ) ;
392+ } finally {
393+ setDeletingCommentId ( null ) ;
394+ }
395+ } ;
396+
307397
308398 if ( loadingPost ) {
309399 return < div className = "post-detail-container" > < div className = "post-detail-left" > < p > 게시글을 불러오는 중입니다…</ p > </ div > </ div > ;
@@ -361,6 +451,15 @@ export default function PostDetail() {
361451 >
362452 🔗
363453 </ button >
454+ { canDeletePost && (
455+ < button
456+ className = "delete-btn"
457+ onClick = { handleDeletePost }
458+ disabled = { deletingPost }
459+ >
460+ { deletingPost ? "삭제 중…" : "삭제" }
461+ </ button >
462+ ) }
364463 </ div >
365464
366465 < div className = "section-divider" />
@@ -420,12 +519,23 @@ export default function PostDetail() {
420519 </ div >
421520
422521 { /* 답글 버튼 */ }
423- < button
424- className = "reply-toggle-btn" // ✅ 클래스 적용
425- onClick = { ( ) => setReplyTarget ( c . id === replyTarget ? null : c . id ) }
426- >
427- 답글
428- </ button >
522+ < div className = "comment-action-row" >
523+ < button
524+ className = "reply-toggle-btn"
525+ onClick = { ( ) => setReplyTarget ( c . id === replyTarget ? null : c . id ) }
526+ >
527+ 답글
528+ </ button >
529+ { canDeleteComment ( c ) && (
530+ < button
531+ className = "comment-delete-btn"
532+ onClick = { ( ) => handleDeleteComment ( c . id ) }
533+ disabled = { deletingCommentId === c . id }
534+ >
535+ { deletingCommentId === c . id ? "삭제 중…" : "삭제" }
536+ </ button >
537+ ) }
538+ </ div >
429539
430540 { /* 대댓글 입력창 */ }
431541 { replyTarget === c . id && (
@@ -460,8 +570,19 @@ export default function PostDetail() {
460570 < ul className = "reply-list" > { /* ✅ 클래스 적용 */ }
461571 { c . replies . map ( ( r ) => (
462572 < li key = { r . id } className = "reply-item" > { /* ✅ 클래스 적용 */ }
463- < b > { r . writer || "익명" } </ b > · { r . createdAt ? new Date ( r . createdAt ) . toLocaleString ( ) : "" }
573+ < div className = "reply-meta" >
574+ < b > { r . writer || "익명" } </ b > · { r . createdAt ? new Date ( r . createdAt ) . toLocaleString ( ) : "" }
575+ </ div >
464576 < div > { r . content } </ div >
577+ { canDeleteComment ( r ) && (
578+ < button
579+ className = "reply-delete-btn"
580+ onClick = { ( ) => handleDeleteComment ( r . id ) }
581+ disabled = { deletingCommentId === r . id }
582+ >
583+ { deletingCommentId === r . id ? "삭제 중…" : "삭제" }
584+ </ button >
585+ ) }
465586 </ li >
466587 ) ) }
467588 </ ul >
0 commit comments