From 160572ba732fa6ef658b98a14627fcb958a9ead6 Mon Sep 17 00:00:00 2001 From: sangkyu Date: Thu, 5 Mar 2026 18:03:41 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[FE]=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Board/CategoryTabs.jsx | 2 +- frontend/src/components/Board/Modal.jsx | 16 +- frontend/src/components/Board/PostItem.jsx | 33 +- frontend/src/components/Sidebar.jsx | 142 +++++-- frontend/src/components/Sidebar.module.css | 56 ++- frontend/src/pages/Board.jsx | 354 +++++++++--------- frontend/src/utils/boardApi.js | 27 +- frontend/src/utils/boardRoute.js | 54 +++ 8 files changed, 425 insertions(+), 259 deletions(-) create mode 100644 frontend/src/utils/boardRoute.js diff --git a/frontend/src/components/Board/CategoryTabs.jsx b/frontend/src/components/Board/CategoryTabs.jsx index 0a6aa347..3d98f10a 100644 --- a/frontend/src/components/Board/CategoryTabs.jsx +++ b/frontend/src/components/Board/CategoryTabs.jsx @@ -17,7 +17,7 @@ const CategoryTabs = ({ activeTab, onTabChange, tabs, onCreateSubBoard }) => { ); diff --git a/frontend/src/components/Board/Modal.jsx b/frontend/src/components/Board/Modal.jsx index e294189f..025eaa89 100644 --- a/frontend/src/components/Board/Modal.jsx +++ b/frontend/src/components/Board/Modal.jsx @@ -8,6 +8,9 @@ const Modal = ({ setTitle, content, setContent, + boardOptions, + selectedBoardId, + onBoardChange, selectedFiles, onFileChange, onRemoveFile, @@ -104,8 +107,17 @@ const Modal = ({
- onBoardChange?.(e.target.value)} + > + + {(boardOptions || []).map((board) => ( + + ))}
드롭다운 화살표 diff --git a/frontend/src/components/Board/PostItem.jsx b/frontend/src/components/Board/PostItem.jsx index 188c243b..ad5fedf9 100644 --- a/frontend/src/components/Board/PostItem.jsx +++ b/frontend/src/components/Board/PostItem.jsx @@ -7,12 +7,18 @@ 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'; +import { toBoardRouteSegment } from '../../utils/boardRoute'; const PostItem = ({ post, onLike, onBookmark }) => { const navigate = useNavigate(); const { team } = useParams(); const postId = post.postId || post.id; + const boardName = post.boardName || post.board?.boardName; + const authorName = post.user?.name || post.userName || '익명'; + const createdAt = post.createdDate || post.createdAt || post.date; + const likeCount = Number(post.likeCount || 0); + const bookmarkCount = Number(post.bookmarkCount || 0); const handleCardClick = () => { if (!postId) { @@ -21,25 +27,14 @@ const PostItem = ({ post, onLike, onBookmark }) => { 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; + const teamPath = toBoardRouteSegment(boardName) || team; if (!teamPath) { alert('게시판 정보를 찾을 수 없습니다.'); return; } - const path = `/board/${teamPath}/post/${postId}`; + const path = `/board/${encodeURIComponent(teamPath)}/post/${postId}`; navigate(path, { state: { post } }); }; @@ -78,8 +73,8 @@ const PostItem = ({ post, onLike, onBookmark }) => {
- 운영진 - {getTimeAgo(post.date)} + {authorName} + {getTimeAgo(createdAt)}
{post.title}
@@ -96,7 +91,9 @@ const PostItem = ({ post, onLike, onBookmark }) => { src={post.isBookmarked ? BookmarkFilledIcon : BookmarkIcon} alt="북마크" /> - {post.isBookmarked && 1} + {bookmarkCount > 0 && ( + {bookmarkCount} + )}
diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 2671a619..f3e2a924 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -5,31 +5,60 @@ import { useState, useEffect } from 'react'; import { api } from '../utils/axios'; import { toast } from 'react-toastify'; import { useAuth } from '../contexts/AuthContext'; +import { getParentBoards } from '../utils/boardApi'; +import { isAllBoardName, toBoardPath } from '../utils/boardRoute'; const Sidebar = ({ isOpen, isRoot, onClose }) => { const nav = useNavigate(); const location = useLocation(); - - const boardList = [ - { name: '전체 게시판', path: '/board' }, - { name: '증권1팀 게시판', path: '/board/securities-1' }, - { name: '증권2팀 게시판', path: '/board/securities-2' }, - { name: '증권3팀 게시판', path: '/board/securities-3' }, - { name: '자산운용팀 게시판', path: '/board/asset-management' }, - { name: '금융IT팀 게시판', path: '/board/finance-it' }, - { name: '매크로팀 게시판', path: '/board/macro' }, - { name: '트레이딩팀 게시판', path: '/board/trading' }, - ]; - - const currentBoard = boardList.find( - (item) => item.path === location.pathname - ); - const [selectedBoard, setSelectedBoard] = useState( - currentBoard?.name || '전체 게시판' - ); + const [boardList, setBoardList] = useState([]); + const [selectedBoard, setSelectedBoard] = useState(''); + const [isBoardMenuOpen, setIsBoardMenuOpen] = useState(false); const { isLoggedIn, logout } = useAuth(); const [isPresident, setIsPresident] = useState(false); + useEffect(() => { + const loadParentBoards = async () => { + try { + const boards = await getParentBoards(); + const mappedBoards = (Array.isArray(boards) ? boards : []).map((board) => ({ + name: isAllBoardName(board.boardName) + ? '전체 게시판' + : String(board.boardName || '').includes('게시판') + ? board.boardName + : `${board.boardName} 게시판`, + path: toBoardPath(board.boardName), + })); + + const uniqueBoards = mappedBoards.filter( + (item, index, array) => + item.path && array.findIndex((candidate) => candidate.path === item.path) === index + ); + + uniqueBoards.sort((a, b) => { + if (a.path === '/board') return -1; + if (b.path === '/board') return 1; + return 0; + }); + + setBoardList(uniqueBoards); + } catch { + setBoardList([{ name: '전체 게시판', path: '/board' }]); + } + }; + + loadParentBoards(); + }, []); + + useEffect(() => { + if (!boardList.length) return; + const currentPath = decodeURIComponent(location.pathname); + const currentBoard = boardList.find( + (item) => decodeURIComponent(item.path) === currentPath + ); + setSelectedBoard(currentBoard?.name || boardList[0].name); + }, [boardList, location.pathname]); + useEffect(() => { const checkAdminRole = async () => { if (!isLoggedIn) { @@ -62,6 +91,27 @@ const Sidebar = ({ isOpen, isRoot, onClose }) => { } }; + const handleBoardSelect = (board) => { + if (!board?.path) return; + + setSelectedBoard(board.name); + setIsBoardMenuOpen(false); + + const currentPath = decodeURIComponent(location.pathname); + const targetPath = decodeURIComponent(board.path); + + if (currentPath === targetPath) { + nav(board.path, { + replace: false, + state: { boardSwitchAt: Date.now() }, + }); + } else { + nav(board.path); + } + + handleNavLinkClick(); + }; + return ( <> {/* 모바일 오버레이 */} @@ -80,27 +130,43 @@ const Sidebar = ({ isOpen, isRoot, onClose }) => {
게시판 - + + + {isBoardMenuOpen && ( +
    + {boardList.length > 0 ? ( + boardList.map((item) => ( +
  • + +
  • + )) + ) : ( +
  • 게시판 로딩 중...
  • + )} +
+ )} +
diff --git a/frontend/src/components/Sidebar.module.css b/frontend/src/components/Sidebar.module.css index 87643f85..1be92afb 100644 --- a/frontend/src/components/Sidebar.module.css +++ b/frontend/src/components/Sidebar.module.css @@ -159,21 +159,63 @@ height: 19px; } -.boardSelect { +.boardMenu { + position: relative; + width: 100%; + margin-top: 6px; +} + +.boardMenuTrigger { width: 100%; background: transparent; color: #ffffff; - border: none; + border: 1px solid #2f3442; padding: 8px 3px; border-radius: 6px; - margin-top: 6px; + text-align: left; text-decoration: none; font-weight: 500; - font-size: 16px; + font-size: 15px; cursor: pointer; } -.boardSelect option { - color: black; - background: white; +.boardMenuList { + list-style: none; + margin: 8px 0 0 0; + padding: 6px; + border: 1px solid #2f3442; + border-radius: 8px; + background: #0d111c; + display: flex; + flex-direction: column; + gap: 2px; +} + +.boardMenuItem { + width: 100%; + background: transparent; + border: none; + border-radius: 6px; + padding: 8px 6px; + text-align: left; + color: #9da7bd; + font-size: 14px; + font-weight: 500; + cursor: pointer; +} + +.boardMenuItem:hover { + background: #1a2235; + color: #c9d3ea; +} + +.boardMenuItemActive { + color: #ffffff; + background: #1a2235; +} + +.boardMenuLoading { + color: #8a94a8; + font-size: 13px; + padding: 8px 6px; } diff --git a/frontend/src/pages/Board.jsx b/frontend/src/pages/Board.jsx index c2b34ccc..0a0b556d 100644 --- a/frontend/src/pages/Board.jsx +++ b/frontend/src/pages/Board.jsx @@ -8,6 +8,27 @@ import CategoryTabs from '../components/Board/CategoryTabs'; import CreateSubBoardModal from '../components/Board/CreateSubBoardModal'; import styles from './Board.module.css'; import * as boardApi from '../utils/boardApi'; +import { isAllBoardName, toBoardRouteSegment } from '../utils/boardRoute'; + +const ALL_TAB_ID = 'all'; + +const getPostId = (post) => post?.postId || post?.id; + +const mergePosts = (responses = []) => { + const map = new Map(); + + responses.forEach((response) => { + (response?.content || []).forEach((post) => { + const id = getPostId(post); + if (!id) return; + if (!map.has(id)) { + map.set(id, post); + } + }); + }); + + return Array.from(map.values()); +}; const Board = () => { const { team } = useParams(); @@ -23,36 +44,32 @@ const Board = () => { const [sortOption, setSortOption] = useState('latest'); const [loading, setLoading] = useState(false); const [boardsLoaded, setBoardsLoaded] = useState(false); - const [activeSubBoard, setActiveSubBoard] = useState('all'); + const [activeSubBoard, setActiveSubBoard] = useState(ALL_TAB_ID); const [showSubBoardModal, setShowSubBoardModal] = useState(false); const [subBoardName, setSubBoardName] = useState(''); - const [subBoardTabs, setSubBoardTabs] = useState([]); + const [subBoardTabs, setSubBoardTabs] = useState([{ id: ALL_TAB_ID, name: '전체 게시판' }]); + const [writeBoardId, setWriteBoardId] = useState(''); - // 페이지네이션 state const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 4; const prevPostsRef = useRef(posts); - const currentPath = team ? `/board/${team}` : '/board'; - const currentBoardId = boardIdMap[currentPath]; - const currentBoardName = boardNameMap[currentPath]; + const currentSegment = team ? decodeURIComponent(team) : 'root'; + const currentBoardId = boardIdMap[currentSegment]; + const currentBoardName = boardNameMap[currentSegment] || '게시판'; useEffect(() => { prevPostsRef.current = posts; }, [posts]); - useEffect(() => { - loadBoardList(); - }, []); - - const loadBoardList = async () => { + const loadBoardList = useCallback(async () => { try { const boards = await boardApi.getParentBoards(); if (!boards || boards.length === 0) { - console.warn('게시판이 없습니다.'); - alert('등록된 게시판이 없습니다.'); + setBoardIdMap({}); + setBoardNameMap({}); setBoardsLoaded(true); return; } @@ -60,25 +77,16 @@ const Board = () => { const idMap = {}; const nameMap = {}; - const nameToPath = { - 증권1팀: 'securities-1', - 증권2팀: 'securities-2', - 증권3팀: 'securities-3', - 자산운용: 'asset-management', - 금융IT: 'finance-it', - 매크로: 'macro', - 트레이딩: 'trading', - }; - boards.forEach((board) => { - const boardName = board.boardName; - const path = - boardName === '전체' ? '/board' : `/board/${nameToPath[boardName]}`; + const boardName = String(board.boardName || '').trim(); + const segment = isAllBoardName(boardName) + ? 'root' + : toBoardRouteSegment(boardName); - if (path) { - idMap[path] = board.boardId; - nameMap[path] = boardName; - } + if (!segment) return; + + idMap[segment] = board.boardId; + nameMap[segment] = boardName; }); setBoardIdMap(idMap); @@ -88,150 +96,116 @@ const Board = () => { console.error('게시판 목록 불러오기 실패:', error); setBoardsLoaded(true); } - }; + }, []); + + useEffect(() => { + loadBoardList(); + }, [loadBoardList]); - const fetchPosts = useCallback(async () => { - if (!currentBoardId) return; + const fetchPostsByBoardIds = useCallback(async (boardIds, keyword = '') => { + const uniqueBoardIds = Array.from(new Set((boardIds || []).filter(Boolean))); + + if (uniqueBoardIds.length === 0) { + setPosts([]); + setCurrentPage(1); + return; + } try { setLoading(true); - if (Array.isArray(currentBoardId)) { - const allPostsPromises = currentBoardId.map((id) => - boardApi.getPosts(id).catch((err) => { - console.error(`게시판 ${id} 조회 실패:`, err); - return { content: [] }; - }) - ); - - const allPostsArrays = await Promise.all(allPostsPromises); - const allPosts = allPostsArrays.flatMap( - (response) => response.content || [] - ); - - setPosts(allPosts); - } else { - const response = await boardApi.getPosts(currentBoardId); - const postsData = response.content || []; - setPosts(Array.isArray(postsData) ? postsData : []); - } + const requests = uniqueBoardIds.map((boardId) => + keyword && keyword.trim() + ? boardApi.searchPosts(boardId, keyword).catch(() => ({ content: [] })) + : boardApi.getPosts(boardId).catch(() => ({ content: [] })) + ); - // 게시글 로드 후 페이지를 1로 리셋 + const responses = await Promise.all(requests); + const merged = mergePosts(responses); + setPosts(merged); setCurrentPage(1); } catch (error) { - console.error('게시글 불러오기 실패:', error); + console.error('게시글 조회 실패:', error); setPosts([]); } finally { setLoading(false); } - }, [currentBoardId]); + }, []); - const handleSearch = useCallback( - async (keyword) => { - if (!currentBoardId) return; + const getBoardIdsForTab = useCallback( + (tabId) => { + if (!currentBoardId) return []; - if (!keyword || !keyword.trim()) { - fetchPosts(); - return; + if (tabId && tabId !== ALL_TAB_ID) { + return [tabId]; } - try { - setLoading(true); - - if (Array.isArray(currentBoardId)) { - const allPostsPromises = currentBoardId.map((id) => - boardApi.searchPosts(id, keyword).catch(() => ({ content: [] })) - ); - - const allPostsArrays = await Promise.all(allPostsPromises); - const allPosts = allPostsArrays.flatMap( - (response) => response.content || [] - ); + const childBoardIds = subBoardTabs + .filter((tab) => tab.id !== ALL_TAB_ID) + .map((tab) => tab.id); - setPosts(allPosts); - } else { - const response = await boardApi.searchPosts(currentBoardId, keyword); - const postsData = response.content || []; - setPosts(postsData); - } - - // 검색 후 페이지를 1로 리셋 - setCurrentPage(1); - } catch (error) { - console.error('검색 실패:', error); - alert('검색에 실패했습니다.'); - } finally { - setLoading(false); - } + return [currentBoardId, ...childBoardIds]; }, - [currentBoardId, fetchPosts] + [currentBoardId, subBoardTabs] ); useEffect(() => { - const loadSubBoards = async () => { - if (currentBoardId && !Array.isArray(currentBoardId)) { - try { - const subBoards = await boardApi.getSubBoards(currentBoardId); + const loadSubBoardsAndPosts = async () => { + if (!boardsLoaded || !currentBoardId) return; - if (subBoards && subBoards.length > 0) { - const tabs = subBoards.map((board) => ({ + try { + const subBoards = await boardApi.getSubBoards(currentBoardId); + const tabs = [{ id: ALL_TAB_ID, name: '전체 게시판' }]; + + if (Array.isArray(subBoards) && subBoards.length > 0) { + tabs.push( + ...subBoards.map((board) => ({ id: board.boardId, name: board.boardName, - })); - setSubBoardTabs(tabs); - setActiveSubBoard(tabs[0].id); - - const response = await boardApi.getPosts(tabs[0].id); - const postsData = response.content || []; - setPosts(Array.isArray(postsData) ? postsData : []); - setCurrentPage(1); - } else { - setSubBoardTabs([]); - setActiveSubBoard(null); - fetchPosts(); - } - } catch (error) { - console.error('하위 게시판 조회 실패:', error); - setSubBoardTabs([]); - setActiveSubBoard(null); - fetchPosts(); + })) + ); } - } else { - setSubBoardTabs([]); - setActiveSubBoard(null); - fetchPosts(); + + setSubBoardTabs(tabs); + setActiveSubBoard(ALL_TAB_ID); + + const allBoardIds = [currentBoardId, ...tabs.filter((tab) => tab.id !== ALL_TAB_ID).map((tab) => tab.id)]; + await fetchPostsByBoardIds(allBoardIds); + } catch (error) { + console.error('하위 게시판 조회 실패:', error); + setSubBoardTabs([{ id: ALL_TAB_ID, name: '전체 게시판' }]); + setActiveSubBoard(ALL_TAB_ID); + await fetchPostsByBoardIds([currentBoardId]); } }; - if (boardsLoaded && currentBoardId) { - loadSubBoards(); - } - }, [boardsLoaded, currentBoardId, fetchPosts]); + loadSubBoardsAndPosts(); + }, [boardsLoaded, currentBoardId, fetchPostsByBoardIds]); const handleTabChange = useCallback( async (tabId) => { setActiveSubBoard(tabId); + const targetBoardIds = getBoardIdsForTab(tabId); + await fetchPostsByBoardIds(targetBoardIds); + }, + [fetchPostsByBoardIds, getBoardIdsForTab] + ); - if (!currentBoardId) return; - - try { - setLoading(true); - - const response = await boardApi.getPosts(tabId); - const postsData = response.content || []; - setPosts(Array.isArray(postsData) ? postsData : []); - setCurrentPage(1); // 탭 변경 시 페이지 리셋 - } catch (error) { - console.error('탭 변경 중 오류:', error); - setPosts([]); - } finally { - setLoading(false); - } + const handleSearch = useCallback( + async (keyword) => { + const targetBoardIds = getBoardIdsForTab(activeSubBoard); + await fetchPostsByBoardIds(targetBoardIds, keyword); }, - [currentBoardId] + [activeSubBoard, fetchPostsByBoardIds, getBoardIdsForTab] ); const handleOpenModal = () => { + if (activeSubBoard && activeSubBoard !== ALL_TAB_ID) { + setWriteBoardId(activeSubBoard); + } else { + const firstSubBoardId = subBoardTabs.find((tab) => tab.id !== ALL_TAB_ID)?.id; + setWriteBoardId(firstSubBoardId || currentBoardId || ''); + } setShowModal(true); }; @@ -240,6 +214,7 @@ const Board = () => { setTitle(''); setContent(''); setSelectedFiles([]); + setWriteBoardId(''); }; const handleOpenSubBoardModal = () => { @@ -257,21 +232,38 @@ const Board = () => { return; } + if (!currentBoardId) { + alert('부모 게시판 정보를 찾을 수 없습니다.'); + return; + } + try { - await boardApi.createBoard(subBoardName, currentBoardId); - alert('하위 게시판이 생성되었습니다!'); + const created = await boardApi.createSubBoard( + subBoardName.trim(), + currentBoardId + ); handleCloseSubBoardModal(); const subBoards = await boardApi.getSubBoards(currentBoardId); - const tabs = subBoards.map((board) => ({ - id: board.boardId, - name: board.boardName, - })); + const tabs = [ + { id: ALL_TAB_ID, name: '전체 게시판' }, + ...(Array.isArray(subBoards) + ? subBoards.map((board) => ({ id: board.boardId, name: board.boardName })) + : []), + ]; + setSubBoardTabs(tabs); - if (tabs.length > 0) { - setActiveSubBoard(tabs[0].id); - } + const nextTabId = created?.boardId || ALL_TAB_ID; + setActiveSubBoard(nextTabId); + + const targetBoardIds = + nextTabId === ALL_TAB_ID + ? [currentBoardId, ...tabs.filter((tab) => tab.id !== ALL_TAB_ID).map((tab) => tab.id)] + : [nextTabId]; + + await fetchPostsByBoardIds(targetBoardIds); + alert('하위 게시판이 생성되었습니다!'); } catch (error) { console.error('하위 게시판 생성 실패:', error); alert('하위 게시판 생성에 실패했습니다.'); @@ -293,16 +285,6 @@ const Board = () => { return; } - if (subBoardTabs.length === 0) { - alert('하위 게시판이 없습니다.\n먼저 하위 게시판을 생성해주세요.'); - return; - } - - if (!activeSubBoard) { - alert('하위 게시판을 선택해주세요.'); - return; - } - if (!title || !title.trim()) { alert('제목을 입력해주세요.'); return; @@ -313,6 +295,14 @@ const Board = () => { return; } + const targetBoardId = writeBoardId || + (activeSubBoard === ALL_TAB_ID ? currentBoardId : activeSubBoard); + + if (!targetBoardId || targetBoardId === ALL_TAB_ID) { + alert('작성할 하위 게시판을 선택해주세요.'); + return; + } + try { const postData = { title: title.trim(), @@ -320,19 +310,18 @@ const Board = () => { files: selectedFiles, }; - await boardApi.createPost(activeSubBoard, postData); + await boardApi.createPost(targetBoardId, postData); handleCloseModal(); setSelectedFiles([]); - handleTabChange(activeSubBoard); + const reloadBoardIds = getBoardIdsForTab(activeSubBoard); + await fetchPostsByBoardIds(reloadBoardIds); alert('게시글이 작성되었습니다!'); } catch (error) { console.error('게시글 작성 실패:', error); - alert( - `게시글 작성에 실패했습니다: ${error.message || '알 수 없는 오류'}` - ); + alert(`게시글 작성에 실패했습니다: ${error.message || '알 수 없는 오류'}`); } }; @@ -341,11 +330,13 @@ const Board = () => { setPosts((currentPosts) => currentPosts.map((post) => { - if ((post.postId || post.id) === postId) { + if (getPostId(post) === postId) { return { ...post, isLiked: !post.isLiked, - likeCount: post.isLiked ? post.likeCount - 1 : post.likeCount + 1, + likeCount: post.isLiked + ? Math.max((post.likeCount || 1) - 1, 0) + : (post.likeCount || 0) + 1, }; } return post; @@ -366,12 +357,12 @@ const Board = () => { setPosts((currentPosts) => currentPosts.map((post) => { - if ((post.postId || post.id) === postId) { + if (getPostId(post) === postId) { return { ...post, isBookmarked: !post.isBookmarked, bookmarkCount: post.isBookmarked - ? (post.bookmarkCount || 1) - 1 + ? Math.max((post.bookmarkCount || 1) - 1, 0) : (post.bookmarkCount || 0) + 1, }; } @@ -388,7 +379,6 @@ const Board = () => { } }, []); - // 정렬 옵션 변경 핸들러 (페이지 리셋 포함) const handleSortChange = (option) => { setSortOption(option); setCurrentPage(1); @@ -406,18 +396,16 @@ const Board = () => { return dateA - dateB; } if (sortOption === 'popular') { - return b.likeCount - a.likeCount; + return (b.likeCount || 0) - (a.likeCount || 0); } return 0; }); - // 페이지네이션 계산 const indexOfLastPost = currentPage * itemsPerPage; const indexOfFirstPost = indexOfLastPost - itemsPerPage; const currentPosts = sortedPosts.slice(indexOfFirstPost, indexOfLastPost); const totalPages = Math.ceil(sortedPosts.length / itemsPerPage); - // 페이지 변경 핸들러 const handlePageChange = (pageNumber) => { setCurrentPage(pageNumber); window.scrollTo({ top: 0, behavior: 'smooth' }); @@ -437,9 +425,7 @@ const Board = () => {

게시판

-

- 게시판을 찾을 수 없습니다. (경로: {currentPath}) -

+

게시판을 찾을 수 없습니다. (경로: {currentSegment})

); } @@ -452,14 +438,12 @@ const Board = () => { - {!Array.isArray(currentBoardId) && ( - - )} + { ) : currentPosts.length > 0 ? ( currentPosts.map((post) => ( { )}
- {/* 페이지네이션 컨트롤 */} {!loading && sortedPosts.length > 0 && (
@@ -523,6 +504,9 @@ const Board = () => { setTitle={setTitle} content={content} setContent={setContent} + boardOptions={subBoardTabs.filter((tab) => tab.id !== ALL_TAB_ID)} + selectedBoardId={writeBoardId} + onBoardChange={setWriteBoardId} selectedFiles={selectedFiles} onFileChange={handleFileChange} onRemoveFile={handleRemoveFile} diff --git a/frontend/src/utils/boardApi.js b/frontend/src/utils/boardApi.js index 3bf86e35..564934e2 100644 --- a/frontend/src/utils/boardApi.js +++ b/frontend/src/utils/boardApi.js @@ -32,22 +32,35 @@ export const getSubBoards = async (parentBoardId = null) => { }; /* - * 게시판 생성 - * POST /api/board + * 하위 게시판 생성 (회장 권한) + * POST /api/admin/board * @param {string} boardName - 게시판 이름 * @param {string|null} parentBoardId - 부모 게시판 ID (최상위는 null) */ -export const createBoard = async (boardName, parentBoardId = null) => { - const requestBody = { boardName }; +export const createSubBoard = async (boardName, parentBoardId = null) => { + const normalizedBoardName = String(boardName || '').trim(); - if (parentBoardId) { - requestBody.parentBoardId = parentBoardId; + if (!normalizedBoardName) { + throw new Error('boardName is required'); } - const response = await api.post('/api/board', requestBody); + const requestBody = { + boardName: normalizedBoardName, + parentBoardId: parentBoardId ?? null, + }; + + const response = await api.post('/api/admin/board', requestBody); return response.data; }; +/* + * 게시판 생성 (하위 호환용) + * createSubBoard와 동일한 엔드포인트를 사용합니다. + */ +export const createBoard = async (boardName, parentBoardId = null) => { + return createSubBoard(boardName, parentBoardId); +}; + // ==================== 게시글 API ==================== /* diff --git a/frontend/src/utils/boardRoute.js b/frontend/src/utils/boardRoute.js new file mode 100644 index 00000000..9097309b --- /dev/null +++ b/frontend/src/utils/boardRoute.js @@ -0,0 +1,54 @@ +const BOARD_SEGMENT_MAP = { + 전체: 'root', + '전체 게시판': 'root', + 증권1팀: 'securities-1', + '증권1팀 게시판': 'securities-1', + 증권2팀: 'securities-2', + '증권2팀 게시판': 'securities-2', + 증권3팀: 'securities-3', + '증권3팀 게시판': 'securities-3', + 자산운용: 'asset-management', + 자산운용팀: 'asset-management', + '자산운용팀 게시판': 'asset-management', + 금융IT: 'finance-it', + 금융IT팀: 'finance-it', + '금융IT팀 게시판': 'finance-it', + 매크로: 'macro', + 매크로팀: 'macro', + '매크로팀 게시판': 'macro', + 트레이딩: 'trading', + '트레이딩팀 게시판': 'trading', +}; + +export const isAllBoardName = (boardName = '') => { + const normalized = String(boardName).trim(); + return normalized === '전체' || normalized === '전체 게시판'; +}; + +export const toBoardRouteSegment = (boardName = '') => { + const normalized = String(boardName).trim(); + if (!normalized) return ''; + + if (BOARD_SEGMENT_MAP[normalized]) { + return BOARD_SEGMENT_MAP[normalized]; + } + + if (normalized.endsWith('게시판')) { + const withoutSuffix = normalized.replace(/게시판$/, '').trim(); + if (BOARD_SEGMENT_MAP[withoutSuffix]) { + return BOARD_SEGMENT_MAP[withoutSuffix]; + } + return withoutSuffix; + } + + return normalized; +}; + +export const toBoardPath = (boardName = '') => { + if (isAllBoardName(boardName)) { + return '/board'; + } + + const segment = toBoardRouteSegment(boardName); + return segment ? `/board/${encodeURIComponent(segment)}` : '/board'; +}; From be5622b25e0f84b4cd6e0fa0d88998daeac71d39 Mon Sep 17 00:00:00 2001 From: sangkyu Date: Sat, 7 Mar 2026 13:30:15 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[FE]=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20ui=20=EC=88=98=EC=A0=95,=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20ui=20=EC=88=98=EC=A0=95,=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=ED=8C=90=20=EB=AA=A9=EB=A1=9D=20=EB=94=94=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Board/BoardActions.jsx | 4 +- .../components/Board/BoardActions.module.css | 9 +- .../src/components/Board/CategoryTabs.jsx | 19 +- .../components/Board/CategoryTabs.module.css | 12 ++ frontend/src/components/Board/Modal.jsx | 174 +++++++++++------ .../src/components/Board/Modal.module.css | 155 +++++++++------ .../Board/PostDetail/FileAttachmentList.jsx | 22 ++- .../Board/PostDetail/PostEditForm.jsx | 178 +++++++++++++----- .../components/Board/PostDetail/PostView.jsx | 12 ++ frontend/src/components/Board/PostItem.jsx | 10 +- frontend/src/components/Sidebar.jsx | 76 ++++---- frontend/src/components/Sidebar.module.css | 97 ++++++++-- frontend/src/pages/Board.jsx | 78 ++++++-- frontend/src/pages/PostDetail.jsx | 82 +++++++- frontend/src/pages/PostDetail.module.css | 178 +++++++++++++++--- 15 files changed, 833 insertions(+), 273 deletions(-) diff --git a/frontend/src/components/Board/BoardActions.jsx b/frontend/src/components/Board/BoardActions.jsx index 69d94437..380e54bc 100644 --- a/frontend/src/components/Board/BoardActions.jsx +++ b/frontend/src/components/Board/BoardActions.jsx @@ -3,9 +3,11 @@ import styles from './BoardActions.module.css'; import PlusIcon from '../../assets/board_plus.svg'; import DropdownArrowIcon from '../../assets/boardSelectArrow.svg'; -const BoardActions = ({ sortOption, onSortChange, onWrite }) => { +const BoardActions = ({ sortOption, onSortChange, onWrite, resultCount = 0 }) => { return (
+
{resultCount}건의 검색결과
+
onBoardChange?.(e.target.value)} + > + + {(boardOptions || []).map((board) => ( + + ))} + +
+ 드롭다운 화살표 +
+
+
+ +
@@ -53,81 +105,85 @@ const Modal = ({
- -
+
+
document.getElementById('fileUpload').click()} - onDragOver={handleDragOver} - onDrop={handleDrop} + onClick={handleFileButtonClick} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleFileButtonClick(); + } + }} > 폴더 파일 추가 -
-
+ +
+
+ {isDragOver && ( +
+ 파일을 업로드하려면 여기에 놓아주세요. +
+ )} +