rest-api 관련 실습 진행 (blog 예제)
•
react
◦
링크 테이블 추가 후 코드 내용
import { Box, Button, FormControl, FormLabel, Grid2, Paper, Stack, styled, Switch, TextField, Typography } from "@mui/material";
import { useForm } from "react-hook-form";
import { favAPI } from "../../api/services/favorite";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
const FavForm = () => {
    // const { favId } = useParams();
    const { state } = useLocation();
    const navigate = useNavigate();
    const { register, formState: { errors }, handleSubmit, setValue,  } = useForm();
    const [uploadFile, setUploadFile] = useState();
    const imgRef = useRef();
    const uploadFilePreview = () => {
        const reader = new FileReader();
        reader.readAsDataURL(imgRef.current.files[0]);
        reader.onloadend = () => {
            setUploadFile(reader.result);
        };
    };
    useEffect(() => {
        if (state) {
            state.image && setShowFileInput(false);
            setValue("title", state.title);
            setValue("url", state.url);
        }
    }, []);
    const [showFileInput, setShowFileInput] = useState(true);
    const toggleFileInput = () => {
        setShowFileInput(prev => !prev);
    }
    const onSubmit = async (data) => {
        try {
            const formData = new FormData();
            Object.keys(data).forEach(key => {
                formData.append(key, data[key]);
            });
            if (data.logo.length) {
                formData.append("image", data.logo[0]);
            } else {
                formData.delete("image");
                if (showFileInput) {
                    formData.append("deleteImage", true);
                }
            }
            if (state) {
                formData.append("id", state.id);
                const res = await favAPI.modifyFav(formData);
            } else {
                const res = await favAPI.writeFav(formData);
            }
            navigate("/favorite")
        } catch (error) {
            console.error(error);
        }
    }
    return (
        <Paper
            elevation={3}
            sx={{
                padding: 4,
                maxWidth: 600,
                margin: "auto",
                marginTop: 4,
            }}
            >
         <Box component="form" onSubmit={handleSubmit(onSubmit)} noValidate>
            <Grid2 container spacing={3}>
                <FormControl fullWidth>
                    <FormLabel htmlFor="title">사이트 이름</FormLabel>
                    <TextField
                        autoComplete="title"
                        {...register("title", { required: true })}
                        fullWidth
                        id="title"
                        placeholder="구글"
                        error={errors.title ? true : false}
                        helperText={errors.title && "사이트 이름은 필수값입니다."}
                    />
                </FormControl>
                <FormControl fullWidth>
                    <FormLabel htmlFor="url">URL</FormLabel>
                    <TextField
                        id="url"
                        fullWidth
                        {...register("url", { 
                            required: true,
                            pattern: /((?:https\:\/\/)|(?:http\:\/\/)|(?:www\.))?([a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?:\??)[a-zA-Z0-9\-\._\?\,\'\/\\\+&%\$#\=~]+)/
                        })}
                        autoComplete="url"
                        placeholder="www.google.com"
                        variant="outlined"
                        error={errors.url ? true : false}
                        helperText={errors.url && "주소 규격에 맞지 않습니다."}
                    />
                </FormControl>
                <FormControl fullWidth>
                    <FormLabel component="legend">로고 이미지</FormLabel>
                    {state?.image && (
                        <Stack
                        direction="row"
                        spacing={1}
                        sx={{ alignItems: "center", marginBottom: 1 }}
                        >
                        <Typography>변경</Typography>
                        <AntSwitch
                            defaultChecked
                            onClick={toggleFileInput}
                        />
                        <Typography>유지</Typography>
                        </Stack>
                    )}
                    {showFileInput &&
                        <>
                            <TextField
                                type="file"
                                {...register("logo")}
                                slotProps={{ htmlInput : {"accept": "image/*"}}}
                                variant="outlined"
                                inputRef={imgRef}
                                onChange={uploadFilePreview}
                            />
                        </>
                    }
                    { (!showFileInput || uploadFile) && 
                        <Box sx={{ textAlign: "center", marginTop: 2 }}>
                            <img
                                src={uploadFile ? uploadFile : `${process.env.REACT_APP_SERVER}/img/${state.image.saved}`}
                                alt="Current Logo"
                                style={{ maxWidth: "100%", maxHeight: 200 }}
                            />
                        </Box>
                    }
                </FormControl>
                <Button
                    type="submit"
                    fullWidth
                    variant="contained"
                >
                    {state ? "수정하기" : "등록하기"}
                </Button>
            </Grid2>   
        </Box>
        </Paper>
    );
}
 
const AntSwitch = styled(Switch)(({ theme }) => ({
    width: 36,
    height: 20,
    padding: 0,
    display: "flex",
    "&:active": {
      "& .MuiSwitch-thumb": {
        width: 15,
      },
      "& .MuiSwitch-switchBase.Mui-checked": {
        transform: "translateX(16px)",
      },
    },
    "& .MuiSwitch-switchBase": {
      padding: 2,
      "&.Mui-checked": {
        transform: "translateX(16px)",
        color: "#fff",
        "& + .MuiSwitch-track": {
          backgroundColor: "#1890ff",
          opacity: 1,
        },
      },
    },
    "& .MuiSwitch-thumb": {
      width: 16,
      height: 16,
      borderRadius: 8,
      transition: theme.transitions.create(["width"], {
        duration: 200,
      }),
    },
    "& .MuiSwitch-track": {
      borderRadius: 10,
      backgroundColor: "rgba(0,0,0,.25)",
      opacity: 1,
    },
  }));
export default FavForm;
JavaScript
복사
◦
FavList
import { useNavigate } from "react-router-dom";
import { Box, Button } from "@mui/material";
import { useEffect, useReducer, useState } from "react";
import { favAPI } from "../../api/services/favorite";
import FavAccordion from "./FavAccordion";
const favListReducer = (state, action) => {
  switch (action.type) {
      case "SET_FAVS":
          return action.payload;
      case "DELETE_FAV":
          return state.filter(fav => fav.id != action.payload.id);
  }
}
const FavList = () => {
  const [favList, dispatch] = useReducer(favListReducer, []);
  const navigate = useNavigate();
  const getFavList = async () => {
    const res = await favAPI.getFavList();
    dispatch({type: "SET_FAVS", payload: res.data});
  };
  useEffect(() => {
    getFavList();
  }, []);
  return (
    <>
      <Button onClick={() => navigate("/favorite/write")}>즐겨찾기 추가</Button>
      
      <Box sx={{width: "500px"}}>
        {favList.map((fav) => (
          <FavAccordion key={fav.id} fav={fav} dispatch={dispatch}/>
        ))}
      </Box>
    </>
  );
};
export default FavList;
JavaScript
복사
◦
FavAccordion
import {
  Accordion,
  AccordionDetails,
  AccordionSummary,
  Avatar,
  Box,
  Button,
  styled,
  Typography,
} from "@mui/material";
import ArrowForwardIosSharpIcon from "@mui/icons-material/ArrowForwardIosSharp";
import { useState } from "react";
import Swal from "sweetalert2";
import { favAPI } from "../../api/services/favorite";
import { useNavigate } from "react-router-dom";
const FavAccordion = ({fav, dispatch}) => {
  const navigate = useNavigate();
  const [isExpand, setIsExpand] = useState(false);
  const handleExpand = () => setIsExpand(prev=> !prev);
  const handleMove = () => {
    let urlLink = fav.url;
    if (!fav.url.startsWith("http://") && !fav.url.startsWith("https://")) {
      urlLink = `http://${fav.url}`;
    }
    window.open(urlLink, "_blank", "noopener, noreferrer");
  }
  const handleDelete = () => {
    Swal.fire({
      title: "확실해요?",
      text: "한 번 삭제하면 다시 되돌릴 수 없어요",
      showCancelButton: true,
      confirmButtonColor: "#3085d6",
      cancelButtonColor: "#d33",
      confirmButtonText: "응! 지워줘."
    }).then(async (result) => {
      if (result.isConfirmed) {
        try {
          const res = await favAPI.deleteFav(fav.id);
          if (res.status == 200) {
            Swal.fire({
              text: "삭제되었습니다.",
              icon: "success"
            });
            dispatch({type:"DELETE_FAV", payload: fav})
          }
        } catch (err) {
          console.error(err);
        }
      }
    });
  }
  const handleModify = () => {
    navigate(`/favorite/modify/${fav.id}`, { state : fav });
  }
  console.log(fav);
  return (
    <MyAccordion expanded={isExpand} onClick={handleExpand}>
      <MyAccordionSummary>
        <Box sx={{display: "flex", justifyContent: "space-between", width: "100%"}}>
          <Avatar alt={fav.title} src={`${process.env.REACT_APP_SERVER}/img/${fav.image?.saved}`} />
          <span>{fav.id}</span>
          <span style={{width: "40%"}}>{fav.title}</span>
          <Button onClick={handleModify}>수정</Button>
          <Button onClick={handleDelete}>삭제</Button>
        </Box>
      </MyAccordionSummary>
      <MyAccordionDetails>
        <Button onClick={handleMove}>{fav.url}</Button>
      </MyAccordionDetails>
    </MyAccordion>
  );
};
const MyAccordion = styled((props) => (
  <Accordion disableGutters elevation={0} square {...props} />
))(({ theme }) => ({
  border: `1px solid ${theme.palette.divider}`,
  "&:not(:last-child)": {
    borderBottom: 0,
  },
  "&::before": {
    display: "none",
  },
}));
const MyAccordionSummary = styled((props) => (
  <AccordionSummary
    expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: "0.9rem" }} />}
    {...props}
  />
))(({ theme }) => ({
  backgroundColor: "rgba(0, 0, 0, .03)",
  flexDirection: "row-reverse",
  "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": {
    transform: "rotate(90deg)",
  },
  "& .MuiAccordionSummary-content": {
    marginLeft: theme.spacing(1),
  },
  ...theme.applyStyles("dark", {
    backgroundColor: "rgba(255, 255, 255, .05)",
  }),
}));
const MyAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
  padding: theme.spacing(2),
  borderTop: "1px solid rgba(0, 0, 0, .125)",
}));
export default FavAccordion;
JavaScript
복사
•
springboot - 회원가입 인증 관련 내용 진행
◦
AuthController
package com.kosta.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.kosta.domain.SignUpRequest;
import com.kosta.domain.UserDeleteRequest;
import com.kosta.domain.UserResponse;
import com.kosta.domain.UserUpdateRequest;
import com.kosta.service.AuthService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
	private final AuthService authService;
	
	// 회원가입
	@PostMapping("")
	public ResponseEntity<UserResponse> signUp(@RequestBody SignUpRequest signUpRequest) {
		log.info("[signUp] 회원가입 진행. 요청정보 : {}", signUpRequest);
		UserResponse userResponse = authService.signUp(signUpRequest);
		return ResponseEntity.status(HttpStatus.CREATED).body(userResponse);
	}
	
	// 회원 전체 리스트
	@GetMapping("")
	public ResponseEntity<List<UserResponse>> getUserList() {
		log.info("[getUserList] 회원 전체 조회");
		List<UserResponse> userList = authService.getUserList();
		return ResponseEntity.ok(userList);
	}
	
	// 회원 정보 수정
	@PatchMapping("")
	public ResponseEntity<UserResponse> updateUser(@RequestBody UserUpdateRequest userUpdateReqeust) {
		log.info("[updateUser] 회원 정보 수정. 수정 요청 정보 : {}", userUpdateReqeust);
		UserResponse userResponse = authService.updateUser(userUpdateReqeust);
		return ResponseEntity.ok(userResponse);
	}
	
	// 회원 삭제
	@DeleteMapping("")
	public ResponseEntity<?> userWithdrawal(@RequestBody UserDeleteRequest userDeleteRequest) {
		log.info("[updateUser] 회원 삭제. 삭제 요청 정보 : {}", userDeleteRequest);
		authService.deleteUser(userDeleteRequest);
		return ResponseEntity.ok(null);
	}
}
Java
복사
◦
AuthService
package com.kosta.service;
import java.util.List;
import com.kosta.domain.SignUpRequest;
import com.kosta.domain.UserDeleteRequest;
import com.kosta.domain.UserResponse;
import com.kosta.domain.UserUpdateRequest;
public interface AuthService {
	UserResponse signUp(SignUpRequest signUpRequest);
	List<UserResponse> getUserList();
	UserResponse updateUser(UserUpdateRequest userUpdateReqeust);
	void deleteUser(UserDeleteRequest userDeleteRequest);
}
Java
복사
◦
AuthServiceImpl
package com.kosta.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.kosta.domain.SignUpRequest;
import com.kosta.domain.UserDeleteRequest;
import com.kosta.domain.UserResponse;
import com.kosta.domain.UserUpdateRequest;
import com.kosta.entity.User;
import com.kosta.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
	private final UserRepository userRepository;
	@Override
	public UserResponse signUp(SignUpRequest signUpRequest) {
		User user = User.builder().email(signUpRequest.getEmail()).name(signUpRequest.getName())
				.password(signUpRequest.getPassword()).build();
		User savedUser = userRepository.save(user);
		return UserResponse.toDTO(savedUser);
	}
	@Override
	public List<UserResponse> getUserList() {
		List<User> userList = userRepository.findAll();
		return userList.stream().map(UserResponse::toDTO).toList();
	}
	@Override
	public UserResponse updateUser(UserUpdateRequest userUpdateReqeust) {
		User user = userRepository.findByEmail(userUpdateReqeust.getEmail())
				.orElseThrow(() -> new IllegalArgumentException("회원 정보 조회에 실패했습니다. [없는 이메일]"));
		if (!user.getPassword().equals(userUpdateReqeust.getPassword())) {
			throw new RuntimeException("비밀번호 입력 오류");
		}
		if (userUpdateReqeust.getName() != null)
			user.setName(userUpdateReqeust.getName());
		User updatedUser = userRepository.save(user);
		return UserResponse.toDTO(updatedUser);
	}
	@Override
	public void deleteUser(UserDeleteRequest userDeleteRequest) {
		User user = userRepository.findByEmail(userDeleteRequest.getEmail())
				.orElseThrow(() -> new IllegalArgumentException("회원 정보 조회에 실패했습니다. [없는 이메일]"));
		if (!user.getPassword().equals(userDeleteRequest.getPassword())) {
			throw new RuntimeException("비밀번호 입력 오류");
		}	
		userRepository.delete(user);
	}
}
Java
복사

