clone 하면 node_modules 폴더는 존재하지 않는다node_modules 는 용량이 크고 로컬에서만 사용하는 파일이기 때문에 Git 저장소에 포함시키지 않는다.gitignore
vite 로 프로젝트를 만들면 기본적으로 .gitignore 가 생성node_modules가 포함되어 있어 Git에 포함되지 않는다⚠️ node_modules가 없으면 실행 불가
최초 react project 를 clone 하면 node_modules 가 없는 상태라서 실행히 불가하다
의존성 설치
package.json 파일에 명시된 의존 dependency 가 모두 일괄 다운로드 및 설치가 된다node_modules 폴더가 자동 생성되고 그 안에 설치된다npm install
dir
특정 클래스를 추가할지 말지를 classnames 없이 구현하면?
true)이 state 값이라고 생각을 한다면state => true 면 text-dnager 가 이 위치에 출력state => false 면 '' 아무 것도 출력되지 않는다<p className={true ? 'text-danger' : ''}>xxx</p>
className={'text-danger')
or
className={''}
같은 동작을 classnames 를 이용하면?
true)이 state 값이라고 생각을 한다면state => true 면 왼쪽 object 의 key 값과 동일한 class 추가state => false 면 아무것도 추가되지 않는다import cn from 'classnames';
<p className = {cn({'text-danger':true})}>xxx</p>
checked 속성으로 체크 상태를 제어 가능const handleChange2 = (e) => {
const value = e.target.value; // checkbox 의 value 값 "bg-info" or "text-danger"
const isChecked = e.target.checked; // 체크 여부 true or false
// 상태값 변경하기
setMessageStyle({
...messageStyle,
[value]:isChecked
});
};
[value]:isChecked 의 결과 예시 4가지{"bg-info":true}
{"bg-info":false}
{"text-dnager":true}
{"text-danger":false}
my-name처럼 -이 포함된 key 는 jsx 에서 식별자로 인식될 수 없다" "로 감싸고 대괄호 표기법을 사용object 참조 방법
> {num:1, my-name:"kim"}
Uncaught SyntaxError: Unexpected token '-'
➜ my name 에서 - 가 연산자로 인식 되어서 에러 발생
> const m1 = {num:1, "my-name":"kim"}
undefined
➜ object 의 키값을 " " 로 감싸면 해결
> m1
{ num: 1, 'my-name': 'kim' }
> m1.my-name
Uncaught ReferenceError: name is not defined
➜ my name 에서 - 가 연산자로 인식 되어서 에러 발생
> m1["my-name"]
'kim'
➜ .key 값 대신에 ["key값"] 형식으로 object 에 저장된 값을 참조한다
RestClientController 생성 : 고객 목록 , 수정 , 추가할 api 생성
React 에서는 라우팅 정보 추가 등 작업
localhost:9000/clients와localhost:5173/#/clients의 페이지 기능과 UI를 동일하게 구현
jsx 페이지는 총 4개 : 목록, 신규 등록, 자세히 보기, 수정
clients 링크 추가<Nav.Item>
<Nav.Link as={NavLink} to="/clients">고객 목록</Nav.Link>
</Nav.Item>
Clients 라우트 경로 추가{path:"/clients", element:<Client/>}
@RequestMapping("/v1")
@RequiredArgsConstructor
@RestController
public class RestClientController {
private final ClientService clientService;
@GetMapping("/clients")
public List<ClientDto> list() {
return clientService.getClients();
}
@PostMapping("/clients")
public ClientDto create(@Valid @RequestBody ClientDto dto) {
// 새 고객 정보를 저장하고
Long num = clientService.addClient(dto);
// 방금 추가한 회원의 번호를 dto 에 담아서
dto.setNum(num);
// 리턴
return dto;
}
@GetMapping("/clients/{num}")
public ClientDto getClient(@PathVariable Long num) {
return clientService.getClient(num);
}
@PutMapping("/clients/{num}")
public ClientDto update(@PathVariable long num, @Valid @RequestBody ClientDto dto) {
dto.setNum(num);
clientService.update(dto);
return dto;
}
}
function NotFound() {
const navigate = useNavigate();
return<>
<h1>404</h1>
<p>페이지를 찾을 수 없습니다</p>
<p>주소가 잘못 되었거나, 페이지가 이동/삭제 되었을 수 있어요</p>
<Button as={NavLink} to="/" variant="primary">홈으로</Button>
<Button onClick={()=>navigate(-1)} variant="warning" className="ms-1">이전 페이지</Button>
</>
}
NotFound 라우트 경로 추가* 위의 라우트 경로를 제외한 모든 경로들은 해당 컴포넌트에서 처리{path:"*", element:<NotFound/>}
Client 라우트 경로 추가{path:"/clients", element:<Client/>}
function Client() {
// 고객 목록을 상태값으로 관리
const [list, setList] = useState([]);
// 컴포넌트가 활성화되는 시점에
useEffect(()=>{
// api 서버로부터 고객 목을 얻어와서
axios.get("/api/v1/clients")
.then(res=>{
// 상태값을 넣어준다
setList(res.data);
})
}, []);
return<>
<div className="d-flex justify-content-between mt-3 mb-3">
<h3>고객 목록</h3>
<NavLink to="/clients/new">신규 등록</NavLink>
</div>
<table className="table table-hover">
<thead>
<tr>
<th>번호</th>
<th>이름</th>
<th>생일</th>
<th>등록일</th>
<th>자세히</th>
</tr>
</thead>
<tbody>
{list.map(item =>
<tr key={item.num}>
<td>{item.num}</td>
<td>{item.userName}</td>
<td>{item.birthday}</td>
<td>{item.createdAt}</td>
<td>
<NavLink className="btn btn-primary" to={`/clients/${item.num}`}>보기</NavLink>
</td>
</tr>
)}
</tbody>
</table>
</>
}
@JsonFormat 어노테이션 추가dto 를 리턴하면 해당 dto 의 필드에 있는 내용이 json 문자열 로 변경되는데 LocalDataTime 에 있는 날짜 정보를@JsonFormat 어노테이션을 이용해서 원하는 형식으로 변경되도록 한다@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd일 HH:mm")
private LocalDateTime createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd일 HH:mm")
private LocalDateTime updatedAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd일")
private LocalDate birthday;
ClientDetail 라우트 경로 추가{path:"/clients/:num", element:<ClientDetail/>},
function ClientDetail() {
// location 객체에 state 라는 키값으로 담긴 내용을 읽어오기 위한 hook
const {state} = useLocation();
// "/clients/:num" 에서 num 을 읽어내기 위한 hook
const {num} = useParams();
// 회원 한 명의 정보를 state 값으로 관리
const [dto, setDto] = useState({});
useEffect(()=>{
// 이름이 없는 화살표 함수를 만들자 마자 call() 하는 구조
(async ()=>{
try{
// 서버로부터 data 가 올 때까지 기다린다
// 정상 응답이 되면
const res = await axios.get(`/api/v1/clients/${num}`);
// 회원 한 명의 정보를 상태값으로 넣어준다
setDto(res.data);
}
catch(err){
console.log(err);
}
})();
},[]);
return<>
{state && <p className="alert alert-success">{state.message}</p>}
<h3>고객 상세 정보</h3>
<table className="table table-bordered">
<tbody>
<tr>
<th>번호</th>
<td>{dto.num}</td>
</tr>
<tr>
<th>이름</th>
<td>{dto.userName}</td>
</tr>
<tr>
<th>생일</th>
<td>{dto.birthday}</td>
</tr>
<tr>
<th>수정일</th>
<td>{dto.updatedAt}</td>
</tr>
<tr>
<th>등록일</th>
<td>{dto.createdAt}</td>
</tr>
</tbody>
</table>
<div className="mt-3">
<Button variant="primary" as={NavLink} to="/clients">목록으로</Button>
<Button className="ms-1" variant="warning" as={NavLink} to={`/clients/${dto.num}/edit`}>수정</Button>
</div>
</>
}
ClientForm 라우트 경로 추가{path:"/clients/new", element:<ClientForm/>}
useLocation 추가location 객체에 state 라는 키값으로 담긴 내용을 읽어오기 위한 hookconst {state} = useLocation();
alert 에 state 상태값 추가{state && <p className="alert alert-success">{state.message}</p>}
function ClientForm() {
// route 이동을 위한 함수
const navigate = useNavigate();
// 에러를 상태값으로 관리
const [errors, setErrors] = useState({});
// 폼에 submit 이벤트가 일어났을 때 실행할 함수
const handleSubmit = (e) => {
// 폼 전송 막기
e.preventDefault();
// 이벤트가 일어난 바로 그 요소 (form) 의 참조값
const form = e.target;
const formData = new FormData(form);
// 폼에 입력한 내용을 이용해서 object 얻어내기
const obj = Object.fromEntries(formData);
// axios.post(form.action, obj)
axios[form.method](form.action, obj)
.then(res => {
// client 정보 추가 성공 시
// res.data 는 방금 추가한 고객 정보
navigate(`/clients/${res.data.num}`, {
state:{
message:"고객 정보를 추가했습니다"
}
});
})
.catch(err => {
// client 정보 추가 실패 시 (@Valid 위반)
console.log(err);
// @Valid 에러 객체를 state 에 넣어준다
setErrors(err.response.data.errors);
})
};
return<>
<h3>새 Client 등록 양식</h3>
<form action="/api/v1/clients" method="post" onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label" htmlFor="userName">userName <span className="text-danger">*</span></label>
<input className="form-control" type="text" name="userName" id="userName" placeholder="이름 입력"/>
{errors.userName && <small className="form-text text-danger">{errors.userName}</small>}
</div>
<div className="mb-3">
<label className="form-label" htmlFor="birthday">생일 (선택)</label>
<input className="form-control" type="date" name="birthday" id="birthday"/>
<small className="form-text text-muted">나중에 입력 가능</small>
{errors.birthday && <small className="form-text text-danger">{errors.birthday}</small>}
</div>
<button className="btn btn-success" type="submit">저장</button>
</form>
</>
}
ClientUodateForm 라우트 경로 추가{path:"/clients/:num/edit", element:<ClientUpdateForm/>}
function ClientUpdateForm() {
// "clients/:num:edit" 에서 num 에 전달된 값 읽어내기
const {num} = useParams();
// 수정할 고객의 정보를 상태값으로 관리
const [dto, setDto] = useState({
userName:"",
birthday:""
});
// 원본 dto 값을 상태값으로 관리
const [originalDto, setOriginalDto] = useState({});
// 필드 에러 정보를 상태값으로 관리
const [errors, setErrors] = useState({});
useEffect(()=>{
axios.get(`/api/v1/clients/${num}`)
.then(res=>{
setDto(res.data)
setOriginalDto(res.data);
});
},[]);
const handleChange = (e) => {
setDto({
...dto,
[e.target.name]:e.target.value
});
};
const navigate = useNavigate();
// form 에 submit 이벤트가 발생했을 때 실행할 함수
const handleSubmit = async (e) => {
e.preventDefault();
try {
await axios.put(e.target.action, dto);
alert("수정 완료");
navigate(`/clients/${num}`, {
state:{
message:"수정된 고객 정보 입니다"
}
})
} catch (err) {
console.log(err);
setErrors(err.response.data.errors);
}
};
return<>
<h3>고객 정보 수정 양식</h3>
<form action={`/api/v1/clients/${num}`} onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label" htmlFor="userName">userName</label>
<input className="form-control" onChange={handleChange} type="text" name="userName" id="userName"
value={dto.userName} placeholder="이름 입력"/>
{errors.userName && <small className="form-text text-danger">{errors.userName}</small>}
</div>
<div className="mb-3">
<label className="form-label" htmlFor="birthday">생일 (선택)</label>
<input className="form-control" onChange={handleChange} type="date" name="birthday" id="birthday"
value={dto.birthday}/>
<small className="form-text text-muted">나중에 입력 가능</small>
{errors.birthday && <small className="form-text text-danger">{errors.birthday}</small>}
</div>
<button className="btn btn-success" type="submit">수정</button>
<button onClick={()=>setDto(originalDto)} className="ms-1 btn btn-secondary" type="reset">취소</button>
</form>
</>
}