1️⃣  롤링페이퍼 작성이 한번만 되는 에러

  • 원인) 작성시 닉네임과 롤링페이퍼 내용이 requestDto로 설정되어 있는데, 닉네임이 유니크키로 설정되었기 때문에 하나 이상의 롤링페이퍼를 작성할 수 없었음.
  • 해결) 닉네임 유니크키 해제 후 정상 작동

기본키와 유니크키 차이점

더보기
  • 기본키는 즉, 프라이머리 키는 해당 테이블의 식별자 역할을 합니다. 바로 이 제약조건으로 테이블에 하나만 지정할 수 있습니다. 예를 들면, 사람을 식별할 때는 사람의 이름 대신 주민등록번호를 사용합니다. 그 이유는, 바로 유일한 성질을 가지고 있기 떄문입니다. 이렇게 중복성이 없는 유일성을 가진 성질을 의미합니다. 그 중 사용자가 선택한 것을 기본키라고 할 수 있습니다.
  • 유니크 키는 유일성을 가지기 위해 설정한 것입니다. 따라서 지정이 되면 중복이 되는 것을 제어하는 역할을 하게 됩니다. 에를 들면 회원 이름을 중복으로 설정하지 않게 하는 것입니다. 중복되는 이름의 경우에는 뒤에 숫자를 붙여서 최초의 한 사람만 기입할 수 있도록 하는 것입니다. 결국, 프라이머리 키는 유니크 키의 성질을 포함하는 것을 알 수 있습니다. 그 중에, 설계자가 기본적으로 선택한 키라고 할 수 있습니다. 유니크키는 하나의 테이블에 각각 컬럼마다 지정이 가능합니다. 그러나 프라이머리키는 오직 하나만 설정할 수 있습니다.  

 

2️⃣  댓글 좋아요 수만 오르고 좋아요 취소는 되지 않는 에러*

  • 원인) 쿼리문에서 findByUserIdAndUsername 이라고 명시했지만, ()안 매개변수값 순서를 거꾸로 입력
  • 해결) 올바르게 순서 정렬 후 정상 작동

 

3️⃣  유효성 검사가 정상적으로 수행되지않지만 에러없이 통과*

  • 원인) Build.gradle 파일 내부 dependencies 설정에서 상위버전 valid dependency와 하위버전이 동시에 존재할 경우 dependency간 충돌이 있어 애플리케이션은 에러는 없지만 유효성 검사가 진행되지 않음
  • 해결) 사용되지 않거나 중복으로 설정되어진 dependency를 주석처리 또는 삭제하는 것으로 정상적으로 유효성 검사 수행

 

4️⃣  AWS S3 서버 이미지 업로드 불가

  • 문제) S3에 이미지가 로컬에만 저장되고 S3로 convert 되지 않음
  • 원인) 추측 - ACL(액세스 제어 목록) 권한설정 문제
  • 해결) 추측 - 모든 사람(퍼블릭 액세스) 부분에 객체에 나열 권한을 부여

 

5️⃣  예외처리 부분*

  • 문제) 모든 에러의 상태코드가 200번으로 반환됨
  • 원인) MsgResponseDto로 customException 부분의 메세지부분만 return이 되고 Httpstatus 코드는 반환이 안되고 있었기 때문에 발생함
  • 해결) 반환타입을 ResponseEntity로 감싸고 HttpStatus 상태코드도 같이 반환하는 방식으로 변경

 

6️⃣  CORS 에러*

  • 문제) api사용시 cors 에러
  • 발생원인) CORS설정을 하지않아 프론트 3000포트에서 백 8080포트의 자원요청 불가
  • 해결) 서버에서 CorsConfigurationSource를 통한 CORS 설정으로 해결

 

(추가 해결 방안)

  • @CrossOrigin 어노테이션 사용 컨트롤러 또는 사용할 메소드에 어노테이션을 추가
  • WebMvcConfigurer 설정스프링 프로젝트를 생성하면 @SpringBootApplication 어노테이션이 적용된main함수에서WebMvcConfigurer 을 @Bean으로 등록하여 Cors매핑을 추가

 

7️⃣  토큰이 삭제되지 않아 토큰 만료 메세지가 계속 뜨는 에러

  • 문제)만료된 토큰을 계속 들고 있어서 만료되었습니다 메세지가 계속 뜸
  • 원인)토큰 값이 만료되면 자동적으로 토큰 값을 삭제 해줘 야 되는데 계속 남아있음
  • 해결)localStorage.removeItem("token")을 이용하여 status 401 이 뜰 때 if문을 사용하여 제거함

 

8️⃣  json 에러

  • 문제)서버로 데이터를 전송할 때 json문자열로 보내야 하는데 변환되지 않는 에러
  • 원인)"Content-Type": "application/json”이 담긴 config를 헤더 값에 넣어주지 못함
  • 해결)config 만 해준 곳에 {headers : config} 를 넣어 직접 명시를 해줌으로써 해결

프론트엔드 리엑트 코드 부분

 

※ 그 외 트러블슈팅 해결 방법 - 프론트엔드 리엑트 코드는 잘 몰라서, 아래와 같이 스프링에 System.out.pringln 을 사용하여 들어가는 값을 확인해 주면서 에러 체크를 하기도 했다.

 

※ 아쉬웠던 부분 / 정리 💡

  • 협업은 의사소통이 정말 중요하다. 특히 API 명세를 잘 작성하고 수정이 될 때마다 업데이트를 해줘야 한다.
  • 각 파트가 서로의 기능이 완성이 될 때마다 공유를 하며 맞춰가는 과정이 필요했는데 나중에 한꺼번에 합치려다보니 더 힘들었던 것 같다. 완성된 기능끼리는 바로바로 합치는 과정이 필요하다.

사전 세팅

  • 먼저 AWS에서 S3 버킷을 만들고 IAM에서 사용자를 만들어 access key와 secret ket를 발급받는 작업이 필요하다. 
  • 참고 링크 : https://jojoldu.tistory.com/300
 

SpringBoot & AWS S3 연동하기

안녕하세요? 이번 시간엔 SpringBoot & AWS S3 연동하기 예제를 진행해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하는 Github와

jojoldu.tistory.com

 

구현 코드 (1) S3 부분

먼저, S3 라는 패키지를 만들어 이미지를 S3에 업로드하고 삭제하는 부분은 이곳에 구현을 했다.

 

1. AmazonS3Config 파일

@Configuration
public class AmazonS3Config {

    @Value("${cloud.aws.credentials.accessKey}")                        //S3 accessKey
    private String accessKey;

    @Value("${cloud.aws.credentials.secretKey}")                        //S3 secretKey
    private String secretKey;

    @Value("${cloud.aws.region.static}")                                //S3 region
    private String region;

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }
}

 

2. S3Controller 파일

사실 이 파일은 전체 서비스와 합칠 때 필요한 부분은 아니지만, 그 전에 업로드가 되는지 보기위해 테스트용으로 있는 컨트롤러이다.

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class S3Controller {
    private final S3Service s3Uploader;

    @GetMapping("/images")
    public String getImage(@AuthenticationPrincipal UserDetailsImpl userDetails)throws IOException{
        return s3Uploader.getThumbnailPath("img.png");
    }

    @PostMapping("/images")
    public String delete(@RequestParam(required = true) String fileName){
         s3Uploader.deleteFile(fileName);
        return "success";
    }
}

 

3. S3Service 부분

@Slf4j
@Component
@RequiredArgsConstructor
@Service
public class S3Service {

    private final AmazonS3Client amazonS3Client;

    private final ImageFileRepository imageFileRepository;
    @Value("${cloud.aws.s3.bucket}")                                                        //bucket 이름
    public String bucket;

    public void upload(MultipartFile multipartFile, String dirName, Board board, User user) throws IOException {
        File uploadFile = convert(multipartFile).orElseThrow(() -> new IllegalArgumentException("파일 전환 실패"));

        ImageFile imageFile = new ImageFile(upload(uploadFile, dirName), user, board);
        imageFileRepository.save(imageFile);
//        return upload(uploadFile, dirName);
    }

    // S3로 파일 업로드하기
    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + UUID.randomUUID(); // S3에 저장된 파일 이름
        String uploadImageUrl = putS3(uploadFile, fileName); // s3로 업로드
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }

    // S3로 업로드
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    // 로컬에 저장된 이미지 지우기
    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("File delete success");
            return;
        }
        log.info("File delete fail");
    }

    private Optional<File> convert(MultipartFile multipartFile) throws IOException {
        File convertFile = new File(System.getProperty("user.dir") + "/" + multipartFile.getOriginalFilename());
        // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
                fos.write(multipartFile.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();
    }

    // find image from s3
    public String getThumbnailPath(String path) {
        return amazonS3Client.getUrl(bucket, path).toString();
    }

    //remove s3 object
    public void deleteFile(String fileName){
        DeleteObjectRequest request = new DeleteObjectRequest(bucket, fileName);
        amazonS3Client.deleteObject(request);
    }
}

 

 

코드 구현 (2) 게시판 코드에 적용하기

1. ImageFile 엔티티를 추가해 주었다.

@Getter
@Entity
@NoArgsConstructor
public class ImageFile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;                    // id

    @Column(nullable = false)           // image 경로
    private String path;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "userid", nullable = false)
    private User user;                  // userid

    @OneToOne
    @JoinColumn(name = "boardid", nullable = false)
    private Board board;

    public ImageFile(String path, User user, Board board){
        this.path = path;
        this.user = user;
        this.board = board;
    }
}

 

 

2. BoardController

  • formdata 형식이기 때문에 POST 부분을 @RequestBody 가 아니라 @RequestPart 로 바꿔주어야 한다.
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class BoardController {

    // Connect to boardservice
    private final BoardService boardService;

    // DB save
    @PostMapping(value = "/boards", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
    public ResponseEntity<BoardResponseDto> createBoard(@RequestPart BoardRequestDto requestDto,
                                                        @RequestPart(required = false) MultipartFile image,
                                                        @AuthenticationPrincipal UserDetailsImpl userDetails) throws IOException{
        return ResponseEntity.ok(boardService.createBoard(requestDto, userDetails.getUser(), image));
    }

    // DB select all
    @GetMapping("/boards")
    public ResponseEntity<List<BoardResponseDto>> getBoards(){return ResponseEntity.ok(boardService.getBoards());}

    // DB select one
    @GetMapping("/boards/{id}")
    public ResponseEntity<BoardResponseDto> getBoard(@PathVariable long id){return ResponseEntity.ok(boardService.getBoard(id));}

    // DB update
    @PatchMapping("/boards/{id}")
    public ResponseEntity<BoardResponseDto> updateBoard(@PathVariable Long id,
                                                        @RequestBody BoardRequestDto requestDto,
                                                        @AuthenticationPrincipal UserDetailsImpl userDetails){
        return ResponseEntity.ok(boardService.update(id,requestDto, userDetails.getUser()));
    }

    // DB delete
    @DeleteMapping("/boards/{id}")
    public ResponseEntity<BoardDeleteResponseDto> deleteBoard(@PathVariable Long id,
                                                              @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return ResponseEntity.ok(boardService.deleteBoard(id, userDetails.getUser()));
    }
}

 

3. BoardService 

 



@Slf4j
@Service
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;                                                      // board repo connect
    private final CommentRepository commentRepository;                                                  // comment repo connect
    private final CommentLikeRepository commentLikeRepository;                                          // commentLike repo connect
    private final BoardLikeRepository boardLikeRepository;                                              // boardLike repo connect
    private final ImageFileRepository imageFileRepository;                                              // ImageFile repo connect
    private final S3Service s3Uploader;

    @Value("${cloud.aws.s3.bucket}")
    public String bucket;

    // Board Create function
    public BoardResponseDto createBoard(BoardRequestDto requestDto, User user,MultipartFile image) throws IOException {

        // 1. create board Object and insert DB
        Board board = new Board(requestDto, user);                                                      // DTO -> Entity
        boardRepository.save(board);

//        if(!image.isEmpty()) {
        if (image != null){
            s3Uploader.upload(image, "static", board, user);
        }

        return new BoardResponseDto(board,user.getUsername(), "");                                          // return Response  Entity -> DTO

    }

    // Get Boards from DB (all)
    public List<BoardResponseDto> getBoards() {
        // 1. find boardList
        List<Board> BoardList = boardRepository.findAllByOrderByModifiedAtAsc();                            // Select All
        List<BoardResponseDto> BoardResponseDtoList = new ArrayList<>();

        // 2. find commentList and add ResponseDto
        for(Board board: BoardList){
            List<Comment> comments = commentRepository.findAllByBoardOrderByIdAsc(board);
            List<CommentResponseDto> commentList = new ArrayList<>();

            Optional<ImageFile> Img = imageFileRepository.findByBoard(board);
            String Img_path = "";

            if(Img.isEmpty()){
                Img_path = "";
            }
            else{
                Img_path = Img.get().getPath();
            }

            for (Comment comment : comments){
                commentList.add(new CommentResponseDto(comment));
            }

            if(comments.isEmpty()){
                BoardResponseDtoList.add(new BoardResponseDto(board,board.getUser().getUsername(), Img_path));
            }else{
                BoardResponseDtoList.add(new BoardResponseDto(board,board.getUser().getUsername(), commentList, Img_path));
            }
        }
        return BoardResponseDtoList;
    }

    // Get memo from DB (one)
    public BoardResponseDto getBoard(long id){
        // 1. find board
        Board board = boardRepository.findById(id).orElseThrow(()->                                        // Select one
                new CustomException(ErrorCode.NO_POST_FOUND)
        );

        // 2. find comment
        List<Comment> comments = commentRepository.findAllByBoardOrderByIdAsc(board);
        List<CommentResponseDto> commentList = new ArrayList<>();

        Optional<ImageFile> Img = imageFileRepository.findByBoard(board);
        String Img_path = "";

        if(Img.isEmpty()){
            Img_path = "";
        }
        else{
            Img_path = Img.get().getPath();
        }

        // 3. add ResponseDto
        for (Comment comment : comments){
            commentList.add(new CommentResponseDto(comment));
        }
        if(comments.isEmpty()){
            return new BoardResponseDto(board,board.getUser().getUsername(), Img_path);                               // Entity -> DTO
        }else{
            return new BoardResponseDto(board, board.getUser().getUsername(),commentList, Img_path);
        }
    }

    // DB update function
    @Transactional
    public BoardResponseDto update(Long id, BoardRequestDto requestDto, User user) {
        // 1. 유저 권한 GET
        UserRoleEnum userRoleEnum = user.getRole();
        Board board;

        // 2. 유저 권한에 따른 동작 방식 결정
        if(userRoleEnum == UserRoleEnum.USER){
            board = boardRepository.findById(id).orElseThrow(                                               // find memo
                    () -> new CustomException(ErrorCode.NO_POST_FOUND)
            );

            if(board.getUser().getId().equals(user.getId())){                                               // 자기 자신이 작성한 게시물이면
                board.update(requestDto);                                                                   // DB Update
            }else{
                throw new CustomException(ErrorCode.NO_MODIFY_POST);
            }
        }else{                                                                                              // 관리자 권한일 때,
            board = boardRepository.findById(id).orElseThrow(                                               // find board
                    () -> new CustomException(ErrorCode.NO_POST_FOUND)
            );
            board.update(requestDto);
        }
        return new BoardResponseDto(board,user.getUsername(), "");
    }

    // DB delete function (data delete)
    public BoardDeleteResponseDto deleteBoard(Long id, User user) {
        // 1. 유저 권한 GET
        UserRoleEnum userRoleEnum = user.getRole();
        Board board;

        // 2. 유저 권한에 따른 동작 방식 결정
        if(userRoleEnum == UserRoleEnum.USER){
            board = boardRepository.findById(id).orElseThrow(                                               // find memo
                    () -> new CustomException(ErrorCode.NO_ROLLING_FOUND)
            );

            if(board.getUser().getId().equals(user.getId())){                                               // 자기 자신이 작성한 게시물이면
                boardLikeRepository.deleteAllByBoard(board);                                                // DB DELETE (선행)
                List<Comment> commentList = commentRepository.findAllByBoardOrderByIdAsc(board);
                for(Comment comment: commentList){
                    commentLikeRepository.deleteAllByComment(comment);
                }

                Optional<ImageFile> imageFile = imageFileRepository.findByBoard(board);

                if (imageFile.isPresent()){
                    String path =  imageFile.get().getPath(); //이미지 파일 path값 가져오기
                    String filename = path.substring(58); // path값에서 키값 추출
                    s3Uploader.deleteFile(filename); //키값으로 s3 삭제

                    imageFileRepository.deleteAllByBoard(board);
                }

                commentRepository.deleteAllByBoard(board);
                boardRepository.deleteById(id);                                                             // 해당 게시물 삭제
            }else{
                System.out.println("Error");
                throw new CustomException(ErrorCode.NO_DELETE_POST);

            }
        }else{                                                                                              // 관리자 권한일 떄
            board = boardRepository.findById(id).orElseThrow(                                               // find board
                    () -> new CustomException(ErrorCode.NO_POST_FOUND)
            );
            boardLikeRepository.deleteAllByBoard(board);
            List<Comment> commentList = commentRepository.findAllByBoardOrderByIdAsc(board);
            for(Comment comment: commentList){
                commentLikeRepository.deleteAllByComment(comment);
            }

            commentRepository.deleteAllByBoard(board);
            boardRepository.deleteById(id);
        }
        return  new BoardDeleteResponseDto("삭제 성공", HttpStatus.OK.value());
    }

}

1. 프로젝트 이름 : Grow Together 🌱

 

 

2. 프로젝트 소개  :항해99 10기 b반 커뮤니티 사이트

  • 카테고리는 롤링페이퍼작성 / 정보공유 게시판 두 개로 나눠진다
  • 롤링페이퍼 작성 페이지에선, 크루원의 이름을 클릭하면 롤링페이퍼를 작성할 수 있다.
  • 정보공유 게시판은 각자 알게된 꿀팁과 정보들을 공유하는 공간이다.

 

3. 와이어 프레임

 

4. API 명세 링크 :

https://www.notion.so/d85b6b77cdaa4ce28fbfdb77af51ae0b?v=4bb451dd18e84cc4b8323f9537dd027f

 

5. ERD

 

 

6. 역할 분담

🔰차이진

더보기
  • 롤링페이지 전체조회 ( 반 명단 조회)
  • 롤링페이지 상세조회/ 작성 / 수정 / 삭제
  • S3
  • 로그인/ 회원가입 기능 추가 ( 소셜 - 시간이 된다면)

🔰황지성

더보기
  • 정보공유 게시글 작성/조회/수정/삭제
  • 정보공유 게시글 좋아요
  • 로그인 / 회원가입 / 회원 탈퇴
  • AWS 배포
  • S3

🔰김재영

더보기
  • 정보공유 상세페이지 댓글 작성 / 수정 / 삭제
  • 정보 공유 상세페이지 좋아요 기능 추가
  • 상세페이지 대댓글 작성 / 수정 / 삭제( 가능하면 )
  • S3

 

7.  주요 기능 / 기술 스택

  • JWT이용한 로그인
  • 로그인 유효성 검사
  • Spring Security이용한 인증/ 인가
  • 게시글 CRUD
  • 댓글 CRUD + 댓글 좋아요 기능
  • 롤링페이퍼 CURD
  • AWS S3이용한 이미지 업로드/조회/삭제

  • Spring Boot
  • Spring Security
  • JWT
  • AWS (S3, RDS, EC2)
  • MySQL
  • Github

 

8. 깃헙 주소

https://github.com/myrollingpaper/BE

 

GitHub - myrollingpaper/BE

Contribute to myrollingpaper/BE development by creating an account on GitHub.

github.com

 

+ 추가 

프론트엔드와 협업 후 완성된 모습 ( 팀원 하차로 스코프를 게시글 CRUD로 줄여서 완성)

 

 

+ Recent posts