diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b959b7e1..15624fe8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -41,6 +41,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Board/BoardActions.module.css b/frontend/src/components/Board/BoardActions.module.css index 5296ab57..051d799d 100644 --- a/frontend/src/components/Board/BoardActions.module.css +++ b/frontend/src/components/Board/BoardActions.module.css @@ -21,8 +21,8 @@ } .boardActions { - width: 908px; - max-width: calc(100% - 20px); + width: 100%; + max-width: 100%; box-sizing: border-box; display: flex; align-items: center; diff --git a/frontend/src/components/Board/CategoryTabs.jsx b/frontend/src/components/Board/CategoryTabs.jsx new file mode 100644 index 00000000..0a6aa347 --- /dev/null +++ b/frontend/src/components/Board/CategoryTabs.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import styles from './CategoryTabs.module.css'; + +const CategoryTabs = ({ activeTab, onTabChange, tabs, onCreateSubBoard }) => { + return ( +
+
+ {tabs?.map((tab) => ( + + ))} +
+ + +
+ ); +}; + +export default CategoryTabs; diff --git a/frontend/src/components/Board/CategoryTabs.module.css b/frontend/src/components/Board/CategoryTabs.module.css new file mode 100644 index 00000000..5c579b99 --- /dev/null +++ b/frontend/src/components/Board/CategoryTabs.module.css @@ -0,0 +1,72 @@ +.categoryTabsRow { + display: flex; + justify-content: space-between; + align-items: center; + margin: 30px 0 20px 0; + width: 100%; +} + +.tabsLeft { + display: flex; + gap: 10px; +} + +.tab { + height: 43px; + padding: 12px 20px; + background: #ffffff; + border: 1px solid #d4d4d4; + border-radius: 8px; + + /* 폰트 스타일 */ + font-size: 16px; + font-weight: 600; + font-style: normal; + line-height: 100%; + letter-spacing: 0; + color: rgba(23, 23, 23, 1); + + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + display: flex; + align-items: center; + justify-content: center; +} + +.tab:hover { + background: #f5f5f5; + border-color: #a8a8a8; +} + +.tab.active { + background: rgba(238, 249, 255, 1); + border: 1px solid transparent; + color: rgba(29, 128, 244, 1); + font-weight: 600; + font-size: 16px; + line-height: 100%; + letter-spacing: 0; +} + +/* 하위 게시판 추가 버튼 */ +.subBoardButton { + height: 43px; + padding: 12px 20px; + border-radius: 8px; + border: 1px solid #d4d4d4; + background: #ffffff; + + font-size: 14px; + font-weight: 500; + color: rgba(23, 23, 23, 1); + + cursor: pointer; + white-space: nowrap; + transition: all 0.2s; +} + +.subBoardButton:hover { + background: #f5f5f5; + border-color: #a8a8a8; +} diff --git a/frontend/src/components/Board/CreateSubBoardModal.jsx b/frontend/src/components/Board/CreateSubBoardModal.jsx new file mode 100644 index 00000000..7571637a --- /dev/null +++ b/frontend/src/components/Board/CreateSubBoardModal.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import styles from './CreateSubBoardModal.module.css'; + +const CreateSubBoardModal = ({ value, onChange, onSave, onClose }) => { + const handleSubmit = (e) => { + e.preventDefault(); + onSave(); + }; + + return ( +
+
e.stopPropagation()}> +

하위 게시판 생성

+ +
+ onChange(e.target.value)} + placeholder="하위 게시판 이름을 입력하세요" + className={styles.input} + autoFocus + /> + +
+ + +
+
+
+
+ ); +}; + +export default CreateSubBoardModal; diff --git a/frontend/src/components/Board/CreateSubBoardModal.module.css b/frontend/src/components/Board/CreateSubBoardModal.module.css new file mode 100644 index 00000000..bfe01a77 --- /dev/null +++ b/frontend/src/components/Board/CreateSubBoardModal.module.css @@ -0,0 +1,79 @@ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modalContent { + background: white; + border-radius: 12px; + padding: 32px; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +.modalTitle { + font-size: 24px; + font-weight: 600; + margin-bottom: 24px; + color: #171717; +} + +.input { + width: 100%; + padding: 12px 16px; + font-size: 16px; + border: 1px solid #d4d4d4; + border-radius: 8px; + margin-bottom: 24px; + box-sizing: border-box; +} + +.input:focus { + outline: none; + border-color: #4a90e2; +} + +.buttonGroup { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.cancelButton, +.saveButton { + padding: 10px 24px; + font-size: 16px; + font-weight: 500; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; +} + +.cancelButton { + background: #f5f5f5; + border: 1px solid #d4d4d4; + color: #171717; +} + +.cancelButton:hover { + background: #e5e5e5; +} + +.saveButton { + background: #4a90e2; + border: none; + color: white; +} + +.saveButton:hover { + background: #3a80d2; +} diff --git a/frontend/src/components/Board/Modal.module.css b/frontend/src/components/Board/Modal.module.css index 447bc5c2..af017752 100644 --- a/frontend/src/components/Board/Modal.module.css +++ b/frontend/src/components/Board/Modal.module.css @@ -34,7 +34,7 @@ display: flex; flex-direction: column; position: relative; - overflow: hidden; /* ✨ auto → hidden으로 변경 (모서리 유지) */ + overflow: hidden; } .header { @@ -43,7 +43,7 @@ justify-content: space-between; align-items: center; margin-bottom: var(--spacing-md); - flex-shrink: 0; /* ✨ 헤더 고정 */ + flex-shrink: 0; } .title { @@ -70,14 +70,13 @@ display: flex; flex-direction: column; gap: var(--spacing-md); - flex: 1; /* ✨ 0 1 auto → 1로 변경 (남은 공간 차지) */ + flex: 1; margin-bottom: var(--spacing-sm); - overflow-y: auto; /* ✨ 스크롤 추가 */ + overflow-y: auto; overflow-x: hidden; - padding-right: 8px; /* ✨ 스크롤바 공간 */ + padding-right: 8px; } -/* ✨ 스크롤바 스타일 추가 */ .form::-webkit-scrollbar { width: 6px; } @@ -168,7 +167,6 @@ flex-shrink: 0; } -/* 파일 목록 */ .fileList { margin-top: 12px; } @@ -187,7 +185,7 @@ font-size: 14px; color: #333; flex: 1; - word-break: break-all; /* ✨ 긴 파일명 줄바꿈 */ + word-break: break-all; } .removeFileButton { @@ -198,7 +196,7 @@ cursor: pointer; padding: 0 8px; transition: color 0.2s; - flex-shrink: 0; /* ✨ 버튼 크기 고정 */ + flex-shrink: 0; } .removeFileButton:hover { @@ -319,7 +317,7 @@ background: rgba(29, 128, 244, 1); color: rgba(255, 255, 255, 1); align-self: flex-end; - flex-shrink: 0; /* ✨ 버튼 고정 */ + flex-shrink: 0; transition: all 0.2s; } diff --git a/frontend/src/components/Board/PostDetail/CommentSection.jsx b/frontend/src/components/Board/PostDetail/CommentSection.jsx new file mode 100644 index 00000000..faf03e2a --- /dev/null +++ b/frontend/src/components/Board/PostDetail/CommentSection.jsx @@ -0,0 +1,230 @@ +import React, { useState } from 'react'; +import styles from '../../../pages/PostDetail.module.css'; +import ProfileIcon from '../../../assets/board_profile.svg'; +import EditIcon from '../../../assets/boardPencil.svg'; +import DeleteIcon from '../../../assets/boardCloseIcon.svg'; +import BookmarkIcon from '../../../assets/boardBookMark.svg'; +import BookmarkFilledIcon from '../../../assets/boardBookMark.fill.svg'; +import HeartIcon from '../../../assets/boardHeart.svg'; +import HeartFilledIcon from '../../../assets/boardHeart.fill.svg'; +import { getTimeAgo } from '../../../utils/TimeUtils'; +const CommentItem = ({ + comment, + onReplyClick, + onUpdateComment, + onDeleteComment, + isReplying, + onSubmitReply, + showCommentMenu, + setShowCommentMenu, + replyTargetId, + depth = 0, +}) => { + const commentId = comment.commentId || comment.id; + const author = + comment.author || comment.user?.name || comment.createdBy?.name || '사용자'; + const text = comment.text || comment.content; + const date = comment.createdDate || comment.createdAt || comment.date; + + const [localReplyText, setLocalReplyText] = useState(''); + + React.useEffect(() => { + if (!isReplying) setLocalReplyText(''); + }, [isReplying]); + + const handleLocalSubmit = () => { + onSubmitReply(commentId, localReplyText); + }; + + return ( +
+
+
+
+ 프로필 +

{author}

+
+

{getTimeAgo(date)}

+ +
+ + {showCommentMenu === commentId && ( +
+ + +
+ )} +
+
+ +

{text}

+ + {depth === 0 && ( + + )} +
+ + {depth === 0 && isReplying && ( +
+