#문제상황
현재 일정 정리 웹을 개발하면서
나는 할 일에 주로 할일 속성을 부여하여
따로 날짜에 상관없이 보여주는 작업 을 하고 있었다
때문에 엔티티 속성을 변경해야했다.
public class TodoEntity {
@Id
@GeneratedValue(generator="system-uuid") // 자동으로 id 생성
@GenericGenerator(name="system-uuid", strategy="uuid")
private String id;
private String userId;
private String title;
private boolean done;
private Date date; // 날짜 필드 추가
private boolean isMainTask; // 메인 할 일 여부
}
이렇게 isMainTask 속성을 추가하고
대략 아래와 같이 리액트에서 isMainTask를 구분해서 전달하는 로직으로
control={
<Checkbox
checked={this.state.item.isMainTask}
onChange={this.onCheckboxChange}
id="isMainTask"
color="primary"
/>
}
전체코드
더보기
App.js
import React from 'react';
import Todo from './Todo';
import AddTodo from './AddTodo';
import { Container, Grid, AppBar, Toolbar, Typography, Paper, List, IconButton, TextField } from "@material-ui/core";
import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import Pagination from '@mui/material/Pagination';
import { call, signout } from './service/ApiService';
import DeleteDoneAll from './DeleteDoneAll';
import Clear from './Clear';
import WeatherWidget from './WeatherWidget';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
loading: true,
date: new Date(), // 날짜 상태 추가
page: 1, // 현재 페이지
itemsPerPage: 5, // 페이지당 항목 수
};
}
add = (item) => {
call("/todo", "POST", item).then((response) =>
this.setState({ items: response.data })
);
}
delete = (item) => {
call("/todo", "DELETE", item).then((response) =>
this.setState({ items: response.data })
);
}
clearAllDonelist = () => {
const thisItems = this.state.items;
thisItems.forEach((tdl) => {
if (tdl.done === true) {
call("/todo", "DELETE", tdl).then((response) =>
this.setState({ items: response.data })
);
}
});
}
clearAll = () => {
const thisItems = this.state.items;
thisItems.forEach((tdl) => {
call("/todo", "DELETE", tdl).then((response) =>
this.setState({ items: response.data })
);
});
}
update = (item) => {
call("/todo", "PUT", item).then((response) =>
this.setState({ items: response.data })
);
}
componentDidMount() {
call("/todo", "GET", null).then((response) =>
this.setState({ items: response.data, loading: false })
);
}
handleDateChange = (date) => {
this.setState({ date });
}
handlePageChange = (event, value) => {
this.setState({ page: value });
}
render() {
const { items, date, page, itemsPerPage } = this.state;
// 메인 할 일 필터링
const mainTasks = items.filter(item => item.isMainTask);
// 선택한 날짜의 할일만 필터링
const filteredItems = items.filter(item => {
const itemDate = new Date(item.date);
return itemDate.toDateString() === date.toDateString();
});
// 페이지네이션을 위한 항목 분할
const startIndex = (page - 1) * itemsPerPage;
const paginatedItems = filteredItems.slice(startIndex, startIndex + itemsPerPage);
const todoItems = paginatedItems.length > 0 ? (
<div className="lists">
<List>
{paginatedItems.map((item, idx) => (
<Todo item={item} key={item.id} delete={this.delete} update={this.update} />
))}
</List>
</div>
) : (
<p>선택한 날짜에 할일이 없습니다.</p>
);
const navigationBar = (
<AppBar position="static" style={{ height: 60 }}>
<Toolbar style={{ minHeight: 50 }}>
<Grid justifyContent="space-between" container>
<Grid item>
<Typography variant="h6">Today quest</Typography>
</Grid>
<Grid item>
<IconButton color="inherit" onClick={signout}>로그아웃</IconButton>
</Grid>
</Grid>
</Toolbar>
</AppBar>
);
const todoListPage = (
<div>
{navigationBar}
<Container maxWidth="md">
<WeatherWidget />
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
label="Select Date"
value={this.state.date}
onChange={this.handleDateChange}
renderInput={(params) => <TextField {...params} fullWidth />}
/>
</LocalizationProvider>
<AddTodo add={this.add} />
<div className="TodoList">
{/* 메인 할 일을 항상 표시 */}
<Paper style={{ margin: 16 }}>
<Typography variant="h6" style={{ margin: 16 }}>Main Tasks</Typography>
<List>
{mainTasks.map((item, idx) => (
<Todo item={item} key={item.id} delete={this.delete} update={this.update} />
))}
</List>
</Paper>
<Paper style={{ margin: 16 }}>
<Typography variant="h6" style={{ margin: 16 }}>Tasks for {date.toDateString()}</Typography>
{todoItems}
<Pagination
count={Math.ceil(filteredItems.length / itemsPerPage)}
page={page}
onChange={this.handlePageChange}
color="primary"
style={{ display: 'flex', justifyContent: 'center', marginTop: 16 }}
/>
</Paper>
</div>
</Container>
<DeleteDoneAll clearAllDonelist={this.clearAllDonelist} />
<Clear clearAll={this.clearAll} />
</div>
);
const loadingPage = <h1>Loading...</h1>
const content = this.state.loading ? loadingPage : todoListPage;
return (
<div className="App">
{content}
</div>
);
}
}
export default App;
AddTodo.js
import React from "react";
import { TextField, Paper, Button, Grid, Checkbox, FormControlLabel } from "@material-ui/core";
import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
class AddTodo extends React.Component {
constructor(props) {
super(props);
this.state = {
item: { title: "", isMainTask: false, date: new Date() } // 메인 할 일 여부 및 날짜 추가
};
this.add = props.add; // props 의 함수를 this.add 에 연결, props에는 상위 컴포넌트(App.js)의 함수, 매개변수가 들어 있음.
}
onInputChange = (e) => {
const thisItem = { ...this.state.item, title: e.target.value };
console.log("Updated title:", thisItem.title); // 로그 추가
this.setState({ item: thisItem });
}
onCheckboxChange = (e) => {
const thisItem = { ...this.state.item, isMainTask: e.target.checked };
console.log("Updated isMainTask:", thisItem.isMainTask); // 로그 추가
this.setState({ item: thisItem });
}
onDateChange = (date) => {
const thisItem = { ...this.state.item, date: date };
console.log("Updated date:", thisItem.date); // 로그 추가
this.setState({ item: thisItem });
}
onButtonClick = () => {
console.log("Todo item being added:", this.state.item); // 로그 추가
this.add(this.state.item);
this.setState({ item: { title: "", isMainTask: false, date: new Date() } }); // text 값을 추가하고 입력 필드는 초기화시킨다.
}
enterKeyEventHandler = (e) => {
if (e.key === 'Enter') {
this.onButtonClick();
}
}
render() {
return (
<Paper style={{ margin: 16, padding: 16 }}>
<Grid container alignItems="center" spacing={2}>
<Grid item xs={3} md={3}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
label="Select Date"
value={this.state.item.date}
onChange={this.onDateChange}
renderInput={(params) => <TextField {...params} fullWidth />}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={6} md={6}>
<TextField
placeholder="Add Todo here"
fullWidth
onChange={this.onInputChange}
value={this.state.item.title}
onKeyPress={this.enterKeyEventHandler}
/>
</Grid>
<Grid item xs={2} md={2}>
<FormControlLabel
control={
<Checkbox
checked={this.state.item.isMainTask}
onChange={this.onCheckboxChange}
id="isMainTask"
color="primary"
/>
}
label="Main"
/>
</Grid>
<Grid item xs={1} md={1}>
<Button
fullWidth
color="secondary"
variant="outlined"
onClick={this.onButtonClick}
>
+
</Button>
</Grid>
</Grid>
</Paper>
);
}
}
export default AddTodo;
이와같이 MainTask를 분리하는 작업을 하였는데
어째선지 제대로 뜨지 않는것이다....
로직에는 이상이 없었기 때문에
아무리 살펴봐도 문제를 찾을 수 없었는데
H2 콘솔을 보니
백엔드에서는 IS_MAIN_TASK 로 들어가 있는 것이다.
프론트엔드와 백엔드의 필드명이 일치하지 않아서 생기는 문제였다.
#발생원인
- 백엔드와 프론트엔드의 명명 규칙 차이: Java에서는 보통 카멜 케이스(camelCase)를 사용하고, 데이터베이스에서는 대문자와 언더스코어를 사용
- 자동 생성된 필드명: ORM(Object-Relational Mapping) 도구나 JSON 라이브러리가 자동으로 필드명을 생성할 때, 일관되지 않은 명명 규칙을 따르는 경우가 있습니다.
설명: JSON 을 전달할때 백엔드는 카멜 케이스를 사용했기 때문에
isMainTask를 IS_MAIN_TASK로 전달했기 때문에 이런 문제가 생긴 것이다.
JSON 전달을 카멜 케이스를 사용하지 않은 이름으로 전달 해주면 된다.
#문제 해결
package com.example.todo.dto;
import com.example.todo.model.TodoEntity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
private String id;
private String title;
private boolean done;
private Date date; // 날짜 필드 추가
@JsonProperty("isMainTask") // JSON 필드명 설정
private boolean isMainTask;
public TodoDTO(final TodoEntity entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.done = entity.isDone();
this.date = entity.getDate(); // 날짜 필드 추가
this.isMainTask = entity.isMainTask(); // 메인 할 일 여부
}
public static TodoEntity toEntity(final TodoDTO dto) {
return TodoEntity.builder()
.id(dto.getId())
.title(dto.getTitle())
.done(dto.isDone())
.date(dto.getDate()) // 날짜 필드 추가
.isMainTask(dto.isMainTask()) // 메인 할 일 여부
.build();
}
}
이와같이 @JsonProperty 를 사용해서 JSON 필드명과 엔티티 필드명을 일치시켜주면 문제는 해결된다.
@PostMapping("/todo")
public ResponseEntity<?> createTodo(@RequestBody TodoDTO dto) {
try {
TodoEntity entity = TodoDTO.toEntity(dto);
List<TodoEntity> entities = todoService.create(entity);
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
return ResponseEntity.ok().body(dtos);
} catch (Exception e) {
return ResponseEntity.badRequest().body("Error: " + e.getMessage());
}
}
여기서 json을 전송할때 자동을 바뀌어서 보내지게 된