- BoardList.jsx 코드 - 차장님이 설명해주신 내용 있는 코드. - 철저히 분석해서 내 걸로 이해할 것. 만들것.
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import Pagination from './Pagination';
const BoardList = () => {
const [posts, setPosts] = useState([
{"rnum":1,"hitCnt":18,"idx":5756,"title":"BatchTitle2","creaNm":"관리자","creaDtm":"2025-09-16"},
]);
const [searchTerm, setSearchTerm] = useState('');//검색어 (검색바)
const [submittedSearchTerm, setSubmittedSearchTerm] = useState(''); //검색어 (페이징 리스트)
const [searchType, setSearchType] = useState('TITLE'); //검색 조건
const [submittedSearchType, setSubmittedSearchType] = useState('TITLE');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(100);
//핸들링 이벤트의 실행주체
const handleSearch = () => {
console.log(searchTerm);
console.log(searchType);
setSubmittedSearchTerm(searchTerm);
setCurrentPage(1); // Reset to first page on new search
};
//이벤트 감지기 (키이벤트)
const handleKeyPress = (e) => {
if (e.key === 'Enter') {//엔터를 입력 했을때
handleSearch();
}
};
//서버 주소 처리
const retriveBoard = (type, keyword) => {
console.log(type, keyword);
axios.get("<http://localhost:8080/RestBoard/board/api/list>"
, {params: {
type: type,
keyword: keyword,
page: currentPage,
}})
.then(response => {
setPosts(response.data.list);
setTotalPages(response.data.totalpage);
console.log(response);
}).catch(error=>{
console.log(error);
// [추가] API 오류 시 대체 데이터 설정
setPosts([{ "rnum": 1, "hitCnt": 0, "idx": 9999, "title": "[오류] 목록을 불러올 수 없습니다. 백엔드 확인 필요.", "creaNm": "시스템", "creaDtm": "2025-11-04" }]);
setTotalPages(1);
})
};
useEffect(()=>{
retriveBoard(submittedSearchType, submittedSearchTerm);
}, [submittedSearchTerm, currentPage])
// Change page
const handlePageChange = (page) => setCurrentPage(page);
return (
<>
<h2>Board</h2>
<div className="row justify-content-between mb-3">
<div className="col-md-auto">
<Link to="/new" className="btn btn-primary">New Post</Link>
</div>
<div className="col-md-7">
<div className="input-group">
<select className="form-select" style={{ flex: '0 0 120px' }} value={searchType} onChange={e => setSearchType(e.target.value)}>
<option value="TITLE">Title</option>
<option value="CONTENTS">Content</option>
<option value="ID">Author</option>
</select>
<input
type="text"
className="form-control"
placeholder={`Search by ${searchType}...`}
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
onKeyDown={e => handleKeyPress(e)}
/>
<button className="btn btn-outline-secondary" type="button" onClick={e => handleSearch()}>Search</button>
</div>
</div>
</div>
{/* Post List */}
<ul className="list-group mb-4">
<li className="list-group-item d-flex justify-content-between align-items-center">
<span>제목</span> <span>글쓴이</span>
</li>
{posts.map(post => (
<li key={post.id} className="list-group-item d-flex justify-content-between align-items-center">
<Link to={`/post/${post.idx}`}>{post.title}</Link>
<span>{post.creaNm}</span>
</li>
))}
</ul>
{/* Pagination
*/}
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} />
</>
);
};
export default BoardList;
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import BoardList from './components/BoardList';
import PostView from './components/PostView';
import PostForm from './components/PostForm';
import './App.css';
//spring 서블릿
function App() {
return (
<Router>
<Header />
<main>
<Routes>
<Route path="/" element={<BoardList />} />
<Route path="/list" element={<BoardList />} />
<Route path="/post/:id" element={<PostView />} />
<Route path="/new" element={<PostForm />} />
</Routes>
</main>
</Router>
);
}
export default App;
import React from 'react';
import { Link } from 'react-router-dom';
const Header = () => {
return (
<header>
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div className="container">
<Link className="navbar-brand" to="/">React Board</Link>
<div className="collapse navbar-collapse">
<ul className="navbar-nav ms-auto mb-2 mb-lg-0">
<li className="nav-item">
<Link className="nav-link" to="/">Home</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/new">New Post</Link>
</li>
</ul>
</div>
</div>
</nav>
</header>
);
};
export default Header;
// Pagination.js
import React from 'react';
import '../Pagination.css';
// 2. 부모로부터 필요한 값과 함수를 props로 받습니다.
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
// 1. 자식은 더 이상 페이지 상태를 직접 관리하지 않습니다.
// const [currentPage, setCurrentPage] = useState(1); // 이 줄을 삭제!
const pagesPerGroup = 10;
const startPage = Math.floor((currentPage - 1) / pagesPerGroup) * pagesPerGroup + 1;
const endPage = Math.min(startPage + pagesPerGroup - 1, totalPages);
const pageNumbers = [];
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(i);
}
// 3. 모든 클릭 이벤트 핸들러에서 onPageChange를 호출합니다.
const handleClick = (pageNumber) => {
onPageChange(pageNumber);
};
const handlePrevClick = () => {
onPageChange(Math.max(currentPage - 1, 1));
};
const handleNextClick = () => {
onPageChange(Math.min(currentPage + 1, totalPages));
};
const handlePrevGroupClick = () => {
onPageChange(Math.max(startPage - pagesPerGroup, 1));
};
const handleNextGroupClick = () => {
onPageChange(Math.min(startPage + pagesPerGroup, totalPages));
};
return (
<div className="pagination">
<button onClick={handlePrevGroupClick} disabled={startPage === 1}>
<<
</button>
<button onClick={handlePrevClick} disabled={currentPage === 1}>
<
</button>
{pageNumbers.map((number) => (
<button
key={number}
onClick={() => handleClick(number)}
className={currentPage === number ? 'active' : ''}
disabled={currentPage === number}
>
{number}
</button>
))}
<button onClick={handleNextClick} disabled={currentPage === totalPages}>
>
</button>
<button onClick={handleNextGroupClick} disabled={endPage === totalPages}>
>>
</button>
</div>
);
};
export default Pagination;
import React from 'react';
import { useParams, Link } from 'react-router-dom';
import axios from 'axios'; // 추가 axios 임포트
const PostView = () => {
const { id } = useParams();
// [추가] 게시글 상태 및 로딩 상태 추가
const [post, setPost] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// [추가] useEffect를 사용하여 API에서 게시글 데이터 조회
useEffect(() => {
setIsLoading(true);
axios.get(`http://localhost:8080/RestBoard/board/api/view`, {
params: { idx: id }
})
.then(response => {
// API 응답의 데이터 구조에 따라 post 객체를 설정합니다.
setPost(response.data);
})
.catch(error => {
console.error("Error fetching post:", error);
// 에러 발생 시 더미 데이터로 대체
setPost({
idx: id,
title: `[ERROR] Post ${id} Load Failed`,
creaNm: `Error User`,
content: `Could not fetch content. Check API endpoint: <http://localhost:8080/RestBoard/board/api/view?idx=${id}`>
});
})
.finally(() => {
setIsLoading(false);
});
}, [id]);
// [추가] 로딩 상태 처리
if (isLoading) {
return <h2>Loading post...</h2>;
}
// [추가] 데이터가 없을 경우 처리
if (!post) {
return <h2>Post not found or failed to load.</h2>;
}
// In a real app, you would fetch the post data based on the id
// const post = { id: id, title: `Post ${id}`, author: `User${id}`, content: `This is the full content of post ${id}.` };
return (
<>
<h2>{post.title}</h2>
<p><strong>Author:</strong> {post.creaNm}</p>
<p>{post.content}</p>
<Link to="/list" className="btn btn-secondary">Back to List</Link>
</>
);
};
export default PostView;
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const PostForm = () => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
// In a real app, you would handle the form submission (e.g., API call)
console.log({ title, content });
// [수정] alert() 대신 console.log로 대체했습니다.
console.log('New post created!');
navigate('/list');
};
return (
<>
<h2>New Post</h2>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="title" className="form-label">Title</label>
<input
type="text"
className="form-control"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label htmlFor="content" className="form-label">Content</label>
<textarea
className="form-control"
id="content"
rows="5"
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
</div>
<button type="submit" className="btn btn-primary">Submit</button>
</form>
</>
);
};
export default PostForm;
{
"name": "react_board",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.12.2",
"bootstrap": "^5.3.8",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.1"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.2",
"eslint": "^9.35.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"vite": "^7.1.6"
}
}
- 11월 4일. 오후 6시 16분. - 상단 헤더도 추가했고. 조회(검색)도 잘 된다. 렌더링 된다. - 이 코드 철저히 숙달. 이해.