Conversation
Walkthrough보드/하위게시판 탭, 생성 모달, 첨부파일, 댓글, PostDetail 리팩터링 및 API 기반 boardApi 추가와 함께 라우트에 Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant FE as 프론트엔드
participant API as boardApi (클라이언트)
participant BE as 백엔드
rect rgb(230,245,255)
Note over User,BE: 보드 및 하위게시판 로드
User->>FE: Board 페이지 진입
FE->>API: getParentBoards()
API->>BE: GET /api/boards
BE-->>API: boards
API-->>FE: boards
FE->>API: getSubBoards(parentBoardId)
API->>BE: GET /api/boards/{id}/children
BE-->>API: subboards
API-->>FE: subboards
end
rect rgb(240,255,240)
Note over User,BE: 게시글 생성 (파일 포함)
User->>FE: 게시글 작성 + 파일 첨부 -> 제출
FE->>API: createPost(boardId, formData)
API->>BE: POST /api/posts (multipart)
BE-->>API: created post
API-->>FE: post
FE->>FE: 게시글 목록 갱신 (getPosts / optimistic update)
end
rect rgb(255,245,230)
Note over User,BE: 상세조회·댓글·상호작용
User->>FE: Post 상세 진입
FE->>API: getPost(postId)
API->>BE: GET /api/posts/{id}?comments...
BE-->>API: post + comments
API-->>FE: post + comments
User->>FE: 댓글 작성/좋아요/북마크
FE->>API: createComment / toggleLike / toggleBookmark
API->>BE: POST/PUT endpoints
BE-->>API: 결과
API-->>FE: 결과
FE->>FE: 댓글 트리 재구성 / UI 업데이트
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (21)
frontend/src/components/Board/SearchBar.jsx (1)
6-16:handleSearch함수가 실질적으로 빈 함수입니다.
handleSearch가 주석만 포함하고 있어 Enter 키 입력이나 버튼 클릭 시 아무 동작도 하지 않습니다. Board.jsx에서searchTerm변경 시 자동으로 검색이 실행된다면, 이 함수와 관련 이벤트 핸들러를 제거하거나 명시적으로 검색을 트리거하는 로직을 추가하는 것이 좋습니다.const SearchBar = ({ searchTerm, onSearchChange }) => { - const handleSearch = () => { - if (searchTerm.trim()) { - // 검색은 Board.jsx에서 searchTerm 변경 시 자동 실행됨 - } - }; - - const handleKeyDown = (e) => { - if (e.key === 'Enter') { - handleSearch(); - } - }; + // 검색은 Board.jsx에서 searchTerm 변경 시 자동 실행됨frontend/src/utils/TimeUtils.js (1)
29-40: 시간 단위 계산 시 중복 연산이 있습니다.
minutes,hours,days는 조건문에서 사용되기 전에 항상 계산됩니다. 성능상 큰 이슈는 아니지만, 조건부로 계산하면 약간 더 효율적입니다.- const minutes = Math.floor(diffInSeconds / 60); - const hours = Math.floor(diffInSeconds / 3600); - const days = Math.floor(diffInSeconds / 86400); - const months = Math.floor(days / 30); - const years = Math.floor(days / 365); - if (diffInSeconds < 60) return '방금 전'; - else if (minutes < 60) return `${minutes}분 전`; - else if (hours < 24) return `${hours}시간 전`; - else if (days < 30) return `${days}일 전`; - else if (months < 12) return `${months}개월 전`; - else return `${years}년 전`; + + const minutes = Math.floor(diffInSeconds / 60); + if (minutes < 60) return `${minutes}분 전`; + + const hours = Math.floor(diffInSeconds / 3600); + if (hours < 24) return `${hours}시간 전`; + + const days = Math.floor(diffInSeconds / 86400); + if (days < 30) return `${days}일 전`; + + const months = Math.floor(days / 30); + if (months < 12) return `${months}개월 전`; + + return `${Math.floor(days / 365)}년 전`;frontend/src/pages/PostDetail.module.css (1)
446-629: 새로운 스타일에서 하드코딩된 색상 값 대신 CSS 변수 사용을 권장합니다.
:root에 정의된 CSS 변수(예:--color-button,--color-accent)가 있지만, 새로 추가된 스타일에서는#3b82f6,#ef4444,#10b981등 하드코딩된 색상을 사용하고 있습니다. 일관성과 유지보수를 위해 CSS 변수로 통일하는 것이 좋습니다.예시:
:root { /* 기존 변수들... */ --color-success: #10b981; --color-danger: #ef4444; --color-primary-blue: #3b82f6; } .saveButton { background-color: var(--color-primary-blue); } .removeFileButton { background-color: var(--color-danger); }frontend/src/utils/boardApi.js (3)
61-62:alert()와window.location.href사용은 React 앱에서 권장되지 않습니다.React Router의
navigate나 Context를 통한 인증 상태 관리를 사용하면 더 일관된 사용자 경험을 제공할 수 있습니다. 현재 방식은 페이지가 완전히 새로고침되어 앱 상태가 손실됩니다.
93-105: 클라이언트 측 필터링은 비효율적일 수 있습니다.
getSubBoards가 모든 하위 게시판을 가져온 후 클라이언트에서 필터링합니다. 데이터가 많아지면 성능 문제가 발생할 수 있으므로, 가능하다면 서버 API에서parentBoardId파라미터를 지원하도록 요청하는 것이 좋습니다.export const getSubBoards = async (parentBoardId = null) => { - const response = await apiClient.get('/api/board/childs'); - - // parentBoardId가 제공되면 해당 부모의 자식만 필터링 - if (parentBoardId) { - return response.data.filter( - (board) => board.parentBoardId === parentBoardId - ); - } - - // 제공되지 않으면 모든 하위 게시판 반환 - return response.data; + const params = parentBoardId ? { parentBoardId } : {}; + const response = await apiClient.get('/api/board/childs', { params }); + return response.data; };
67-74: 에러 객체를 재구성하면 원본 스택 트레이스가 손실됩니다.에러를 새 객체로 변환하면 디버깅이 어려워질 수 있습니다. 필요한 경우 원본 에러를
cause로 포함시키는 것을 고려하세요.- return Promise.reject({ status, message, data: error.response?.data }); + const enhancedError = new Error(message); + enhancedError.status = status; + enhancedError.data = error.response?.data; + enhancedError.cause = error; + return Promise.reject(enhancedError);frontend/src/pages/Board.module.css (1)
16-26: 고정 너비 대신 유연한 너비 설정을 고려하세요.
width: 200px로 변경했지만, 더 긴 게시판 제목이 있을 경우 잘릴 수 있습니다.min-width또는max-width와text-overflow조합을 사용하면 더 유연하게 대응할 수 있습니다..boardTitle { - width: 200px; + min-width: 200px; + max-width: 400px; height: 36px; font-weight: 600; font-size: 36px; line-height: 100%; letter-spacing: 0%; color: rgba(23, 23, 23, 1); margin: 0; opacity: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; }frontend/src/components/Board/CreateSubBoardModal.jsx (2)
5-8: 입력 유효성 검사를 추가하는 것을 고려해주세요.
handleSubmit에서 빈 문자열이나 공백만 있는 입력을 검증하지 않고onSave()를 호출하고 있습니다. 부모 컴포넌트에서 검증을 수행할 수도 있지만, 사용자 경험 향상을 위해 이 컴포넌트에서 기본적인 검증을 추가하는 것을 권장합니다.다음과 같이 간단한 검증을 추가할 수 있습니다:
const handleSubmit = (e) => { e.preventDefault(); + if (!value.trim()) { + return; + } onSave(); };
10-23: 접근성 개선을 위한 제안입니다.모달의 기본 구조는 잘 구현되어 있습니다. 추가적인 접근성 향상을 위해 다음을 고려해보세요:
- return ( + return ( - <div className={styles.modalOverlay} onClick={onClose}> + <div + className={styles.modalOverlay} + onClick={onClose} + role="dialog" + aria-modal="true" + aria-labelledby="modal-title" + > - <div className={styles.modalContent} onClick={(e) => e.stopPropagation()}> + <div className={styles.modalContent} onClick={(e) => e.stopPropagation()}> - <h2 className={styles.modalTitle}>하위 게시판 생성</h2> + <h2 id="modal-title" className={styles.modalTitle}>하위 게시판 생성</h2>frontend/src/components/Board/Modal.jsx (1)
58-70: 파일 업로드 검증을 추가하는 것을 고려해주세요.드래그 앤 드롭 및 파일 선택 시 파일 타입, 크기, 개수에 대한 제한이 없습니다. 사용자가 의도하지 않은 대용량 파일이나 부적절한 파일 타입을 업로드할 수 있습니다.
다음과 같은 검증 로직 추가를 권장합니다:
const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); const droppedFiles = Array.from(e.dataTransfer.files); + + // 파일 검증 예시 + const validFiles = droppedFiles.filter(file => { + const isValidSize = file.size <= 10 * 1024 * 1024; // 10MB 제한 + const isValidType = ['image/', 'application/pdf'].some(type => + file.type.startsWith(type) + ); + return isValidSize && isValidType; + }); + + if (validFiles.length !== droppedFiles.length) { + alert('일부 파일이 제외되었습니다. (크기 또는 타입 제한)'); + } - onFileChange({ target: { files: droppedFiles } }); + onFileChange({ target: { files: validFiles } }); };frontend/src/components/Board/Modal.module.css (2)
80-96: 커스텀 스크롤바 브라우저 호환성을 확인해주세요.Webkit 기반 스크롤바 스타일링을 사용하고 있습니다. Firefox는 다른 문법을 사용하므로, Firefox 사용자에게도 일관된 경험을 제공하려면 추가 스타일이 필요합니다.
.form::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } + +/* Firefox 스크롤바 스타일링 */ +.form { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.15) transparent; +}
184-189: 단어 분리 방식을 개선하는 것을 고려해주세요.
word-break: break-all은 단어를 무조건 중간에서 끊을 수 있어 가독성이 떨어질 수 있습니다. 파일 이름의 경우overflow-wrap: break-word가 더 자연스러운 결과를 제공합니다..fileName { font-size: 14px; color: #333; flex: 1; - word-break: break-all; + overflow-wrap: break-word; + word-break: break-word; }frontend/src/components/Board/PostItem.jsx (2)
24-32:nameToPath매핑이Board.jsx에 중복되어 있습니다.이 매핑이
Board.jsx(라인 53-61)에도 동일하게 정의되어 있습니다. 유지보수성과 일관성을 위해 공통 유틸리티나 상수 파일로 추출하는 것을 권장합니다.
utils/boardPaths.js파일을 생성하여 중앙화:export const BOARD_NAME_TO_PATH = { 증권1팀: 'securities-1', 증권2팀: 'securities-2', 증권3팀: 'securities-3', 자산운용: 'asset-management', 금융IT: 'finance-it', 매크로: 'macro', 트레이딩: 'trading', };
44-66:onBookmark과onLike콜백 호출 전 존재 여부 확인을 추가하세요.부모 컴포넌트에서 이 콜백들을 전달하지 않을 경우 런타임 에러가 발생할 수 있습니다.
const handleBookmarkClick = (e) => { e.stopPropagation(); e.preventDefault(); if (!postId) { console.error('북마크 실패: 게시글 ID 없음'); return; } - onBookmark(postId); + onBookmark?.(postId); }; const handleLikeClick = (e) => { e.stopPropagation(); e.preventDefault(); if (!postId) { console.error('좋아요 실패: 게시글 ID 없음'); return; } - onLike(postId); + onLike?.(postId); };frontend/src/pages/Board.jsx (2)
267-274: 파일 검증이 누락되었습니다.파일 크기나 파일 타입에 대한 검증 없이 파일을 추가하고 있습니다. 악의적이거나 너무 큰 파일 업로드를 방지하기 위해 검증을 추가하세요.
const handleFileChange = (e) => { const files = Array.from(e.target.files); + const maxSize = 10 * 1024 * 1024; // 10MB + const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']; + + const validFiles = files.filter((file) => { + if (file.size > maxSize) { + alert(`${file.name}: 파일 크기가 10MB를 초과합니다.`); + return false; + } + if (!allowedTypes.includes(file.type)) { + alert(`${file.name}: 지원하지 않는 파일 형식입니다.`); + return false; + } + return true; + }); + - setSelectedFiles((prevFiles) => [...prevFiles, ...files]); + setSelectedFiles((prevFiles) => [...prevFiles, ...validFiles]); };
53-61:nameToPath매핑이PostItem.jsx와 중복됩니다.앞서
PostItem.jsx리뷰에서 언급한 대로, 이 매핑을 공통 유틸리티로 추출하세요.frontend/src/pages/PostDetail.jsx (5)
436-440:prompt()를 사용한 댓글 수정은 사용자 경험이 좋지 않습니다.브라우저 기본
prompt()대신 인라인 편집 UI나 모달을 사용하는 것이 더 나은 UX를 제공합니다. 현재는 기능적으로 동작하지만, 향후 개선을 권장합니다.
95-101: 파일 상단에서 이미 import한useState,useEffect를 사용하세요.
CommentItem내에서React.useState와React.useEffect를 사용하고 있습니다. 일관성을 위해 상단에서 import한 훅을 사용하는 것이 좋습니다.+ import React, { useState, useEffect, useCallback } from 'react'; - import React, { useState, useEffect } from 'react'; // CommentItem 내부에서: - const [localReplyText, setLocalReplyText] = React.useState(''); + const [localReplyText, setLocalReplyText] = useState(''); - React.useEffect(() => { + useEffect(() => { if (!isReplying) { setLocalReplyText(''); } }, [isReplying]);
336-346: 파일 다운로드 로직이 개선될 수 있습니다.DOM 요소를 동적으로 생성/제거하는 대신
window.open()또는fetch+blob패턴을 사용할 수 있습니다. 현재 구현도 동작하지만, 일부 브라우저에서 다운로드 속성이 동작하지 않을 수 있습니다.
77-204:CommentItem컴포넌트가 별도 파일로 분리되면 좋겠습니다.
CommentItem은 약 130줄의 코드로 독립적인 기능을 가지고 있습니다. 재사용성과 유지보수를 위해components/Board/CommentItem.jsx로 분리하는 것을 권장합니다.
234-262: 비동기 작업에 대한 정리(cleanup)가 누락되었습니다.컴포넌트 언마운트 시 진행 중인 API 호출을 취소하지 않으면 메모리 누수나 상태 업데이트 경고가 발생할 수 있습니다.
useEffect(() => { + let isMounted = true; + const fetchPostAndComments = async () => { if (!postId) { setError('게시글 ID가 없습니다.'); setLoading(false); return; } try { setLoading(true); const data = await boardApi.getPost(postId); + if (!isMounted) return; setPost(data); // ... rest of the logic } catch (err) { + if (!isMounted) return; console.error('게시글 불러오기 실패:', err); setError('게시글을 불러올 수 없습니다.'); setComments([]); } finally { + if (isMounted) setLoading(false); - setLoading(false); } }; fetchPostAndComments(); + + return () => { + isMounted = false; + }; }, [postId]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
frontend/src/App.jsx(1 hunks)frontend/src/components/Board/BoardActions.module.css(1 hunks)frontend/src/components/Board/CategoryTabs.jsx(1 hunks)frontend/src/components/Board/CategoryTabs.module.css(1 hunks)frontend/src/components/Board/CreateSubBoardModal.jsx(1 hunks)frontend/src/components/Board/CreateSubBoardModal.module.css(1 hunks)frontend/src/components/Board/Modal.jsx(4 hunks)frontend/src/components/Board/Modal.module.css(6 hunks)frontend/src/components/Board/PostItem.jsx(5 hunks)frontend/src/components/Board/SearchBar.jsx(2 hunks)frontend/src/pages/Board.jsx(3 hunks)frontend/src/pages/Board.module.css(1 hunks)frontend/src/pages/PostDetail.jsx(4 hunks)frontend/src/pages/PostDetail.module.css(5 hunks)frontend/src/utils/TimeUtils.js(1 hunks)frontend/src/utils/boardApi.js(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
frontend/src/components/Board/Modal.jsx (1)
frontend/src/pages/Board.jsx (1)
selectedFiles(22-22)
frontend/src/pages/Board.jsx (4)
frontend/src/components/Board/SearchBar.jsx (2)
handleSearch(6-10)SearchBar(5-38)frontend/src/components/Board/CategoryTabs.jsx (1)
CategoryTabs(4-24)frontend/src/components/Board/BoardActions.jsx (1)
BoardActions(6-32)frontend/src/components/Board/CreateSubBoardModal.jsx (1)
CreateSubBoardModal(4-41)
frontend/src/components/Board/SearchBar.jsx (1)
frontend/src/pages/PostDetail.jsx (1)
handleKeyDown(496-498)
frontend/src/utils/TimeUtils.js (1)
frontend/src/pages/PostDetail.jsx (1)
date(93-93)
frontend/src/utils/boardApi.js (2)
frontend/src/pages/PostDetail.jsx (2)
error(212-212)commentId(89-89)frontend/src/components/Board/PostItem.jsx (1)
postId(15-15)
frontend/src/App.jsx (1)
frontend/src/pages/PostDetail.jsx (1)
PostDetail(206-798)
🔇 Additional comments (16)
frontend/src/components/Board/SearchBar.jsx (1)
12-16:onKeyPress에서onKeyDown으로 변경 - 좋습니다.
onKeyPress는 deprecated이므로onKeyDown사용이 올바른 방향입니다. PostDetail.jsx의handleKeyDown패턴과도 일관성이 있습니다.Also applies to: 25-25
frontend/src/utils/TimeUtils.js (1)
1-22: 입력 유효성 검사가 잘 구현되었습니다.Date 객체, 문자열, 숫자 타입을 모두 지원하며 잘못된 입력에 대해 안전한 기본값('방금 전')을 반환합니다. ISO 문자열의 'Z' 접미사 처리도 적절합니다.
frontend/src/pages/PostDetail.module.css (2)
31-31: 반응형 레이아웃을 위한 사이드바 오프셋 처리가 적절합니다.
margin-left: 264px로 사이드바 공간을 확보하고, 768px 및 400px 브레이크포인트에서margin-left: 0으로 리셋하여 모바일 레이아웃을 지원합니다.Also applies to: 267-272, 295-299
370-372:.hiddenInput유틸리티 클래스가 추가되었습니다.파일 업로드 등을 위한 숨겨진 input 요소에 사용될 것으로 보입니다.
frontend/src/components/Board/BoardActions.module.css (1)
23-32: 레이아웃 단순화가 적절합니다.
max-width계산을 단순화하고 오른쪽 패딩을 제거한 것은 새로운 CategoryTabs 및 모달 컴포넌트와의 레이아웃 일관성을 위한 합리적인 변경입니다.frontend/src/components/Board/CreateSubBoardModal.module.css (1)
1-79: 모달 스타일링이 적절합니다.새로운 하위 게시판 생성 모달의 CSS 구조가 잘 정의되어 있습니다.
참고:
z-index: 1000이Modal.module.css와 동일합니다. 두 모달이 동시에 열릴 가능성은 낮아 보이지만, 향후 복잡한 UI 플로우에서 문제가 될 수 있으니 z-index 관리 전략을 고려해보시기 바랍니다.frontend/src/components/Board/Modal.jsx (2)
82-102: 파일 목록 렌더링이 잘 구현되었습니다.첨부 파일 목록을 동적으로 렌더링하고 개별 파일 제거 기능을 제공하는 구조가 잘 작성되었습니다. 파일 크기를 KB 단위로 표시하는 것도 사용자 친화적입니다.
22-28: 드래그 앤 드롭 파일 타입 호환성 - 실제 문제 없음부모 컴포넌트의
handleFileChange(Board.jsx line 268)에서Array.from(e.target.files)를 사용하므로,e.target.files가 FileList 타입이든 배열이든 모두 올바르게 처리됩니다. Array.from()은 배열 유사 객체와 이터러블 모두를 배열로 변환하도록 설계되었으므로, 일반 파일 입력 이벤트와 드래그 앤 드롭 이벤트 모두 완전히 호환됩니다.frontend/src/components/Board/Modal.module.css (2)
15-27: 반응형 디자인 개선이 우수합니다.CSS 변수와
clamp()함수를 사용한 반응형 디자인 접근 방식이 훌륭합니다. 다양한 뷰포트에서 일관된 사용자 경험을 제공할 수 있습니다.
170-204: 파일 목록 UI 스타일링이 적절합니다.새로운 파일 목록 관련 스타일(
.fileList,.fileItem,.fileName,.removeFileButton)이 잘 정의되어 있으며, 사용자 인터랙션에 대한 피드백(호버 효과)도 적절하게 구현되었습니다.frontend/src/components/Board/PostItem.jsx (2)
17-42: 네비게이션 및 유효성 검사 로직이 잘 구현되었습니다.
postId유효성 검사와state를 통한 post 데이터 전달이 적절합니다. 에러 핸들링도 포함되어 있어 좋습니다.
119-123:React.memo래핑과displayName설정이 적절합니다.성능 최적화와 디버깅을 위한 좋은 패턴입니다.
frontend/src/pages/Board.jsx (2)
240-266: 하위 게시판 생성 로직이 잘 구현되었습니다.생성 후 목록 새로고침 및 첫 번째 탭 자동 선택 로직이 적절합니다.
381-396: 정렬 로직이 적절하게 구현되었습니다.
createdDate와date필드에 대한 폴백 처리와 다양한 정렬 옵션이 잘 구현되어 있습니다.frontend/src/pages/PostDetail.jsx (2)
16-75: 댓글 트리 구조 유틸리티 함수들이 잘 구현되었습니다.
buildCommentTree,extractRawComments,findCommentInTree함수들이 다양한 API 응답 형식을 처리하며 재귀적 댓글 구조를 올바르게 구축합니다.
264-291: 좋아요/북마크의 낙관적 업데이트가 잘 구현되었습니다.API 호출 실패 시 이전 상태로 롤백하는 패턴이 적절합니다. 서버 응답 데이터로 최종 상태를 업데이트하는 것도 좋은 접근입니다.
cmmoon03
left a comment
There was a problem hiding this comment.
코드래빗이 언급한 부분 수정했습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
frontend/src/components/Board/PostItem.jsx (1)
37-44: 불필요한 삼항 연산자 - 도달 불가능한 폴백 경로.Lines 37-40의 가드에서
!teamPath일 때 조기 반환하므로, Lines 42-44의 삼항 조건teamPath ?는 항상 true입니다. 폴백 경로/board/post/${postId}는 절대 실행되지 않는 데드 코드입니다.기존 리뷰에서 지적된 대로 해당 폴백 경로가 App.jsx에 정의되지 않았지만, 현재 가드 로직으로 인해 실제로 도달하지 않습니다. 명확성을 위해 삼항 연산자를 제거하세요.
- const path = teamPath - ? `/board/${teamPath}/post/${postId}` - : `/board/post/${postId}`; + const path = `/board/${teamPath}/post/${postId}`;frontend/src/pages/PostDetail.jsx (1)
393-393:⚠️ refreshPostAndComments()의 반환값이 무시되고 있습니다.
refreshPostAndComments()는updatedPost를 반환하지만(line 238), 여기서는 사용되지 않습니다. 이전 리뷰에서 지적된 것처럼, React 상태 업데이트는 비동기이므로await refreshPostAndComments()직후에도post변수는 여전히 이전 값을 참조합니다.다음과 같이 수정하세요:
- await refreshPostAndComments(); + const updatedPost = await refreshPostAndComments(); + setEditTitle(updatedPost.title); + setEditContent(updatedPost.content);또는
refreshPostAndComments()내부에서 이미setEditTitle과setEditContent를 호출하고 있으므로(lines 232-233), 여기서는 중복 호출을 제거할 수 있습니다:- await refreshPostAndComments(); + await refreshPostAndComments(); // 이미 내부에서 edit 상태를 업데이트함
🧹 Nitpick comments (4)
frontend/src/components/Board/PostItem.jsx (1)
24-32:nameToPath매핑을 상수로 추출 권장.
nameToPath객체가 컴포넌트 함수 내부에 정의되어 있어 매 렌더링마다 새로 생성됩니다. 재사용성과 유지보수를 위해 컴포넌트 외부 상수나 별도 설정 파일로 추출하는 것을 권장합니다.+const NAME_TO_PATH = { + 증권1팀: 'securities-1', + 증권2팀: 'securities-2', + 증권3팀: 'securities-3', + 자산운용: 'asset-management', + 금융IT: 'finance-it', + 매크로: 'macro', + 트레이딩: 'trading', +}; + const PostItem = ({ post, onLike, onBookmark }) => { // ... const handleCardClick = () => { // ... - const nameToPath = { - 증권1팀: 'securities-1', - // ... - }; - const teamPath = nameToPath[boardName] || team; + const teamPath = NAME_TO_PATH[boardName] || team;frontend/src/pages/PostDetail.jsx (3)
434-459: 댓글 수정 UX 개선이 필요합니다.게시글 수정은 인라인 편집 UI를 사용하지만(lines 526-598), 댓글 수정은
window.prompt()를 사용합니다. 이는 다음과 같은 문제가 있습니다:
- 접근성이 떨어짐
- 사용자 경험의 일관성 부족
- 멀티라인 입력 및 미리보기 불가능
게시글 편집 패턴을 따라 댓글에도 인라인 편집 상태를 추가하는 것을 권장합니다. 예를 들어:
CommentItem에isEditing상태 추가- 편집 모드에서는
textarea로 전환- 저장/취소 버튼 제공
이렇게 하면 사용자 경험이 일관되고 향상됩니다.
16-76: 댓글 트리 구성 로직이 잘 구현되어 있습니다.댓글 트리 관련 헬퍼 함수들이 올바르게 구현되었습니다:
buildCommentTree: Map 기반으로 효율적인 트리 구축extractRawComments: 다양한 응답 형식 처리findCommentInTree: 재귀 검색필요 시 나중에
React.useMemo로 댓글 트리 계산을 메모이제이션하면 대량의 댓글이 있을 때 성능을 개선할 수 있습니다.
342-352: 파일 다운로드에 에러 처리가 없습니다.다운로드 실패 시(예: CORS 오류, 파일 없음, 인증 오류) 사용자에게 피드백이 없습니다.
다운로드 실패를 감지하고 사용자에게 알리는 에러 처리를 추가하는 것을 권장합니다:
const handleAttachmentDownload = (file) => { - const baseUrl = import.meta.env.VITE_API_URL || ''; - const url = `${baseUrl}${file.filePath}`; - - const a = document.createElement('a'); - a.href = url; - a.download = file.originalFilename; - document.body.appendChild(a); - a.click(); - a.remove(); + try { + const baseUrl = import.meta.env.VITE_API_URL || ''; + const url = `${baseUrl}${file.filePath}`; + + const a = document.createElement('a'); + a.href = url; + a.download = file.originalFilename; + a.onerror = () => { + alert('파일 다운로드에 실패했습니다.'); + }; + document.body.appendChild(a); + a.click(); + a.remove(); + } catch (error) { + console.error('다운로드 오류:', error); + alert('파일 다운로드에 실패했습니다.'); + } };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
frontend/src/components/Board/CategoryTabs.jsx(1 hunks)frontend/src/components/Board/CategoryTabs.module.css(1 hunks)frontend/src/components/Board/PostItem.jsx(5 hunks)frontend/src/pages/Board.jsx(3 hunks)frontend/src/pages/PostDetail.jsx(4 hunks)frontend/src/utils/boardApi.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/src/components/Board/CategoryTabs.module.css
- frontend/src/components/Board/CategoryTabs.jsx
- frontend/src/utils/boardApi.js
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/Board/PostItem.jsx (2)
frontend/src/pages/PostDetail.jsx (3)
navigate(209-209)useParams(208-208)post(211-211)frontend/src/pages/Board.jsx (1)
useParams(13-13)
frontend/src/pages/PostDetail.jsx (1)
frontend/src/utils/TimeUtils.js (3)
date(4-4)getTimeAgo(1-41)getTimeAgo(1-41)
🔇 Additional comments (11)
frontend/src/components/Board/PostItem.jsx (6)
11-15: LGTM!
useParams를 통해 라우트에서team을 추출하고,post.postId || post.id폴백으로 다양한 API 응답 형식을 처리하는 방식이 적절합니다. 관련 코드 스니펫의Board.jsx,PostDetail.jsx와 일관된 패턴입니다.
49-59: LGTM!이벤트 전파 차단(
stopPropagation,preventDefault)과postId유효성 검사가 적절히 구현되었습니다. 방어적 프로그래밍 패턴이 잘 적용되었습니다.
61-71: LGTM!
handleBookmarkClick과 일관된 패턴으로 구현되었습니다. 이벤트 처리와 유효성 검사가 적절합니다.
86-89: 하드코딩된 작성자 "운영진" 확인 필요.작성자가
"운영진"으로 하드코딩되어 있습니다. 실제 게시글 작성자 정보(post.author또는 유사 필드)를 사용해야 하는지 확인이 필요합니다.
102-106: 하드코딩된 북마크 카운트 "1" 확인 필요.
post.isBookmarked일 때 카운트를 항상1로 표시하고 있습니다. 좋아요 카운트(post.likeCount)처럼 동적 값(post.bookmarkCount등)을 사용해야 하는지 확인이 필요합니다.- {post.isBookmarked && <span className={styles.count}>1</span>} + {post.bookmarkCount > 0 && ( + <span className={styles.count}>{post.bookmarkCount}</span> + )}
126-128: LGTM!
React.memo로 래핑하여 불필요한 리렌더링을 방지하고,displayName설정으로 디버깅 편의성을 제공합니다.frontend/src/pages/PostDetail.jsx (1)
271-327: LGTM! 좋아요/북마크 낙관적 업데이트 구현이 올바릅니다.낙관적 UI 업데이트 패턴이 잘 구현되어 있습니다:
- 즉각적인 UI 피드백 제공
- API 실패 시 이전 상태로 적절히 롤백
- 사용자 경험 향상
frontend/src/pages/Board.jsx (4)
31-39: LGTM! 클로저 문제가useRef로 올바르게 해결되었습니다.이전 리뷰에서 지적된 클로저 문제가 올바르게 수정되었습니다:
- Line 31:
prevPostsRef도입- Lines 37-39:
useEffect로 ref 업데이트- Lines 327, 352: 롤백 시
prevPostsRef.current사용이제
handleLike와handleBookmark에서 에러 발생 시 올바른 이전 상태로 롤백합니다.Also applies to: 326-376
189-197: 중복useEffect문제가 해결되었습니다.이전 리뷰에서 지적된 중복
useEffect문제가 올바르게 통합되었습니다. 현재 하나의useEffect만 존재하며, 검색어 유무에 따라handleSearch또는fetchPosts를 호출합니다.
283-291: 서브보드 필수 요구사항을 확인하세요.게시글 작성 시 하위 게시판이 필수로 요구됩니다(lines 283-286). 만약 특정 게시판이 하위 게시판 없이 직접 게시글을 허용해야 한다면, 이 검증 로직이 문제가 될 수 있습니다.
다음을 확인하세요:
- 모든 게시판이 반드시 하위 게시판을 가져야 하는지
- 일부 게시판은 하위 게시판 없이 직접 게시글 작성을 허용해야 하는지
만약 후자라면, 다음과 같이 조건부 검증으로 수정하세요:
- if (subBoardTabs.length === 0) { + // 루트 게시판(전체)이 아닌 경우에만 서브보드 필수 + if (!Array.isArray(currentBoardId) && subBoardTabs.length === 0) { alert('하위 게시판이 없습니다.\n먼저 하위 게시판을 생성해주세요.'); return; }
126-157: LGTM! 게시글 조회 로직이 잘 구현되어 있습니다.여러 게시판의 게시글을 병렬로 조회하는 로직이 효율적으로 구현되었습니다:
Promise.all로 병렬 처리- 개별 게시판 실패 시에도 다른 게시판 결과 반환
- 적절한 에러 처리 및 로딩 상태 관리
| const nameToPath = { | ||
| 증권1팀: 'securities-1', | ||
| 증권2팀: 'securities-2', | ||
| 증권3팀: 'securities-3', | ||
| 자산운용: 'asset-management', | ||
| 금융IT: 'finance-it', | ||
| 매크로: 'macro', | ||
| 트레이딩: 'trading', | ||
| }; |
There was a problem hiding this comment.
하드코딩된 게시판 경로 매핑이 유지보수에 취약합니다.
게시판 이름을 URL 경로로 매핑하는 로직이 하드코딩되어 있습니다. 다음과 같은 문제가 있습니다:
- 데이터베이스에서 게시판 이름이 변경되면 매핑이 깨짐
- 새로운 게시판 추가 시 코드 수정 필요
- 매핑에 없는 게시판은 경로가 생성되지 않음
다음과 같은 개선 방안을 고려하세요:
- API 응답에 URL 경로 포함 (권장)
- 게시판 이름을 slug화하는 유틸리티 함수 사용
- 게시판 ID만 사용하는 라우팅 전환 (예:
/board/123)
예시 (slug 생성):
const generateSlug = (name) => {
const slugMap = {
'증권1팀': 'securities-1',
'증권2팀': 'securities-2',
// ... 기존 매핑
};
return slugMap[name] || name.toLowerCase().replace(/\s+/g, '-');
};🤖 Prompt for AI Agents
In frontend/src/pages/Board.jsx around lines 59 to 67 the board name→path
mapping is hardcoded which breaks when names change or new boards are added; fix
by preferring a path field from the API (update the fetch to use board.path and
wire that into link generation), and if backend cannot provide it implement a
small slugify utility (normalize/trim, lowercase, replace spaces/special chars
with hyphens, and optionally map known exceptions) and replace the static map
with generateSlug(board.name); alternatively migrate routing to use board IDs
(e.g., /board/:id) and resolve server-side to the proper resource so the
front-end no longer depends on name-to-path mappings.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/PostDetail.jsx (1)
215-225: 중복된 상태 선언으로 인한 오류 발생.
isEdit,editTitle,editContent,editFiles,newFiles상태가 Lines 215-219와 Lines 221-225에서 두 번 선언되었습니다. 이는 런타임 오류를 유발하거나 예기치 않은 동작을 일으킵니다.Lines 221-225의 중복 선언을 제거하세요:
const [isEdit, setIsEdit] = useState(false); const [editTitle, setEditTitle] = useState(''); const [editContent, setEditContent] = useState(''); const [editFiles, setEditFiles] = useState([]); const [newFiles, setNewFiles] = useState([]); - const [isEdit, setIsEdit] = useState(false); - const [editTitle, setEditTitle] = useState(''); - const [editContent, setEditContent] = useState(''); - const [editFiles, setEditFiles] = useState([]); - const [newFiles, setNewFiles] = useState([]); const [comments, setComments] = useState([]);
♻️ Duplicate comments (1)
frontend/src/pages/Board.jsx (1)
59-67: 하드코딩된 게시판 경로 매핑 - 이전 리뷰에서 지적된 사항.이 하드코딩된
nameToPath매핑은 이전 리뷰에서 이미 지적되었습니다. API 응답에 URL 경로를 포함하거나, 공유 유틸리티로 추출하거나, 게시판 ID 기반 라우팅으로 전환하는 것을 권장합니다.
🧹 Nitpick comments (4)
frontend/src/components/Board/PostItem.jsx (2)
37-46: 도달 불가능한 코드(dead code) 및 불필요한 삼항 연산자.Line 37-40에서
teamPath가 없으면 조기 반환(early return)하므로, Line 42-44의 삼항 연산자에서teamPath는 항상 truthy입니다. 따라서:/board/post/${postId}`` 분기는 절대 실행되지 않는 dead code입니다.if (!teamPath) { alert('게시판 정보를 찾을 수 없습니다.'); return; } - const path = teamPath - ? `/board/${teamPath}/post/${postId}` - : `/board/post/${postId}`; + const path = `/board/${teamPath}/post/${postId}`; navigate(path, { state: { post } });
24-32:nameToPath매핑이Board.jsx와 중복됩니다.이 하드코딩된 매핑이
Board.jsx(lines 59-67)에도 동일하게 존재합니다. 공유 유틸리티나 상수로 추출하여 DRY 원칙을 준수하고 유지보수를 용이하게 하세요.예시 (공유 상수 파일):
// frontend/src/constants/boardPaths.js export const BOARD_NAME_TO_PATH = { 증권1팀: 'securities-1', 증권2팀: 'securities-2', 증권3팀: 'securities-3', 자산운용: 'asset-management', 금융IT: 'finance-it', 매크로: 'macro', 트레이딩: 'trading', };frontend/src/pages/PostDetail.jsx (1)
446-450:prompt()를 사용한 댓글 수정 UX 개선 고려.
prompt()는 브라우저 기본 대화상자로 UX가 제한적입니다. 인라인 편집 UI로 개선하면 더 나은 사용자 경험을 제공할 수 있습니다. 현재 구현도 기능적으로는 문제없습니다.frontend/src/pages/Board.jsx (1)
444-450:currentTeamprop이PostItem에서 사용되지 않습니다.
PostItem컴포넌트의 props에서currentTeam이 제거되었지만(AI 요약 및 PostItem.jsx 코드 확인), 여기서는 여전히 전달하고 있습니다. 불필요한 prop을 제거하세요.sortedPosts.map((post) => ( <PostItem key={post.postId || post.id} post={post} - currentTeam={team} onLike={handleLike} onBookmark={handleBookmark} /> ))
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
frontend/src/App.jsx(1 hunks)frontend/src/components/Board/BoardActions.module.css(1 hunks)frontend/src/components/Board/PostItem.jsx(5 hunks)frontend/src/pages/Board.jsx(3 hunks)frontend/src/pages/PostDetail.jsx(9 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/App.jsx
- frontend/src/components/Board/BoardActions.module.css
🧰 Additional context used
🧬 Code graph analysis (3)
frontend/src/pages/PostDetail.jsx (1)
frontend/src/utils/TimeUtils.js (3)
date(4-4)getTimeAgo(1-41)getTimeAgo(1-41)
frontend/src/pages/Board.jsx (5)
frontend/src/pages/PostDetail.jsx (4)
error(213-213)id(68-68)handleLike(277-304)post(211-211)frontend/src/utils/boardApi.js (15)
response(51-55)response(94-94)response(104-104)response(130-130)response(144-146)response(164-166)response(191-193)response(209-211)response(239-241)response(251-251)response(263-263)response(273-273)response(294-294)response(311-311)response(320-320)frontend/src/components/Board/CategoryTabs.jsx (1)
CategoryTabs(4-24)frontend/src/components/Board/BoardActions.jsx (1)
BoardActions(6-32)frontend/src/components/Board/CreateSubBoardModal.jsx (1)
CreateSubBoardModal(4-41)
frontend/src/components/Board/PostItem.jsx (2)
frontend/src/pages/PostDetail.jsx (3)
navigate(209-209)useParams(208-208)post(211-211)frontend/src/pages/Board.jsx (1)
useParams(13-13)
🔇 Additional comments (8)
frontend/src/components/Board/PostItem.jsx (1)
49-71: LGTM!이벤트 핸들러가
e.stopPropagation()과e.preventDefault()를 적절히 사용하고,postId유효성 검사를 수행합니다. 클릭 이벤트가 카드 클릭과 충돌하지 않도록 잘 처리되었습니다.frontend/src/pages/PostDetail.jsx (4)
16-36: LGTM!
buildCommentTree함수가 평면 댓글 배열을 중첩 트리 구조로 잘 변환합니다. 부모가 없는 댓글도 루트로 처리하여 데이터 불일치 시에도 안전하게 동작합니다.
78-89: CommentItem 컴포넌트 구조가 적절합니다.재귀적 댓글 렌더링, depth 기반 답글 버튼 제어, 로컬 답글 상태 관리가 잘 구현되어 있습니다.
277-304: LGTM!좋아요 토글의 낙관적 UI 업데이트가 잘 구현되었습니다. 이전 상태 스냅샷을 저장하고, API 실패 시 롤백하며, API 응답으로 최종 상태를 동기화합니다.
348-358: 파일 다운로드 구현이 적절합니다.동적 앵커 요소를 생성하여 파일 다운로드를 처리하는 표준적인 방식입니다. 환경 변수를 통한 API URL 구성도 올바릅니다.
frontend/src/pages/Board.jsx (3)
326-349: LGTM!이전 리뷰에서 지적된 클로저 문제가
useRef를 사용하여 올바르게 해결되었습니다. 낙관적 업데이트 전 스냅샷을 저장하고 API 실패 시 롤백하는 패턴이 적절합니다.
189-197: LGTM!이전 리뷰에서 지적된 중복
useEffect문제가 해결되어 단일 effect로 통합되었습니다.searchTerm유무에 따라 검색 또는 전체 조회를 적절히 분기합니다.
241-266: LGTM!하위 게시판 생성 후 탭 목록을 새로고침하고 첫 번째 탭을 활성화하는 흐름이 적절합니다. 에러 핸들링도 포함되어 있습니다.
frontend/src/utils/boardApi.js
Outdated
| @@ -0,0 +1,323 @@ | |||
| // boardApi.js | |||
| import axios from 'axios'; | |||
There was a problem hiding this comment.
utils폴더에 axios.js 파일 있는데 겹치는 로직이 있습니다. 확인해서 겹치는 로직은 없애고 axios.js에서 import해서 사용해주세요.
| return Promise.reject({ status, message, data: error.response?.data }); | ||
| } | ||
| ); | ||
|
|
frontend/src/utils/boardApi.js
Outdated
| * @param {object} postData - { title, content, files } | ||
| */ | ||
| export const updatePost = async (postId, boardId, postData) => { | ||
| console.log('게시글 수정 요청:', { postId, boardId, postData }); |
| background: none; | ||
| border: none; | ||
| cursor: pointer; | ||
| } |
There was a problem hiding this comment.
PostDetail 파일 하나에 몰아넣지 말고 컴포넌트 단위로 분리하는 게 더 좋다는 생각이 들어요. 지금 코드가 너무 길어서 가독성도 떨어지고 pages 폴더의 파일에서 컴포넌트를 조립한다고 생각하시면 됩니다.
| }; | ||
|
|
||
| const handleKeyPress = (e) => { | ||
| const handleKeyDown = (e) => { |
There was a problem hiding this comment.
handleSearch와 handleKeyDown 메서드 둘 다 아무런 기능을 안하지 않나요?
There was a problem hiding this comment.
api 연결해서 검색버튼 누르거나 엔터 칠때마다 api호출하는 식으로 수정했습니다.
cmmoon03
left a comment
There was a problem hiding this comment.
이번에 pull할 때 app.jsx, quantbot 쪽이 잘 안 받아와져서 오류나서 실행 화면을 못 봤는데 이번꺼 커밋하고 다시 수정하겠습니다.
frontend/src/utils/boardApi.js
Outdated
| @@ -0,0 +1,323 @@ | |||
| // boardApi.js | |||
| import axios from 'axios'; | |||
| background: none; | ||
| border: none; | ||
| cursor: pointer; | ||
| } |
| return Promise.reject({ status, message, data: error.response?.data }); | ||
| } | ||
| ); | ||
|
|
| }; | ||
|
|
||
| const handleKeyPress = (e) => { | ||
| const handleKeyDown = (e) => { |
There was a problem hiding this comment.
api 연결해서 검색버튼 누르거나 엔터 칠때마다 api호출하는 식으로 수정했습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
frontend/src/pages/Board.jsx (1)
59-78: 하드코딩된 매핑에 없는 게시판 이름 처리가 누락되었습니다.이전 리뷰에서 하드코딩된 매핑의 유지보수 문제가 지적되었습니다. 추가로,
nameToPath에 없는 게시판 이름의 경우path가/board/undefined가 되어 잘못된 경로가 생성됩니다.boards.forEach((board) => { const boardName = board.boardName; - const path = - boardName === '전체' ? '/board' : `/board/${nameToPath[boardName]}`; + const pathSegment = nameToPath[boardName]; + if (boardName === '전체') { + idMap['/board'] = board.boardId; + nameMap['/board'] = boardName; + } else if (pathSegment) { + const path = `/board/${pathSegment}`; + idMap[path] = board.boardId; + nameMap[path] = boardName; + } else { + console.warn(`매핑되지 않은 게시판: ${boardName}`); + } - - if (path) { - idMap[path] = board.boardId; - nameMap[path] = boardName; - } });
🧹 Nitpick comments (6)
frontend/src/components/Board/PostDetail/FileAttachmentList.jsx (1)
25-30: 클릭 핸들러 로직을 간소화할 수 있습니다.Line 29의 인라인 조건 로직이 다소 복잡합니다. 가독성을 위해 별도 핸들러 함수로 추출하는 것을 고려해보세요.
다음과 같이 리팩터링할 수 있습니다:
+ const handleIconClick = () => { + if (!isEditMode && onDownload) { + onDownload(file); + } + }; + <img src={FolderIcon} alt="파일" className={`${styles.attachmentIcon} ${!isEditMode ? styles.attachmentIconButton : ''}`} - onClick={() => !isEditMode && onDownload && onDownload(file)} + onClick={handleIconClick} />frontend/src/utils/boardApi.js (1)
139-143: console.error를 제거할 수 있습니다.
boardId가 없을 때 이미 에러를 던지고 있으므로, Line 141의console.error는 불필요합니다.export const updatePost = async (postId, boardId, postData) => { if (!boardId) { - console.error('boardId가 없습니다.'); throw new Error('boardId is required'); }frontend/src/components/Board/PostDetail/PostEditForm.jsx (1)
59-71: 파일 업로드 버튼의 DOM 조작 패턴을 개선할 수 있습니다.Line 68의
document.getElementById를 사용한 직접적인 DOM 조작보다는 React ref를 사용하는 것이 더 React 친화적입니다.+import React, { useRef } from 'react'; + const PostEditForm = ({ // ... props }) => { + const fileInputRef = useRef(null); + return ( <> {/* ... */} <div className={styles.fileAddSection}> <input + ref={fileInputRef} type="file" - id="editFileUpload" multiple onChange={onAddNewFile} className={styles.hiddenInput} /> <button className={styles.addFileButton} - onClick={() => document.getElementById('editFileUpload').click()} + onClick={() => fileInputRef.current?.click()} > 파일 추가 </button> </div> </> ); };frontend/src/pages/PostDetail.jsx (1)
238-242: 댓글 수정 UI를 개선할 수 있습니다.
prompt()를 사용한 댓글 수정은 기능적으로는 동작하지만, 더 나은 사용자 경험을 위해 인라인 편집 UI를 고려해볼 수 있습니다. 다만 이는 나중에 개선할 수 있는 부분입니다.향후 개선 시, 다음과 같은 인라인 편집 패턴을 고려할 수 있습니다:
- 댓글 아이템에 편집 모드 상태 추가
- 편집 모드에서 textarea로 전환
- 저장/취소 버튼 표시
frontend/src/pages/Board.jsx (2)
205-225: 불필요한 조건 체크가 있습니다.
handleTabChange에서currentBoardId체크(라인 209)는 실제 API 호출에서tabId를 사용하므로 불필요합니다.const handleTabChange = useCallback( async (tabId) => { setActiveSubBoard(tabId); - if (!currentBoardId) return; + if (!tabId) return; try { setLoading(true); const response = await boardApi.getPosts(tabId);
318-321: 중복된 상태 초기화가 있습니다.
handleCloseModal()이 이미setSelectedFiles([])를 호출하므로(라인 235), 라인 319의 호출은 불필요합니다.await boardApi.createPost(activeSubBoard, postData); handleCloseModal(); - setSelectedFiles([]); handleTabChange(activeSubBoard);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
frontend/src/components/Board/PostDetail/CommentSection.jsx(1 hunks)frontend/src/components/Board/PostDetail/FileAttachmentList.jsx(1 hunks)frontend/src/components/Board/PostDetail/PostEditForm.jsx(1 hunks)frontend/src/components/Board/PostDetail/PostView.jsx(1 hunks)frontend/src/components/Board/PostItem.jsx(5 hunks)frontend/src/components/Board/SearchBar.jsx(2 hunks)frontend/src/pages/Board.jsx(3 hunks)frontend/src/pages/PostDetail.jsx(3 hunks)frontend/src/utils/boardApi.js(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
frontend/src/components/Board/PostDetail/PostEditForm.jsx (2)
frontend/src/pages/PostDetail.jsx (2)
editFiles(72-72)newFiles(73-73)frontend/src/components/Board/PostDetail/FileAttachmentList.jsx (1)
FileAttachmentList(5-53)
frontend/src/components/Board/PostItem.jsx (2)
frontend/src/pages/PostDetail.jsx (3)
navigate(62-62)useParams(61-61)post(65-65)frontend/src/pages/Board.jsx (1)
useParams(13-13)
frontend/src/utils/boardApi.js (2)
frontend/src/components/Board/PostItem.jsx (1)
postId(15-15)frontend/src/components/Board/PostDetail/CommentSection.jsx (1)
commentId(24-24)
frontend/src/components/Board/PostDetail/CommentSection.jsx (1)
frontend/src/pages/PostDetail.jsx (4)
showCommentMenu(77-77)replyTargetId(78-78)post(65-65)comments(75-75)
frontend/src/pages/Board.jsx (6)
frontend/src/components/Board/PostItem.jsx (3)
useParams(13-13)postId(15-15)PostItem(11-118)frontend/src/utils/boardApi.js (14)
response(11-11)response(21-21)response(47-47)response(61-63)response(81-83)response(108-110)response(126-128)response(156-158)response(168-168)response(180-180)response(190-190)response(211-211)response(228-228)response(238-238)frontend/src/components/Board/SearchBar.jsx (2)
handleSearch(8-12)SearchBar(5-35)frontend/src/components/Board/CategoryTabs.jsx (1)
CategoryTabs(4-24)frontend/src/components/Board/BoardActions.jsx (1)
BoardActions(6-32)frontend/src/components/Board/CreateSubBoardModal.jsx (1)
CreateSubBoardModal(4-41)
🔇 Additional comments (18)
frontend/src/components/Board/PostItem.jsx (1)
17-45: LGTM! 방어적 코딩이 잘 적용되었습니다.postId와 teamPath에 대한 유효성 검사가 적절하게 구현되어 있습니다. 사용자에게 명확한 피드백을 제공하고 있으며, 에러 로깅도 잘 되어 있습니다.
frontend/src/utils/boardApi.js (2)
10-240: LGTM! API 함수들의 반환 형식이 일관성 있게 구현되었습니다.모든 API 함수가
response.data를 반환하도록 통일되어 있습니다. 이전 리뷰에서 지적된updateComment의 반환 형식 불일치도 수정되었습니다.
1-3: No code duplication detected between axios.js and boardApi.js.The files follow proper separation of concerns:
axios.jsexports a centralized, pre-configured API instance with token refresh interceptors, whileboardApi.jscorrectly imports and reuses this shared instance for all board-related API calls. No duplicate configuration or interceptor logic exists.Likely an incorrect or invalid review comment.
frontend/src/components/Board/PostDetail/PostView.jsx (1)
9-79: LGTM! 깔끔한 컴포넌트 구조입니다.PostView 컴포넌트가 잘 분리되어 있고, 폴백 로직도 적절하게 처리되어 있습니다. FileAttachmentList와의 통합도 깔끔합니다.
frontend/src/components/Board/SearchBar.jsx (1)
5-35: LGTM! 검색 컴포넌트가 더 독립적으로 개선되었습니다.내부 상태 관리로 전환하여 컴포넌트가 더 자립적이 되었습니다. Enter 키 지원과 빈 문자열 검증도 잘 구현되어 있습니다.
frontend/src/components/Board/PostDetail/PostEditForm.jsx (1)
5-85: LGTM! 폼 구조가 명확하고 파일 관리가 잘 분리되어 있습니다.기존 파일과 새 파일을 별도로 관리하는 접근 방식이 적절합니다. FileAttachmentList를 재사용하여 코드 중복도 방지했습니다.
frontend/src/components/Board/PostDetail/CommentSection.jsx (2)
83-90: LGTM! 댓글 깊이 제한이 잘 구현되었습니다.
depth === 0조건으로 답글 버튼을 제한하여 무한 중첩을 방지한 것이 좋습니다. 이는 UX와 성능 모두에 긍정적입니다.
156-158: LGTM! 키보드 단축키가 사용자 경험을 향상시킵니다.Ctrl+Enter로 댓글 제출하는 기능은 일반적인 UX 패턴이며 잘 구현되어 있습니다.
frontend/src/pages/PostDetail.jsx (4)
124-138: LGTM! 낙관적 업데이트와 롤백이 잘 구현되었습니다.좋아요 기능에서 즉각적인 UI 피드백을 제공하고, 실패 시 이전 상태로 롤백하는 패턴이 훌륭합니다. 사용자 경험을 크게 향상시킵니다.
81-94: LGTM! 데이터 새로고침 로직이 잘 개선되었습니다.이전 리뷰에서 지적된 stale state 문제가 해결되었습니다.
refreshPostAndComments가 업데이트된 post를 반환하고, 호출자가 이를 사용하도록 개선되었습니다.
96-121: LGTM! 초기 데이터 로딩과 에러 처리가 잘 구현되었습니다.loading과 error 상태를 적절히 관리하고, 사용자에게 명확한 피드백을 제공합니다. 에러 발생 시 빈 댓글 배열로 초기화하는 것도 좋은 방어 코딩입니다.
301-349: LGTM! 컴포넌트 구성이 깔끔하고 관심사가 잘 분리되어 있습니다.PostView, PostEditForm, CommentSection으로 명확하게 분리된 구조가 훌륭합니다. 조건부 렌더링도 직관적이며, props 전달이 적절합니다.
frontend/src/pages/Board.jsx (6)
1-35: 상태 및 참조 초기화가 적절합니다.
prevPostsRef를 사용하여 이전 상태를 추적하는 패턴이 잘 구현되어 있습니다. 이전 리뷰에서 지적된 클로저 문제가 해결되었습니다.
90-121: 게시글 로드 로직이 잘 구현되었습니다.배열과 단일
currentBoardId케이스를 분리하여 처리하고,Promise.all을 사용한 병렬 요청 및 개별 에러 처리가 적절합니다.
123-159: 검색 로직이 적절하게 구현되었습니다.빈 검색어 처리와 다중 게시판 검색이 잘 구현되어 있습니다.
161-203: 하위 게시판 로딩 로직이 잘 구성되었습니다.
useEffect내에서 하위 게시판 로드와 첫 번째 탭의 게시글 로드가 적절히 처리되고 있습니다. 이전 리뷰에서 지적된 중복useEffect문제가 해결되었습니다.
332-382: Optimistic UI 패턴이 잘 구현되었습니다.이전 리뷰에서 지적된 클로저 문제가
prevPostsRef를 사용하여 해결되었습니다. 스냅샷을 optimistic update 전에 캡처하고 에러 발생 시 롤백하는 패턴이 올바르게 구현되었습니다.
447-461: 렌더링 로직이 깔끔하게 구현되었습니다.로딩 상태, 빈 상태, 게시글 목록이 조건에 따라 적절하게 렌더링되며,
keyprop도 올바르게 설정되어 있습니다.
| const nameToPath = { | ||
| 증권1팀: 'securities-1', | ||
| 증권2팀: 'securities-2', | ||
| 증권3팀: 'securities-3', | ||
| 자산운용: 'asset-management', | ||
| 금융IT: 'finance-it', | ||
| 매크로: 'macro', | ||
| 트레이딩: 'trading', | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
게시판 이름-경로 매핑을 중앙화하세요.
하드코딩된 nameToPath 매핑이 컴포넌트 내부에 있어 유지보수가 어렵습니다. 이 매핑 로직은 여러 곳에서 사용될 가능성이 높으므로 별도 유틸리티 파일로 분리하는 것이 좋습니다.
다음과 같이 별도 파일로 추출하는 것을 권장합니다:
frontend/src/utils/boardMappings.js 파일을 생성:
export const boardNameToPath = {
'증권1팀': 'securities-1',
'증권2팀': 'securities-2',
'증권3팀': 'securities-3',
'자산운용': 'asset-management',
'금융IT': 'finance-it',
'매크로': 'macro',
'트레이딩': 'trading',
};그리고 이 파일에서 import:
+import { boardNameToPath } from '../../utils/boardMappings';
- 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 = boardNameToPath[boardName] || team;🤖 Prompt for AI Agents
In frontend/src/components/Board/PostItem.jsx around lines 24 to 32, the
nameToPath object is hardcoded inside the component; extract this mapping into a
shared utility to centralize it. Create frontend/src/utils/boardMappings.js that
exports the mapping as boardNameToPath (named export), replace the inline
nameToPath with an import: import { boardNameToPath } from
'src/utils/boardMappings'; and update usages in the component to reference
boardNameToPath; ensure the new file is committed and any other components that
use the same mapping import from the new utility.
| if (sortOption === 'popular') { | ||
| return b.likeCount - a.likeCount; | ||
| } |
There was a problem hiding this comment.
likeCount가 undefined인 경우 정렬 오류 가능성이 있습니다.
likeCount가 undefined일 때 NaN 비교로 인해 예측 불가능한 정렬 결과가 발생할 수 있습니다.
if (sortOption === 'popular') {
- return b.likeCount - a.likeCount;
+ return (b.likeCount || 0) - (a.likeCount || 0);
}🤖 Prompt for AI Agents
In frontend/src/pages/Board.jsx around lines 395 to 397, the comparator uses
a.likeCount and b.likeCount directly which can be undefined and produce
NaN/unstable ordering; update the comparator to coerce undefined to 0 (or
Number) before comparing (e.g., use const aCount = Number(a.likeCount ?? 0) and
bCount = Number(b.likeCount ?? 0)) and then return bCount - aCount so the sort
is predictable when likeCount is missing.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
frontend/src/pages/Board.jsx (2)
63-71: 하드코딩된 게시판 경로 매핑이 유지보수에 취약합니다.이전 리뷰에서 지적된 문제가 아직 해결되지 않았습니다. 게시판 이름이 변경되거나 새로운 게시판이 추가되면 코드 수정이 필요합니다.
API 응답에 URL slug를 포함하거나, 게시판 ID 기반 라우팅(
/board/:boardId)으로 전환하는 것을 권장합니다.
408-410:likeCount가undefined인 경우 정렬 오류 가능성이 있습니다.이전 리뷰에서 지적된 문제가 아직 해결되지 않았습니다.
likeCount가undefined일 때NaN비교로 인해 예측 불가능한 정렬 결과가 발생할 수 있습니다.if (sortOption === 'popular') { - return b.likeCount - a.likeCount; + return (b.likeCount || 0) - (a.likeCount || 0); }
🧹 Nitpick comments (8)
frontend/src/components/Board/PostDetail/PostEditForm.jsx (2)
61-74: 파일 업로드 검증 로직 추가를 권장합니다.현재 파일 입력은 크기 제한, 타입 제한, 개수 제한 등의 검증이 없어 대용량 파일이나 허용되지 않는 파일 형식이 업로드될 수 있습니다.
다음과 같이 검증 로직을 추가하는 것을 권장합니다:
+const handleFileChange = (e) => { + const files = Array.from(e.target.files); + const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']; + + const validFiles = files.filter(file => { + if (file.size > MAX_FILE_SIZE) { + alert(`${file.name}은(는) 10MB를 초과합니다.`); + return false; + } + if (!ALLOWED_TYPES.includes(file.type)) { + alert(`${file.name}의 형식은 지원되지 않습니다.`); + return false; + } + return true; + }); + + if (validFiles.length > 0) { + onAddNewFile({ target: { files: validFiles } }); + } +}; + <input type="file" multiple - onChange={onAddNewFile} + onChange={handleFileChange} + accept=".jpg,.jpeg,.png,.gif,.pdf" style={{ display: 'none' }} id="file-upload" />
5-17: PropTypes 또는 TypeScript를 사용한 타입 검증을 권장합니다.컴포넌트의 props에 대한 타입 검증이 없어 잘못된 타입의 값이 전달될 경우 런타임 오류가 발생할 수 있습니다.
frontend/src/pages/PostDetail.jsx (2)
96-121: useEffect cleanup 함수 추가를 고려하세요.컴포넌트가 언마운트되기 전에 비동기 작업이 완료되면 언마운트된 컴포넌트에 상태 업데이트를 시도할 수 있습니다.
다음과 같이 cleanup 패턴을 적용할 수 있습니다:
useEffect(() => { + let isMounted = true; const fetchPostAndComments = async () => { if (!postId) { setError('게시글 ID가 없습니다.'); setLoading(false); return; } try { setLoading(true); const data = await boardApi.getPost(postId); + if (!isMounted) return; setPost(data); setEditTitle(data.title); setEditContent(data.content); const raw = extractRawComments(data); setComments(buildCommentTree(raw)); setError(null); } catch (error) { console.error('게시글 불러오기 실패:', error); + if (!isMounted) return; setError('게시글을 불러올 수 없습니다.'); setComments([]); } finally { + if (!isMounted) return; setLoading(false); } }; fetchPostAndComments(); + return () => { + isMounted = false; + }; }, [postId]);
238-254: 댓글 수정 시 prompt 대신 모달을 사용하는 것을 권장합니다.
prompt()는 스타일링이 제한적이고 모바일 환경에서 사용자 경험이 좋지 않습니다.별도의 CommentEditModal 컴포넌트를 생성하거나 인라인 편집 UI를 제공하는 것을 권장합니다.
frontend/src/components/Board/SearchBar.module.css (1)
104-109: 미디어 쿼리에서max-width선언이 불필요합니다.
--search-width가100%로 변경되면서max-width: var(--search-width)는max-width: 100%가 되어width: 100%와 중복됩니다.@media (max-width: 1024px) { .searchContainer { width: 100%; - max-width: var(--search-width); } }frontend/src/pages/Board.jsx (3)
35-43:prevPostsRef초기화 방식을 개선할 수 있습니다.
prevPostsRef가posts상태값으로 초기화되지만, 이 시점에서posts는 빈 배열입니다.useRef(null)로 초기화하고useEffect에서만 업데이트하는 것이 더 명확합니다.- const prevPostsRef = useRef(posts); + const prevPostsRef = useRef(null);
498-508: 페이지 수가 많을 때 UI 개선을 고려하세요.현재 구현은 모든 페이지 버튼을 렌더링합니다. 게시글이 많아지면 버튼이 과도하게 표시될 수 있으므로, 일정 수 이상일 때 말줄임표(
...) 패턴을 적용하는 것을 고려하세요.예시:
// 페이지가 7개 이상일 때 말줄임표 적용 const getPageNumbers = () => { if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1); if (currentPage <= 3) return [1, 2, 3, 4, '...', totalPages]; if (currentPage >= totalPages - 2) return [1, '...', totalPages - 3, totalPages - 2, totalPages - 1, totalPages]; return [1, '...', currentPage - 1, currentPage, currentPage + 1, '...', totalPages]; };
324-328:selectedFiles초기화가 중복됩니다.Line 326에서
setSelectedFiles([])를 호출하지만, Line 325의handleCloseModal()내부(Line 242)에서 이미 초기화됩니다.await boardApi.createPost(activeSubBoard, postData); handleCloseModal(); - setSelectedFiles([]); handleTabChange(activeSubBoard);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
frontend/src/components/Board/PostDetail/CommentSection.jsx(1 hunks)frontend/src/components/Board/PostDetail/FileAttachmentList.jsx(1 hunks)frontend/src/components/Board/PostDetail/PostEditForm.jsx(1 hunks)frontend/src/components/Board/PostDetail/PostView.jsx(1 hunks)frontend/src/components/Board/SearchBar.module.css(1 hunks)frontend/src/pages/Board.jsx(4 hunks)frontend/src/pages/Board.module.css(3 hunks)frontend/src/pages/PostDetail.jsx(3 hunks)frontend/src/pages/PostDetail.module.css(12 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/src/components/Board/PostDetail/PostView.jsx
- frontend/src/components/Board/PostDetail/FileAttachmentList.jsx
- frontend/src/components/Board/PostDetail/CommentSection.jsx
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/Board/PostDetail/PostEditForm.jsx (3)
frontend/src/pages/Board.jsx (2)
title(20-20)content(21-21)frontend/src/components/Board/PostDetail/FileAttachmentList.jsx (1)
FileAttachmentList(5-53)frontend/src/pages/PostDetail.jsx (2)
editFiles(72-72)newFiles(73-73)
frontend/src/pages/Board.jsx (6)
frontend/src/components/Board/PostItem.jsx (3)
useParams(13-13)postId(15-15)PostItem(11-118)frontend/src/utils/boardApi.js (14)
response(11-11)response(21-21)response(47-47)response(61-63)response(81-83)response(108-110)response(126-128)response(156-158)response(168-168)response(180-180)response(190-190)response(211-211)response(228-228)response(238-238)frontend/src/components/Board/SearchBar.jsx (2)
handleSearch(8-12)SearchBar(5-35)frontend/src/components/Board/CategoryTabs.jsx (1)
CategoryTabs(4-24)frontend/src/components/Board/BoardActions.jsx (1)
BoardActions(6-32)frontend/src/components/Board/CreateSubBoardModal.jsx (1)
CreateSubBoardModal(4-41)
🔇 Additional comments (11)
frontend/src/pages/PostDetail.jsx (2)
11-27: LGTM!댓글 트리 구조를 효율적으로 구축하고 있습니다. Map을 사용하여 O(n) 시간 복잡도로 부모-자식 관계를 처리하는 방식이 적절합니다.
124-138: LGTM!낙관적 업데이트 패턴이 올바르게 구현되어 있습니다. 이전 상태를 백업하고 실패 시 복원하는 방식이 사용자 경험을 개선합니다.
frontend/src/pages/PostDetail.module.css (3)
4-4: 너비 변경에 대한 검증을 권장합니다.고정 너비
926px에서75%로 변경되었습니다. 매우 작거나 큰 화면에서 레이아웃이 의도대로 동작하는지 확인이 필요합니다.다음과 같이 다양한 화면 크기에서 테스트해주세요:
- 1920px 이상의 큰 화면에서 75%가 너무 넓지 않은지
- 768px 이하에서는 반응형 스타일이 적용되는지
376-378: LGTM!숨겨진 파일 입력을 위한 유틸리티 클래스가 적절하게 추가되었습니다. 접근성을 위해
display: none이 올바르게 사용되었습니다.
703-714: LGTM!답글 textarea 스타일이 잘 구성되어 있습니다.
box-sizing: border-box를 사용하여 일관된 크기 계산을 보장하고 있습니다.frontend/src/components/Board/SearchBar.module.css (1)
1-15: LGTM! 반응형 너비로 변경이 적절합니다.고정 너비(
908px)에서 유동 너비(100%)로 변경하여 다양한 화면 크기에서 검색바가 유연하게 확장됩니다.Board.module.css의 레이아웃 변경과 일관성을 유지합니다.frontend/src/pages/Board.module.css (2)
12-22: 전체 화면 배경 오버레이가 추가되었습니다.
position: fixed와100vw/100vh를 사용한::before의사 요소가 전체 화면 흰색 배경을 생성합니다.z-index: -1과pointer-events: none으로 상호작용을 차단하지 않습니다.다른 페이지나 컴포넌트에서 의도치 않은 시각적 영향이 없는지 확인하세요.
220-251: 페이지네이션 스타일이 잘 구성되었습니다.hover, disabled, active 상태가 적절히 처리되었고, 트랜지션과 접근성 고려(
cursor: not-allowed)가 포함되어 있습니다.frontend/src/pages/Board.jsx (3)
93-127:fetchPosts구현이 적절합니다.배열과 단일
boardId를 모두 처리하고, 개별 게시판 조회 실패 시에도 다른 게시판의 결과를 반환합니다. 로딩 상태와 페이지 리셋 처리가 잘 되어 있습니다.
170-209:loadSubBoardsuseEffect의 의존성 배열을 확인하세요.
fetchPosts가 의존성 배열에 포함되어 있고,fetchPosts는currentBoardId가 변경될 때마다 재생성됩니다. 이로 인해currentBoardId변경 시 이 useEffect가 두 번 트리거될 수 있습니다.하위 게시판이 있는 경우 직접 posts를 로드하고, 없는 경우에만
fetchPosts를 호출하는 현재 로직은 의도적인 것으로 보이나, 불필요한 재실행을 방지하려면fetchPosts를 의존성에서 제외하고 내부에서 직접 API를 호출하는 것을 고려하세요.
339-362: 낙관적 업데이트 패턴이 개선되었습니다.
prevPostsRef를 사용하여 이전 상태를 캡처하고 API 실패 시 롤백하는 방식이 이전 리뷰에서 지적된 클로저 문제를 해결합니다.
| if (sortOption === 'latest') { | ||
| return new Date(b.date) - new Date(a.date); | ||
| const dateA = new Date(a.createdDate || a.date); | ||
| const dateB = new Date(b.createdDate || b.date); | ||
| return dateB - dateA; | ||
| } | ||
| if (sortOption === 'oldest') { | ||
| return new Date(a.date) - new Date(b.date); | ||
| const dateA = new Date(a.createdDate || a.date); | ||
| const dateB = new Date(b.createdDate || b.date); | ||
| return dateA - dateB; |
There was a problem hiding this comment.
날짜 정렬 시 유효하지 않은 날짜 처리가 필요합니다.
createdDate와 date가 모두 없거나 유효하지 않은 경우 Invalid Date가 생성되어 정렬이 예측 불가능해질 수 있습니다.
if (sortOption === 'latest') {
- const dateA = new Date(a.createdDate || a.date);
- const dateB = new Date(b.createdDate || b.date);
+ const dateA = new Date(a.createdDate || a.date || 0);
+ const dateB = new Date(b.createdDate || b.date || 0);
return dateB - dateA;
}
if (sortOption === 'oldest') {
- const dateA = new Date(a.createdDate || a.date);
- const dateB = new Date(b.createdDate || b.date);
+ const dateA = new Date(a.createdDate || a.date || 0);
+ const dateB = new Date(b.createdDate || b.date || 0);
return dateA - dateB;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (sortOption === 'latest') { | |
| return new Date(b.date) - new Date(a.date); | |
| const dateA = new Date(a.createdDate || a.date); | |
| const dateB = new Date(b.createdDate || b.date); | |
| return dateB - dateA; | |
| } | |
| if (sortOption === 'oldest') { | |
| return new Date(a.date) - new Date(b.date); | |
| const dateA = new Date(a.createdDate || a.date); | |
| const dateB = new Date(b.createdDate || b.date); | |
| return dateA - dateB; | |
| if (sortOption === 'latest') { | |
| const dateA = new Date(a.createdDate || a.date || 0); | |
| const dateB = new Date(b.createdDate || b.date || 0); | |
| return dateB - dateA; | |
| } | |
| if (sortOption === 'oldest') { | |
| const dateA = new Date(a.createdDate || a.date || 0); | |
| const dateB = new Date(b.createdDate || b.date || 0); | |
| return dateA - dateB; |
🤖 Prompt for AI Agents
frontend/src/pages/Board.jsx around lines 398 to 406: when sorting by date you
currently create Date objects from a.createdDate || a.date and b.createdDate ||
b.date but do not handle missing/invalid dates, which yields Invalid Date and
unpredictable ordering; fix by normalizing to numeric timestamps: parse each
candidate date, check validity (isNaN(date.getTime()) or Number.isNaN(+date)),
and map invalid/missing ones to a stable fallback numeric value depending on
sortOption (e.g., for 'latest' map invalid to -Infinity so they sort last, for
'oldest' map invalid to +Infinity so they sort last), then subtract these
numeric timestamps for the comparator; ensure you perform this validation for
both branches ('latest' and 'oldest').
| const handleAttachmentDownload = (file) => { | ||
| const baseUrl = import.meta.env.VITE_API_URL || ''; | ||
| const url = `${baseUrl}${file.filePath}`; | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| a.download = file.originalFilename; | ||
| document.body.appendChild(a); | ||
| a.click(); | ||
| a.remove(); | ||
| }; |
There was a problem hiding this comment.
파일 다운로드 경로 검증을 추가하세요.
file.filePath를 직접 사용하여 URL을 생성하므로 악의적인 경로가 포함된 경우 보안 문제가 발생할 수 있습니다.
다음과 같이 경로 검증을 추가하세요:
const handleAttachmentDownload = (file) => {
+ if (!file.filePath || typeof file.filePath !== 'string') {
+ alert('잘못된 파일 경로입니다.');
+ return;
+ }
+ // 상대 경로만 허용하도록 검증
+ if (file.filePath.startsWith('http://') || file.filePath.startsWith('https://') || file.filePath.includes('..')) {
+ alert('허용되지 않는 파일 경로입니다.');
+ return;
+ }
const baseUrl = import.meta.env.VITE_API_URL || '';
const url = `${baseUrl}${file.filePath}`;
const a = document.createElement('a');
a.href = url;
a.download = file.originalFilename;
document.body.appendChild(a);
a.click();
a.remove();
};🤖 Prompt for AI Agents
In frontend/src/pages/PostDetail.jsx around lines 158 to 167, the handler builds
a download URL from file.filePath directly which can allow malicious paths;
validate and normalize the path before creating the link. Ensure file.filePath
is a relative path that matches an allowed pattern (e.g. starts with "/uploads/"
or a configured prefix) and reject or sanitize any value containing suspicious
segments like ".." or backslashes; construct the final URL with the URL
constructor against the base API URL (not simple string concatenation) and
encode the filename when setting the download attribute; on validation failure,
abort and surface a safe error.
1) 작업한 이슈번호
SISC1-211-FE
2) 변경 요약 (What & Why)
3) 스크린샷/동영상 (UI 변경 시)
4) 상세 변경사항 (전부 다)
5) 참고사항
Summary by CodeRabbit
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.