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()}>
+
하위 게시판 생성
+
+
+
+
+ );
+};
+
+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 && (
+
+
+ )}
+
+ {comment.replies && comment.replies.length > 0 && (
+
+ {comment.replies.map((child) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+const CommentSection = ({
+ post,
+ comments,
+ onLike,
+ onBookmark,
+ onAddComment,
+ onUpdateComment,
+ onDeleteComment,
+ onReplyClick,
+ onSubmitReply,
+ replyTargetId,
+ showCommentMenu,
+ setShowCommentMenu,
+}) => {
+ const [commentText, setCommentText] = useState('');
+
+ const handleAdd = () => {
+ onAddComment(commentText);
+ setCommentText('');
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter' && e.ctrlKey) handleAdd();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ 댓글 {comments.length}
+
+
+
+
+
+
+
+
+
+ {comments.length > 0 ? (
+ comments.map((comment) => (
+
+ ))
+ ) : (
+
댓글이 없습니다.
+ )}
+
+
+ );
+};
+
+export default CommentSection;
diff --git a/frontend/src/components/Board/PostDetail/FileAttachmentList.jsx b/frontend/src/components/Board/PostDetail/FileAttachmentList.jsx
new file mode 100644
index 00000000..9ee7c296
--- /dev/null
+++ b/frontend/src/components/Board/PostDetail/FileAttachmentList.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import styles from '../../../pages/PostDetail.module.css';
+import FolderIcon from '../../../assets/boardFolder.svg';
+
+const FileAttachmentList = ({
+ files,
+ isEditMode = false,
+ isNew = false,
+ onRemove,
+ onDownload,
+}) => {
+ if (!files || files.length === 0) return null;
+
+ return (
+
+ {files.map((file, index) => {
+ const key = file.postAttachmentId || `new-${index}`;
+ const fileName = file.originalFilename || file.name;
+ const fileSize = file.size
+ ? `(${(file.size / 1024).toFixed(1)} KB)`
+ : '';
+
+ return (
+
+

!isEditMode && onDownload && onDownload(file)}
+ />
+
+ {fileName}{' '}
+ {isNew && 새파일}
+
+ {fileSize && (
+
{fileSize}
+ )}
+
+ {isEditMode && onRemove && (
+
+ )}
+
+ );
+ })}
+
+ );
+};
+
+export default FileAttachmentList;
diff --git a/frontend/src/components/Board/PostDetail/PostEditForm.jsx b/frontend/src/components/Board/PostDetail/PostEditForm.jsx
new file mode 100644
index 00000000..3ff7821a
--- /dev/null
+++ b/frontend/src/components/Board/PostDetail/PostEditForm.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import styles from '../../../pages/PostDetail.module.css';
+import FileAttachmentList from './FileAttachmentList';
+
+const PostEditForm = ({
+ title,
+ setTitle,
+ content,
+ setContent,
+ editFiles,
+ newFiles,
+ onRemoveExistingFile,
+ onRemoveNewFile,
+ onAddNewFile,
+ onSave,
+ onCancel,
+}) => {
+ return (
+
+ {/* 제목 */}
+
setTitle(e.target.value)}
+ placeholder="제목을 입력하세요"
+ />
+
+ {/* 구분선 */}
+
+
+ {/* 내용 */}
+
+ );
+};
+
+export default PostEditForm;
diff --git a/frontend/src/components/Board/PostDetail/PostView.jsx b/frontend/src/components/Board/PostDetail/PostView.jsx
new file mode 100644
index 00000000..2f68f1fe
--- /dev/null
+++ b/frontend/src/components/Board/PostDetail/PostView.jsx
@@ -0,0 +1,81 @@
+import React 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 { getTimeAgo } from '../../../utils/TimeUtils';
+import FileAttachmentList from './FileAttachmentList';
+
+const PostView = ({
+ post,
+ showMenu,
+ setShowMenu,
+ onEdit,
+ onDelete,
+ onDownload,
+}) => {
+ const authorName =
+ post.author || post.user?.name || post.createdBy?.name || '운영진';
+ const date = post.createdDate || post.createdAt || post.date;
+
+ // 데이터 유효성 검사
+ const hasAttachments = post.attachments && post.attachments.length > 0;
+
+ return (
+ <>
+
+
{post.title}
+
+
+ {showMenu && (
+
+
+
+
+ )}
+
+
+
+
+
+
+

+
+
{authorName}
+
{getTimeAgo(date)}
+
+
+
+ {post.content}
+
+ {hasAttachments && (
+
+
+ 첨부 파일 ({post.attachments.length})
+
+
+
+ )}
+ >
+ );
+};
+
+export default PostView;
diff --git a/frontend/src/components/Board/PostItem.jsx b/frontend/src/components/Board/PostItem.jsx
index d94eaf51..188c243b 100644
--- a/frontend/src/components/Board/PostItem.jsx
+++ b/frontend/src/components/Board/PostItem.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
import styles from './PostItem.module.css';
import ProfileIcon from '../../assets/board_profile.svg';
import BookmarkIcon from '../../assets/boardBookMark.svg';
@@ -8,16 +8,64 @@ import HeartIcon from '../../assets/boardHeart.svg';
import HeartFilledIcon from '../../assets/boardHeart.fill.svg';
import { getTimeAgo } from '../../utils/TimeUtils';
-const PostItem = React.memo(({ post, onLike, onBookmark, currentTeam }) => {
+const PostItem = ({ post, onLike, onBookmark }) => {
const navigate = useNavigate();
+ const { team } = useParams();
+
+ const postId = post.postId || post.id;
const handleCardClick = () => {
- const team = currentTeam || 'all';
- navigate(`/board/${team}/${post.id}`, { state: { post } });
+ if (!postId) {
+ console.error('게시글 ID를 찾을 수 없습니다:', post);
+ alert('게시글 ID를 찾을 수 없습니다.');
+ return;
+ }
+
+ const nameToPath = {
+ 증권1팀: 'securities-1',
+ 증권2팀: 'securities-2',
+ 증권3팀: 'securities-3',
+ 자산운용: 'asset-management',
+ 금융IT: 'finance-it',
+ 매크로: 'macro',
+ 트레이딩: 'trading',
+ };
+
+ const boardName = post.boardName || post.board?.boardName;
+ const teamPath = nameToPath[boardName] || team;
+
+ if (!teamPath) {
+ alert('게시판 정보를 찾을 수 없습니다.');
+ return;
+ }
+
+ const path = `/board/${teamPath}/post/${postId}`;
+
+ navigate(path, { state: { post } });
+ };
+
+ const handleBookmarkClick = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (!postId) {
+ console.error('북마크 실패: 게시글 ID 없음');
+ return;
+ }
+
+ onBookmark(postId);
};
- const handleActionClick = (e) => {
+ const handleLikeClick = (e) => {
e.stopPropagation();
+ e.preventDefault();
+
+ if (!postId) {
+ console.error('좋아요 실패: 게시글 ID 없음');
+ return;
+ }
+
+ onLike(postId);
};
return (
@@ -38,10 +86,10 @@ const PostItem = React.memo(({ post, onLike, onBookmark, currentTeam }) => {
{post.content}
-
+
);
-});
+};
+
+PostItem.displayName = 'PostItem';
-export default PostItem;
+export default React.memo(PostItem);
diff --git a/frontend/src/components/Board/SearchBar.jsx b/frontend/src/components/Board/SearchBar.jsx
index d178ccc1..6496bb28 100644
--- a/frontend/src/components/Board/SearchBar.jsx
+++ b/frontend/src/components/Board/SearchBar.jsx
@@ -1,15 +1,17 @@
-import React from 'react';
+import React, { useState } from 'react';
import styles from './SearchBar.module.css';
import SearchArrowIcon from '../../assets/boardSearchArrow.svg';
-const SearchBar = ({ searchTerm, onSearchChange }) => {
+const SearchBar = ({ onSearch }) => {
+ const [inputValue, setInputValue] = useState('');
+
const handleSearch = () => {
- if (searchTerm.trim()) {
- console.log('검색어:', searchTerm);
+ if (inputValue.trim()) {
+ onSearch(inputValue);
}
};
- const handleKeyPress = (e) => {
+ const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleSearch();
}
@@ -20,18 +22,13 @@ const SearchBar = ({ searchTerm, onSearchChange }) => {
onSearchChange(e.target.value)}
- onKeyPress={handleKeyPress}
+ value={inputValue}
+ onChange={(e) => setInputValue(e.target.value)}
+ onKeyDown={handleKeyDown}
className={styles.searchInput}
- aria-label="검색"
/>
-