๐Ÿ’ป ์ฝ”๋”ฉ ์ผ๊ธฐ : [์Šคํ”„๋ง ๊ฒŒ์‹œํŒ with React] '๊ฒŒ์‹œํŒ ๋งŒ๋“ค๊ธฐ' ํŽธ

ybkยท2024๋…„ 5์›” 21์ผ

spring

๋ชฉ๋ก ๋ณด๊ธฐ
36/55
post-thumbnail

๐Ÿ””'๊ฒŒ์‹œํŒ ๋งŒ๋“ค๊ธฐ'์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž!


๐Ÿ’Ÿ ๊ฒŒ์‹œ๋ฌผ CREATE (๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ)

BoardWrite.jsx(React)

export function BoardWrite() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [writer, setWriter] = useState("");

  function handleSaveClick() {
    axios.post("/api/board/add", {
      title,
      content,
      writer,
    });
  }

  return (
    <Box>
      <Box>๊ธ€ ์ž‘์„ฑ ํ™”๋ฉด</Box>
      <Box>
        <Box>
          <FormControl>
            <FormLabel>์ œ๋ชฉ</FormLabel>
            <Input onChange={(e) => setTitle(e.target.value)} />
          </FormControl>
        </Box>
        <Box>
          <FormControl>
            <FormLabel>๋ณธ๋ฌธ</FormLabel>
            <Textarea onChange={(e) => setContent(e.target.value)} />
          </FormControl>
        </Box>
        <Box>
          <FormControl>
            <FormLabel>์ž‘์„ฑ์ž</FormLabel>
            <Input onChange={(e) => setWriter(e.target.value)} />
          </FormControl>
        </Box>
        <Box>
          <Button colorScheme={"blue"} onClick={handleSaveClick}>
            ์ €์žฅ
          </Button>
        </Box>
      </Box>
    </Box>
  );
}
  • ์ œ๋ชฉ, ๋ณธ๋ฌธ, ์ž‘์„ฑ์ž ๊ฐ’์„ ๋ฐ›์•„์„œ ์ €์žฅ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด POST ์š”์ฒญ์œผ๋กœ ์„œ๋ฒ„์—๊ฒŒ title, content, writer ๊ฐ’์„ ๋„˜๊น๋‹ˆ๋‹ค.
  • ์„œ๋ฒ„์—๊ฒŒ title, content, writer ๊ฐ’์„ ๋„˜๊ฒจ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒํƒœ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

Board.java

@Data
public class Board {
    private Integer id;
    private String title;
    private String content;
    private String writer;
    private LocalDateTime inserted;
}
  • ๊ฒŒ์‹œ๋ฌผ์—์„œ ์‚ฌ์šฉํ•  ํ”„๋กœํผํ‹ฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

BoardController.java

@RestController
@RequestMapping("/api/board")
@RequiredArgsConstructor
public class BoardController {

    private final BoardService service;

    @PostMapping("add")
    public void add(@RequestBody Board board) {
        service.add(board);
    }
}
  • /api/board/add ๊ฒฝ๋กœ๋กœ ๋“ค์–ด์˜ค๋Š” POST ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • @RequestBody ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ JSON ํ˜•์‹์˜ ์š”์ฒญ ๋ณธ๋ฌธ์„ ์ž๋™์œผ๋กœ Board ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ Board ๊ฐ์ฒด๋ฅผ service.add(board)๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ BoardService๋กœ ์ „๋‹ฌํ•˜๊ณ  ๊ฒŒ์‹œ๋ฌผ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class BoardService {
    private final BoardMapper mapper;

    public void add(Board board) {
        mapper.insert(board);
    }
}
  • Mapper์—์„œ ์ž‘์„ฑํ•œ ์ฟผ๋ฆฌ๋ฌธ์— ๋งž๊ฒŒ ๊ฒŒ์‹œ๋ฌผ์„ ์ €์žฅํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Mapper
public interface BoardMapper {
    @Insert("""
            INSERT INTO board (title, content, writer)
            VALUES (#{title}, #{content}, #{writer})
            """)
    int insert(Board board);
}
  • Board๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด board ํ…Œ์ด๋ธ”์— ์‚ฝ์ž…ํ•˜๋„๋ก ์ฟผ๋ฆฌ๋ฌธ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. (@Insert)

๐ŸŸฆ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์— ๋Œ€ํ•œ ๋ถ€๊ฐ€์ ์ธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

๐Ÿ“ข ๋งŒ์•ฝ ๊ฒŒ์‹œ๊ธ€์ด ์ž˜ ์ž‘์„ฑ๋˜์—ˆ๋‹ค๋ฉด 200 ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๊ฒŒ์‹œ๊ธ€์— ์ œ๋ชฉ, ๋ณธ๋ฌธ, ์ž‘์„ฑ์ž๊ฐ€ ๋น„์–ด์žˆ๋‹ค๋ฉด 400 ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
๐Ÿ“ข Service์—์„œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ•˜๊ณ  ๊ทธ๊ฒƒ์„ Controller์—๊ฒŒ ๋„˜๊ฒจ์ค˜ ํ•ด๋‹น ์‘๋‹ต ์ฝ”๋“œ์— ๋งž๊ฒŒ ํ™”๋ฉด์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.


BoardController.java

@RestController
@RequestMapping("/api/board")
@RequiredArgsConstructor
public class BoardController {

    private final BoardService service;

    @PostMapping("add")
    public ResponseEntity add(@RequestBody Board board) {
        if (service.validate(board)) {
            service.add(board);
            return ResponseEntity.ok().build();
        } else {
            return ResponseEntity.badRequest().build();
        }
    }
}
  • BoardController๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ๊ฒŒ์‹œ๊ธ€์„ BoardService๋ฅผ ํ†ตํ•ด ๋“ฑ๋กํ•˜๊ณ , ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ†ตํ•ด ์ ์ ˆํ•œ HTTP ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋งŒ์•ฝ BoardService์— ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ true๋ผ๋ฉด ๊ฒŒ์‹œ๊ธ€์— ๋“ฑ๋ก์ด ์ž˜ ๋˜์–ด 200๋ฒˆ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด 400๋ฒˆ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class BoardService {
    private final BoardMapper mapper;

    ~~~~~~

    // ์ œ๋ชฉ, ๋‚ด์šฉ, ์ž‘์„ฑ์ž ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ(์ž‘์„ฑ๋˜์—ˆ๋Š”์ง€)
    public boolean validate(Board board) {

        if (board.getTitle() == null || board.getTitle().isBlank()) {
            return false;
        }

        if (board.getContent() == null || board.getContent().isBlank()) {
            return false;
        }

        if (board.getWriter() == null || board.getWriter().isBlank()) {
            return false;
        }

        return true;
    }
}
  • Board ๊ฐ์ฒด๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„ ์ œ๋ชฉ, ๋ณธ๋ฌธ, ์ž‘์„ฑ์ž ์ž…๋ ฅ ๊ฐ’์ด null์ด๊ฑฐ๋‚˜ ๋น„์–ด ์žˆ๋‹ค๋ฉด ์œ ํšจํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— false ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

BoardWrite.jsx(React)

export function BoardWrite() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [writer, setWriter] = useState("");
  const toast = useToast();
  const navigate = useNavigate();
  const [loading, setLoading] = useState(false);

  function handleSaveClick() {
    setLoading(true);
    axios
      .post("/api/board/add", {
        title,
        content,
        writer,
      })
      .then(() => {
        toast({
          description: "์ƒˆ ๊ธ€์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
          status: "success",
          position: "top-right",
          duration: 1000,
        });
        navigate("/");
      })
      .catch((error) => {
        const code = error.response.status;

        if (code === 400) {
          toast({
            status: "error",
            description: "๋“ฑ๋ก๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž…๋ ฅํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.",
            position: "top-right",
            duration: 1000,
          });
        }
      })
      .finally(() => setLoading(false));
  }

  // ์ œ๋ชฉ, ๋ณธ๋ฌธ, ์ž‘์„ฑ์ž ์ž‘์„ฑํ•˜์ง€ ์•Š์œผ๋ฉด ํ™œ์„ฑํ™”๊ฐ€ ๋˜์ง€ ์•Š๋Š”๋‹ค.
  let disableSaveButton = false;
  if (title.trim().length === 0) {
    disableSaveButton = true;
  }
  if (content.trim().length === 0) {
    disableSaveButton = true;
  }
  if (writer.trim().length === 0) {
    disableSaveButton = true;
  }

  return (
    <Box>
		~~~~      
          <Button
            isLoading={loading}
            isDisabled={disableSaveButton}
            colorScheme={"blue"}
            onClick={handleSaveClick}
          >
            ์ €์žฅ
          </Button>
		~~~~
    </Box>
  );
}
  • ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ, axios.post("/api/board/add") ๊ฒฝ๋กœ๋กœ ์š”์ฒญ ๋ณธ๋ฌธ์— title, content, writer ๊ฐ’์„ ๋‹ด๊ณ  ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.
  • ์š”์ฒญ ์„ฑ๊ณต(.then()) : "์ƒˆ ๊ธ€์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค." ๋ฉ”์‹œ์ง€๋ฅผ ํ† ์ŠคํŠธ๋กœ ํ‘œ์‹œํ•˜๊ณ  ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™(navigate("/"))ํ•ฉ๋‹ˆ๋‹ค.
  • ์š”์ฒญ ์‹คํŒจ(.catch()) : 400 ์ƒํƒœ ์ฝ”๋“œ์— ๋”ฐ๋ผ "๋“ฑ๋ก๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž…๋ ฅํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”." ๋ฉ”์‹œ์ง€๋ฅผ ํ† ์ŠคํŠธ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
  • disableSaveButton() : ๋งŒ์•ฝ์— title, content, writer ์ž…๋ ฅ ๊ฐ’์ด ๋น„์–ด์žˆ๋‹ค๋ฉด ์ €์žฅ ๋ฒ„ํŠผ์„ ๋น„ํ™œ์„ฑ(true)ํ•ฉ๋‹ˆ๋‹ค.
  • loading : ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋กœ๋”ฉ์ค‘์œผ๋กœ ๋ณ€ํ™”๋˜๋ฉฐ post ์š”์ฒญ ๋ณด๋‚ด๊ธฐ ์ „์— true๋กœ ์„ค์ •ํ•˜๊ณ  ๋ณด๋‚ธ ํ›„์—๋Š” ์š”์ฒญ ์„ฑ๊ณต, ์‹คํŒจ์— ๋”ฐ๋ฅด์ง€ ์•Š๊ณ  ๋ฌด์กฐ๊ฑด ๋กœ๋”ฉ์ด ๋๋‚˜์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— false๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ’Ÿ ๊ฒŒ์‹œ๋ฌผ READ(๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ)


๐ŸŸฆ ๊ฒŒ์‹œ๊ธ€ ์ „์ฒด ๋ณด์—ฌ์ฃผ๊ธฐ(Home)

BoardList.jsx(React)

export function BoardList() {
  const navigate = useNavigate();
  const [boardList, setBoardList] = useState([]);

  // const boardList = [
  //   { id: 5, title: "title1", writer: "who1" },
  // ];

  useEffect(() => {
    axios.get("/api/board/list").then((res) => setBoardList(res.data));
  }, []);

  return (
    <Box>
      <Box>๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก</Box>
      <Box>
        <Table>
          <Thead>
            <Tr>
              <Th>NO</Th>
              <Th>์ œ๋ชฉ</Th>
              <Th>
                <FontAwesomeIcon icon={faUserPen} />
              </Th>
            </Tr>
          </Thead>
          <Tbody>
            {boardList.map((board) => (
              <Tr
                cursor={"pointer"}
                _hover={{ bgColor: "gray.200" }}
                onClick={() => navigate(`/board/${board.id}`)}
                key={board.id}
              >
                <Td>{board.id}</Td>
                <Td>{board.title}</Td>
                <Td>{board.writer}</Td>
              </Tr>
            ))}
          </Tbody>
        </Table>
      </Box>
    </Box>
  );
}
  • axios.get(/api/board/list) ํ˜ธ์ถœ์—์„œ ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ setBoard(res.data)๋ฅผ ํ†ตํ•ด board ์ƒํƒœ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • boardList.map((board) => () : boardList๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ ๊ฐ board ๊ฐ์ฒด์— ๋Œ€ํ•œ ์†์„ฑ(id, title, writer)๋“ค์˜ ๊ฐ’์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  • useEffect() : axios๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ GET ์š”์ฒญ์œผ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๊ณ , ๊ทธ ๋ชฉ๋ก์„ boardList ์ƒํƒœ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

BoardController.java

@GetMapping("list")
public List<Board> list() {
    return service.list();
}
  • /api/board/list๋กœ ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด BoardService์˜ list() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

public List<Board> list() {
    return mapper.selectAll();
}
  • mapper์—์„œ ์กฐํšŒ๋œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Select("SELECT id, title, writer FROM board ORDER BY id DESC")
List<Board> selectAll();
  • board ํ…Œ์ด๋ธ”์— ์žˆ๋Š” ์ „์ฒด ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.


๐ŸŸฆ ๊ฒŒ์‹œ๋ฌผ ํด๋ฆญ ์‹œ ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ ๋ณด๊ธฐ

BoardView.jsx(React)

export function BoardView() {
  const { id } = useParams();
  const [board, setBoard] = useState(null);
  const toast = useToast();
  const navigate = useNavigate();

  useEffect(() => {
    axios
      .get(`/api/board/${id}`)
      .then((res) => setBoard(res.data))
      .catch((err) => {
        if (err.response.status === 404) {
          toast({
            status: "info",
            description: "ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
            position: "top-right",
          });
          navigate("/");
        }
      });
  }, []);

  if (board === null) {
    return <Spinner />;
  }

  return (
    <Box>
      <Box>{board.id}๋ฒˆ ๊ฒŒ์‹œ๋ฌผ</Box>
      <Box>
        <FormControl>
          <FormLabel>์ œ๋ชฉ</FormLabel>
          <Input value={board.title} readOnly />
        </FormControl>
        <Box>
          <FormControl>
            <FormLabel>๋ณธ๋ฌธ</FormLabel>
            <Input value={board.content} readOnly />
          </FormControl>
        </Box>
        <Box>
          <FormControl>
            <FormLabel>์ž‘์„ฑ์ž</FormLabel>
            <Input value={board.writer} readOnly />
          </FormControl>
        </Box>
        <Box>
          <FormControl>
            <FormLabel>์ž‘์„ฑ์ผ์‹œ</FormLabel>
            <Input type={"datetime-local"} value={board.inserted} readOnly />
          </FormControl>
        </Box>
      </Box>
    </Box>
  );
}
  • axios๋กœ /api/board/${id} ๊ฒฝ๋กœ๋กœ get ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์š”์ฒญ์— ์„ฑ๊ณตํ•˜๋ฉด board์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ(title, content, writer)์„ board ์ƒํƒœ์— ์ €์žฅํ•˜๊ณ  ์š”์ฒญ์— ์‹คํŒจํ•˜๋ฉด ์‹คํŒจ ์ฝ”๋“œ๊ฐ€ 404๋ผ๋ฉด ํ† ์ŠคํŠธ๋กœ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ณ  home('/')์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  • board๊ฐ€ null ์ด๋ผ๋ฉด Spinner ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค๋‹ˆ๋‹ค.
  • const { id } = useParams(): URL์˜ ๊ฒฝ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ id ๊ฐ’์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
  • board ๊ฐ์ฒด๋Š” axios.get(/api/board/${id}) ํ˜ธ์ถœ์—์„œ ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ setBoard(res.data)๋ฅผ ํ†ตํ•ด board ์ƒํƒœ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

BoardController.java

// /api/board/5
@GetMapping("{id}")
public ResponseEntity get(@PathVariable Integer id) {
    Board board = service.get(id);

    if (board == null) {
        return ResponseEntity.notFound().build();
    }
    
    return ResponseEntity.ok().body(board);
}
  • /api/board/id๋กœ GET ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด BoardService์˜ list() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋งŒ์•ฝ ์š”์ฒญ๋œ ๊ฒŒ์‹œ๋ฌผ์ด ์กด์žฌํ•˜๋ฉด ๋ฐ˜ํ™˜ํ•˜๊ณ  ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด 404 Not Found ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • @PathVariable : URL ๊ฒฝ๋กœ์—์„œ {id} ๋ถ€๋ถ„์„ ์ถ”์ถœํ•˜์—ฌ ๋ฉ”์„œ๋“œ ๋งค๊ฐœ๋ณ€์ˆ˜ id์— ๋ฐ”์ธ๋”ฉํ•ฉ๋‹ˆ๋‹ค.
  • ResponseEntity๋Š” HTTP ์‘๋‹ต์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ์ฒด๋กœ, ์ƒํƒœ ์ฝ”๋“œ์™€ ์‘๋‹ต ๋ณธ๋ฌธ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

BoardService.java

public Board get(Integer id) {
    return mapper.selectById(id);
}
  • mapper์—์„œ ์กฐํšŒํ•œ id์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๋ฌผ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Select("SELECT * FROM board WHERE id = #{id}")
Board selectById(Integer id);
  • ํ•ด๋‹น id์— ํ•ด๋‹น ํ•˜๋Š” ๊ฒŒ์‹œ๋ฌผ๋งŒ ์กฐํšŒํ•˜๋„๋ก ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ’Ÿ ๊ฒŒ์‹œ๋ฌผ Delete(๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ)

BoardView.jsx(React)

export function BoardView() {
  const { id } = useParams();
  const [board, setBoard] = useState({});
  const toast = useToast();
  const navigate = useNavigate();
  const { isOpen, onClose, onOpen } = useDisclosure();

	~~~

  function handleClickRemove() {
    axios
      .delete(`/api/board/${id}`)
      .then(() => {
        toast({
          status: "success",
          description: `${id}๋ฒˆ ๊ฒŒ์‹œ๋ฌผ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`,
          position: "top-right",
        });
        navigate("/");
      })
      .catch(() => {
        toast({
          status: "error",
          description: `${id}๋ฒˆ ๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.`,
          position: "top-right",
        });
      })
      .finally(() => {
        onClose();
      });
  }

  return (
    <Box>
 	~~~~~
        <Box>
          <Button colorScheme={"purple"}>์ˆ˜์ •</Button>
          <Button colorScheme={"red"} onClick={onOpen}>
            ์‚ญ์ œ
          </Button>
          <Modal isOpen={isOpen} onClose={onClose}>
            <ModalOverlay />
            <ModalContent>
              <ModalHeader></ModalHeader>
              <ModalBody>์ •๋ง๋กœ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?</ModalBody>
              <ModalFooter>
                <Button onClick={onClose}>์ทจ์†Œ</Button>
                <Button colorScheme={"red"} onClick={handleClickRemove}>
                  ํ™•์ธ
                </Button>
              </ModalFooter>
            </ModalContent>
          </Modal>
        </Box>
    </Box>
  );
}
  • ์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ์ด ์˜คํ”ˆ๋˜๊ณ  ๋ชจ๋‹ฌ์—์„œ ์ทจ์†Œ ๋ฒ„ํŠผ๊ณผ ํ•™์ธ ๋ฒ„ํŠผ์ด ๋ณด์ด๋ฉฐ ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ handleClickRemove ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • handleClickRemove() : axios๋กœ /api/board/${id} ๊ฒฝ๋กœ๋กœ delete ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์š”์ฒญ ์„ฑ๊ณตํ•˜๋ฉด ํ† ์ŠคํŠธ ํ‘œ์‹œ์™€ home(/)์œผ๋กœ ๊ฒฝ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

BoardController.java

@DeleteMapping("{id}")
public void delete(@PathVariable Integer id) {
    service.remove(id);
}
  • @PathVariable : URL ๊ฒฝ๋กœ์—์„œ {id} ๋ถ€๋ถ„์„ ์ถ”์ถœํ•˜์—ฌ ๋ฉ”์„œ๋“œ ๋งค๊ฐœ๋ณ€์ˆ˜ id์— ๋ฐ”์ธ๋”ฉํ•ฉ๋‹ˆ๋‹ค.
  • BoardService์—์„œ ํ•ด๋‹น id ๊ฒŒ์‹œ๊ธ€์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
  • /api/board/id๋กœ Delete ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด BoardService์˜ remove() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

public void remove(Integer id) {
    mapper.deleteById(id);
}
  • mapper์—์„œ id์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Delete("DELETE FROM board WHERE id=#{id}")
int deleteById(Integer id);
  • ํ•ด๋‹น id์˜ ๊ฒŒ์‹œ๊ธ€์„ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด ์ฟผ๋ฆฌ๋ฌธ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.



๐Ÿ’Ÿ ๊ฒŒ์‹œ๋ฌผ Update(๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •)


BoardView.jsx(React)

<Button
  colorScheme={"purple"}
  onClick={() => navigate(`/edit/${board.id}`)}
>
  ์ˆ˜์ •
</Button>
  • ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ•ด๋‹น ์•„์ด๋””์˜ ์ˆ˜์ • ๊ฒŒ์‹œ๋ฌผ(/edit/${board.id} ๊ฒฝ๋กœ)๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

BoardEdit.jsx(React)

export function BoardEdit() {
  const { id } = useParams();
  const [board, setBoard] = useState(null);
  const toast = useToast();
  const navigate = useNavigate();
  const { isOpen, onClose, onOpen } = useDisclosure();

  useEffect(() => {
    axios.get(`/api/board/${id}`).then((res) => setBoard(res.data));
  }, []);

  function handleClickSave() {
    axios
      .put("/api/board/edit", board)
      .then(() => {
        toast({
          status: "success",
          description: `${board.id}๋ฒˆ ๊ฒŒ์‹œ๋ฌผ์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`,
          position: "top-right",
        });
        navigate(`/board/${board.id}`);
      })
      .catch((err) => {
        if (err.response.status === 400) {
          toast({
            status: "error",
            description: `๊ฒŒ์‹œ๋ฌผ์ด ์ˆ˜์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž‘์„ฑํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.`,
            position: "top-right",
          });
        }
      })
      .finally(() => {
        onClose();
      });
  }

  if (board === null) {
    return <Spinner />;
  }

  return (
    <Box>
      <Box>{board.id}๋ฒˆ ๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ •</Box>
      <Box>
        <Box>
          <FormControl>
            <FormLabel>์ œ๋ชฉ</FormLabel>
            <Input
              defaultValue={board.title}
              onChange={(e) => setBoard({ ...board, title: e.target.value })}
            />
          </FormControl>
        </Box>
        <Box>
          <FormControl>
            <FormLabel>๋ณธ๋ฌธ</FormLabel>
            <Textarea
              defaultValue={board.content}
              onChange={(e) => setBoard({ ...board, content: e.target.value })}
            ></Textarea>
          </FormControl>
        </Box>
        <Box>
          <FormControl>
            <FormLabel>์ž‘์„ฑ์ž</FormLabel>
            <Input
              defaultValue={board.writer}
              onChange={(e) => setBoard({ ...board, writer: e.target.value })}
            />
          </FormControl>
        </Box>
        <Box>
          <Button colorScheme={"green"} onClick={onOpen}>
            ์ €์žฅ
          </Button>
        </Box>
      </Box>
      <Modal isOpen={isOpen} onClose={onClose}>
        <ModalOverlay />
        <ModalContent>
          <ModalHeader></ModalHeader>
          <ModalBody>์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?</ModalBody>
          <ModalFooter>
            <Button onClick={onClose}>์ทจ์†Œ</Button>
            <Button onClick={handleClickSave} colorScheme={"blue"}>
              ํ™•์ธ
            </Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </Box>
  );
}
  • axios.get(/api/board/${id}) ํ˜ธ์ถœ์—์„œ ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ setBoard(res.data)๋ฅผ ํ†ตํ•ด board ์ƒํƒœ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. board์— ์ €์žฅ๋œ ๊ฐ’์„ ์ˆ˜์ • ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐ”์„ ๋•Œ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ defaultValue๋กœ ์ €์žฅํ•ด์ค๋‹ˆ๋‹ค.
  • onChange={(e) => setBoard({ ...board, ํ”„๋กœํผํ‹ฐ: e.target.value })} : <Input/> ํƒœ๊ทธ์˜ ์ž…๋ ฅ ๊ฐ’์— ๊ธ€์„ ์ž…๋ ฅํ•œ๋‹ค๋ฉด setBoard() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ board ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ์ด์ „ board ์ƒํƒœ๋ฅผ ๋ณต์‚ฌํ•˜๊ณ , ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ํ”„๋กœํผํ‹ฐ(title, content, writer)๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. e.target.value๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ƒˆ๋กœ์šด ํ”„๋กœํผํ‹ฐ ๊ฐ’์ž…๋‹ˆ๋‹ค.
  • ๊ธ€ ์ˆ˜์ • ํ›„ ์ €์žฅ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋ชจ๋‹ฌ์ด ๋‚˜ํƒ€๋‚˜๊ณ  ๋ชจ๋‹ฌ์—์„œ ํ™•์ธ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด handleClickSave ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • handleClickSave ๋ฉ”์„œ๋“œ : board ๊ฐ์ฒด๋ฅผ ๋‹ด์•„ ์„œ๋ฒ„์—๊ฒŒ axios.put(/api/board/edit) ์š”์ฒญ์„ ํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ ์„ฑ๊ณต ์‹œ, toast๋กœ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ณ  ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ(/board/${board.id} ๊ฒฝ๋กœ)๋กœ ์ด๋™ํ•˜๊ณ  ์š”์ฒญ ์‹คํŒจ ์‹œ, ์‘๋‹ต ์ฝ”๋“œ๊ฐ€ 400์ด๋ผ๋ฉด toast๋กœ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

BoardController.java

@PutMapping("edit")
    public ResponseEntity edit(@RequestBody Board board) {
        if (service.validate(board)) {
            service.edit(board);
            return ResponseEntity.ok().build();
        } else {
            return ResponseEntity.badRequest().build();
        }
    }
}
  • /api/board/edit๋กœ ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด BoardService์—์„œ ํ•ด๋‹น id ๊ฒŒ์‹œ๊ธ€์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ•˜์—ฌ ํ•ด๋‹น ํ•˜๋Š” ์‘๋‹ต ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

BoardService.java

public void edit(Board board) {
    mapper.update(board);
}
  • mapper์—์„œ id์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€์„ ์ˆ˜์ •ํ•œ ๊ฒƒ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

BoardMapper.java

@Update("""
        UPDATE board SET title=#{title}, content=#{content}, writer=#{writer}
        WHERE id=#{id}
        """)
int update(Board board);
  • ํ•ด๋‹น id ๊ฒŒ์‹œ๊ธ€์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฟผ๋ฆฌ๋ฌธ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.



profile
๊ฐœ๋ฐœ์ž ์ค€๋น„์ƒ~

0๊ฐœ์˜ ๋Œ“๊ธ€