#문제상황

 

현재 일정 정리 웹을 개발하면서

나는 할 일에 주로 할일 속성을 부여하여

따로 날짜에 상관없이 보여주는 작업 을 하고 있었다

 

때문에 엔티티 속성을 변경해야했다.

 

 

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을 전송할때 자동을 바뀌어서 보내지게 된

+ Recent posts