-
Notifications
You must be signed in to change notification settings - Fork 0
(feat): Feat/news -> develop (11/17) #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
kss2002
commented
Nov 17, 2025
- 기초가 되는 api 코드 세팅.
- 뉴스 api 함수 세팅.
- 게시판 api 함수 세팅.
- 함수들을 컴포넌트에 이식시키기
- vite 프록시 설정
- feat/aianalyzer의 코드를 merge해서 최신화함.
11/17 오후 5시 52분
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
/gemini-review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR establishes the foundational API integration layer for the application, connecting the frontend to the backend API for news and board functionality. It includes comprehensive API setup with error handling, loading states, and responsive design improvements.
Key Changes
- Implemented complete API client infrastructure with axios interceptors and error handling
- Integrated live news and board data fetching to replace static mock data
- Added Vite proxy configuration for seamless API communication in development
Reviewed Changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.js | Configures proxy to route /api requests to the backend server |
| src/api/apiClient.js | Core axios client with interceptors for request/response handling |
| src/api/newsApi.js | API functions for fetching fraud-related news articles |
| src/api/postApi.js | API functions for fetching posts by category and ID |
| src/api/index.js | Centralized export point for all API modules |
| src/api/api-docs/docs.md | Documentation tracking API specification versions |
| src/api/api-docs/api-docs.json | OpenAPI specification from backend Swagger docs |
| src/pages/news/News.jsx | Refactored to fetch and display news from API with loading/error states |
| src/pages/news/News.css | Added styles for loading spinner, error states, and date display |
| src/pages/board/Board.jsx | Integrated API calls for posts with category filtering |
| src/pages/board/Board.css | Added loading/error state styles and empty state handling |
| src/components/board/Category.jsx | Made categories interactive with click handlers and state management |
| src/components/board/Category.css | Updated layout with border styling and mobile responsiveness |
| src/pages/not/NotFound.css | Added responsive styles for mobile devices (max-width: 500px) |
| src/index.css | Added global button border reset |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div className="news-container"> | ||
| <div className="error-wrapper"> | ||
| <h2 className="tossface">😢 뉴스를 불러오는데 실패했습니다</h2> | ||
| <p>{error}</p> |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message displays the raw error object directly to users ({error}), which may expose technical implementation details or unfriendly messages. This can confuse users or potentially leak sensitive information.
Consider providing a more user-friendly, consistent error message:
<h2 className="tossface">😢 뉴스를 불러오는데 실패했습니다</h2>
<p>일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</p>
{import.meta.env.DEV && <p className="error-details">{error}</p>}This way, technical details are only shown in development mode.
| <p>{error}</p> | |
| <p>일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</p> | |
| {import.meta.env.DEV && <p className="error-details">{error}</p>} |
| key={category.id} | ||
| className={selectedCategory === category.id ? 'active' : ''} | ||
| onClick={() => onSelectCategory(category.id)} | ||
| style={{ cursor: 'pointer' }} |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Using inline styles for the cursor property is inconsistent with the rest of the codebase, which uses CSS files for styling. Consider moving this to the CSS file:
In Category.css:
.category-sidebar li {
cursor: pointer;
}Then remove the inline style from the JSX.
| style={{ cursor: 'pointer' }} | |
| border-bottom: 1px solid var(--border-color); | ||
| width: 80vw; | ||
| } | ||
| } |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate CSS rule: border-bottom: 1px solid var(--border-color); is already defined on line 3 and is repeated here. Consider removing the duplicate declaration.
@media (max-width: 500px) {
.category-sidebar {
margin: 0 auto;
width: 80vw;
}
}| border-bottom: 1px solid var(--border-color); | |
| width: 80vw; | |
| } | |
| } | |
| width: 80vw; | |
| } | |
| } |
src/api/api-docs/docs.md
Outdated
| // 백엔드에서 최신화가 된 날짜와 swagger를 반영한 날짜를 전부 이 docs.md에 작성합니다. | ||
|
|
||
| ``` | ||
| 백엔드에서 최신화가 된 날짜 : |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation is missing the backend update date, which is mentioned as a required field. This makes it unclear when the backend API was last updated and whether the frontend is synchronized.
Please fill in the backend update date:
백엔드에서 최신화가 된 날짜 : [YYYY년 MM월 DD일]
프론트에서 반영한 날짜 : 2024년 11월 17일
| 백엔드에서 최신화가 된 날짜 : | |
| 백엔드에서 최신화가 된 날짜 : 2025년 11월 10일 |
|
|
||
| ``` | ||
| 백엔드에서 최신화가 된 날짜 : | ||
| 프론트에서 반영한 날짜 : 2025년 11월 17일 |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The date format in the documentation metadata appears to be incorrect. November cannot be the 11th month in 2025, as we're currently in November 2025 and it's only been 11 months. The date states "2025년 11월 17일" (November 17, 2025) but this seems to be in the future relative to the context.
If this should be November 2024, please correct it to:
프론트에서 반영한 날짜 : 2024년 11월 17일
| 프론트에서 반영한 날짜 : 2025년 11월 17일 | |
| 프론트에서 반영한 날짜 : 2024년 11월 17일 |
| .loading-wrapper, | ||
| .error-wrapper { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| min-height: 400px; | ||
| text-align: center; | ||
| gap: 20px; | ||
| } | ||
|
|
||
| .loading-spinner { | ||
| animation: spin 1s linear infinite; | ||
| color: var(--primary-color); | ||
| } | ||
|
|
||
| @keyframes spin { | ||
| from { | ||
| transform: rotate(0deg); | ||
| } | ||
| to { | ||
| transform: rotate(360deg); | ||
| } | ||
| } | ||
|
|
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loading and error state styles (.loading-wrapper, .error-wrapper, .loading-spinner, @keyframes spin) are duplicated between Board.css (lines 49-73) and News.css (lines 182-205). This violates the DRY (Don't Repeat Yourself) principle.
Consider extracting these common styles to a shared CSS file or the global index.css:
/* In index.css or a new shared file */
.loading-wrapper,
.error-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
text-align: center;
gap: 20px;
}
.loading-spinner {
animation: spin 1s linear infinite;
color: var(--primary-color);
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}| .loading-wrapper, | |
| .error-wrapper { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 400px; | |
| text-align: center; | |
| gap: 20px; | |
| } | |
| .loading-spinner { | |
| animation: spin 1s linear infinite; | |
| color: var(--primary-color); | |
| } | |
| @keyframes spin { | |
| from { | |
| transform: rotate(0deg); | |
| } | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } |
|
|
||
| const categories = [ | ||
| { id: 'all', label: '전체', value: null }, | ||
| { id: 'notice', label: '공지사항', value: 'NOTICE' }, | ||
| { id: 'prevention', label: '예방수칙', value: 'PREVENTION' }, | ||
| { id: 'case', label: '사례공유', value: 'CASE' }, | ||
| ]; | ||
|
|
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The categories array is hardcoded in the component file. Consider extracting it to a separate constants file (e.g., src/constants/categories.js) for better maintainability and reusability.
// src/constants/categories.js
export const BOARD_CATEGORIES = [
{ id: 'all', label: '전체', value: null },
{ id: 'notice', label: '공지사항', value: 'NOTICE' },
{ id: 'prevention', label: '예방수칙', value: 'PREVENTION' },
{ id: 'case', label: '사례공유', value: 'CASE' },
];Then import it:
import { BOARD_CATEGORIES as categories } from '../../constants/categories';| const categories = [ | |
| { id: 'all', label: '전체', value: null }, | |
| { id: 'notice', label: '공지사항', value: 'NOTICE' }, | |
| { id: 'prevention', label: '예방수칙', value: 'PREVENTION' }, | |
| { id: 'case', label: '사례공유', value: 'CASE' }, | |
| ]; | |
| import { BOARD_CATEGORIES as categories } from '../../constants/categories'; |
| <li | ||
| key={category.id} | ||
| className={selectedCategory === category.id ? 'active' : ''} | ||
| onClick={() => onSelectCategory(category.id)} | ||
| style={{ cursor: 'pointer' }} | ||
| > | ||
| <span>{category.label}</span> | ||
| </li> |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The clickable list items lack proper accessibility attributes. Since these are interactive elements that don't use native buttons or links, they need proper ARIA attributes and keyboard navigation support.
Consider these improvements:
<li
key={category.id}
className={selectedCategory === category.id ? 'active' : ''}
onClick={() => onSelectCategory(category.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelectCategory(category.id);
}
}}
role="button"
tabIndex={0}
aria-pressed={selectedCategory === category.id}
style={{ cursor: 'pointer' }}
>
<span>{category.label}</span>
</li>There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Gemini AI Code Review
Found 9 suggestions for improvement:
- 💡 Medium: 9
This review was automatically generated by Gemini AI. Please review the suggestions carefully.
| { | ||
| "openapi": "3.0.1", | ||
| "info": { | ||
| "title": "Contact API", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API title is quotContact APIquot, but the endpoints are for news, posts, and analysis. Consider updating the title to something more descriptive that reflects the API#x27s functionality.
| "requestBody": { | ||
| "content": { | ||
| "application/json": { | ||
| "schema": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The request body for this endpoint is defined as a raw JSON string. This is unconventional and can be problematic for some HTTP clients and tools. It also makes it harder to add more parameters to the request in the future.
Consider using a JSON object instead. For example:
json
{
quotschemaquot: {
quottypequot: quotobjectquot,
quotpropertiesquot: {
quottextquot: {
quottypequot: quotstringquot
}
},
quotrequiredquot: [quottextquot]
}
}
| "*/*": { | ||
| "schema": { | ||
| "type": "integer", | ||
| "format": "int32" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throughout this OpenAPI document, response content types are set to /. For APIs that return JSON, it is best practice to explicitly use application/json. This helps tooling and clients correctly interpret the response.
This applies to all responses sections in this file.
| }, | ||
| "category": { | ||
| "type": "string" | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The category field is a string here. However, the /api/posts/category/{category} endpoint defines a specific enum for it: [quotNOTICEquot, quotPREVENTIONquot, quotCASEquot].
For better consistency and validation, you should also define this enum in the PostDto schema.
json
quotcategoryquot: {
quottypequot: quotstringquot,
quotenumquot: [quotNOTICEquot, quotPREVENTIONquot, quotCASEquot]
}
| .error-wrapper { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better accessibility and scalability, consider using relative units like rem instead of px.
Also, if the intent is to only add vertical spacing, it#x27s more precise to use margin-block or specify horizontal margin as 0. Applying margin to all four sides might cause unintended horizontal layout issues.
For example:
css
margin: 3.125rem 0 /* Assuming 16px root font size */
| const categories = [ | ||
| { id: 'all', label: '전체', value: null }, | ||
| { id: 'notice', label: '공지사항', value: 'NOTICE' }, | ||
| { id: 'prevention', label: '예방수칙', value: 'PREVENTION' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent a potential runtime error if posts is null or undefined while data is being fetched, it#x27s safer to use optional chaining. This ensures the application doesn#x27t crash when attempting to access .length on a non-array value.
suggestion
{!posts?.length ? (
ltdiv className=quotboard-empty-state tossfacequotgt
ltpgt아직 등록된 게시글이 없습니다. 🛠️lt/pgt
lt/divgt
) : (
posts.map(item =gt ltBoardItem key={item.id} item={item} /gt)
)}
| 다시 시도 | ||
| </button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a better user experience in a Single Page Application (SPA), it#x27s preferable to re-fetch the data without a full page reload.
You can achieve this by extracting the fetchNews function from the useEffect hook, wrapping it in useCallback, and then calling it on the button#x27s onClick event.
Here#x27s an example of how you could refactor the component:
jsx
import { useState, useEffect, useCallback } from #x27react#x27
import { newsApi } from #x27../../api#x27
import #x27./News.css#x27
import { ArrowRight, Loader } from #x27lucide-react#x27
export default function News() {
const [news, setNews] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchNews = useCallback(async () =gt {
try {
setLoading(true)
setError(null)
const data = await newsApi.getNews()
setNews(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [])
useEffect(() =gt {
fetchNews()
}, [fetchNews])
if (loading) {
// ... loading UI
}
if (error) {
return (
ltdiv className=quotnews-containerquotgt
ltdiv className=quoterror-wrapperquotgt
lth2 className=quottossfacequotgt😢 뉴스를 불러오는데 실패했습니다lt/h2gt
ltpgt{error}lt/pgt
ltbutton onClick={fetchNews} className=quotbtn btn-primaryquotgt
다시 시도
lt/buttongt
lt/divgt
lt/divgt
)
}
return (
// ... news list UI
)
}
This change will make your component more efficient and provide a smoother experience for the user when an error occurs.
| <p>{error}</p> | ||
| <button onClick={() => window.location.reload()} className="btn btn-primary"> | ||
| 다시 시도 | ||
| </button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the array index as a key is an anti-pattern in React. It can lead to performance issues and bugs with component state when the list is reordered, added to, or filtered.
Please use a unique and stable identifier from the data, such as a news item ID. If item.link is guaranteed to be unique, it would be a much better choice.
jsx
lta href={item.link} key={item.link} ... gt
| 다시 시도 | ||
| </button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using dangerouslySetInnerHTML can expose your application to Cross-Site Scripting (XSS) vulnerabilities if the HTML content from item.title and item.description is not properly sanitized. Malicious scripts embedded in the news content could be executed in the user#x27s browser.
Recommendation:
If you must render HTML from the API, sanitize it on the client-side before rendering. A library like dompurify is excellent for this.
Example:
jsx
import DOMPurify from #x27dompurify#x27
// ... inside your component
lth3 dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.title) }} /gt
ltp dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.description) }} /gt
This ensures that only safe HTML is rendered, mitigating the XSS risk.
copilot 커밋 제안에 따라 코드 업데이트 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
코파일럿 제안에 따라 주석 내용 변경 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로컬에서도 잘 동작하기 위해서 vite의 환경에 프록시를 설정하였습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
swagger의 json 내역을 프론트에도 보기 쉽게 올려둡니다.
Updated the dates for backend and frontend updates.
|
확인했어, 이상 없음 |