back-end/Spring

(Spring Boot)회원관리,게시판,파일업로드

doheun 2023. 4. 13. 14:01
반응형

Spring Boot

이전에 회원관리 기능에 추가로 테이블에 외래키를 사용하여 연결을 한 후에 기능 추가

흐름

  1. 어떤 exception인지 처리를 위해서 로그찍는 클래스 생성

com.example.demo.config.ExceptionHandle.java

@ControllerAdvice
public class ExceptionHandle {
    private Logger logger=LoggerFactory.getLogger(ExceptionHandle.class);

        @ExceptionHandler(Exception.class)
        public String handleException(Exception e, Model model) {
            logger.error("Exception 발생:{}",e.getMessage());
            model.addAttribute("msgs", "오류가 발생하여 확인중입니다.");
            return "thymeleaf/error";
        }
}
  1. 게시판(board), 파일업로드(fileboard) DTO생성
    BoardDto.java, FileBoardDto.java
  2. 게시판 쿼리작성
    조인했을 때 게시판dto에 파일업로드의 dto를 담을 DTO객체를 새로 생성해야함(구조만 만들어놓으면 myBatis가 처리)
    조인할 가능성이 있는 테이블에서 기본키를 가지는 게시판dto에 private FileBoardDto fileBoardDto를 선언후
    boardMapper.xml에서 resultMap으로 매핑
    property -> DTO
    column -> DB
    파일을 추가할 때 게시판의 seq값을 가져와야하는데 게시판 추가할 때의 증가하는 값을 파일추가의 seq값으로 가져오기 위해서 LAST_INSERT_ID()라는 함수를 사용(다른기능도 공부)

boardMapper.xml

<mapper namespace="com.example.demo.mapper.BoardMapper">
    <resultMap type="BoardDto" id="BoardDtoMap">
        <result property="board_seq" column="BOARD_SEQ"/>
        <result property="id" column="ID"/>
        <result property="title" column="TITLE"/>
        <result property="content" column="CONTENT"/>
        <result property="regdate" column="REGDATE"/>
        <association property="fileBoardDto" resultMap="FileBoardDtoMap"></association>

    </resultMap>

    <resultMap type="FileBoardDto" id="FileBoardDtoMap">
        <result property="file_seq" column="FILE_SEQ"/>
        <result property="board_seq" column="BOARD_SEQ"/>
        <result property="origin_filename" column="ORIGIN_FILENAME"/>
        <result property="stored_filename" column="STORED_FILENAME"/>
    </resultMap>

    외부조인
    <select id="getBoard" resultMap="BoardDtoMap" parameterType="int">
        SELECT T.BOARD_SEQ, ID,TITLE,CONTENT,REGDATE,FILE_SEQ,ORIGIN_FILENAME
        FROM BOARD T LEFT OUTER JOIN FILEBOARD F
        ON T.BOARD_SEQ=F.BOARD_SEQ;
        WHERE T.BOARD_SEQ=#{board_seq}
    </select>

    <insert id="insertBoard" parameterType="BoardDto">
        INSERT INTO BOARD
        VALUES(NULL,#{id},#{title},#{content},SYSDATE())
    </insert>

    <insert id="insertFileBoard" parameterType="FileBoardDto">
        INSERT INTO FILEBOARD
        VALUES(NULL,LAST_INSERT_ID(),#{origin_filename},#{stored_filename})
    </insert>
</mapper>

BoardDtoMap에서 BoardDto형태로 받고 association을 통해 FIleBoardDtoMap을 가진 resultMap을 찾아서 가도록
4. Dao처리(@Mapper)
BoardMapper.java

@Mapper
public interface BoardMapper {
    //글목록
    public List<BoardDto> getAllList();

    //글조회
    public BoardDto getBoard(int board_seq);

    //글추가
    public boolean insertBoard(BoardDto dto);

    //파일정보추가
    public boolean insertFileBoard(FileBoardDto dto);

}
  1. Service클래스(@Service ,@Autowired- xml객체 가져올 때)

BoardService.java

@Service
public class BoardService {
    @Autowired
    private BoardMapper boardMapper;

    public List<BoardDto> getAllList(){
        return boardMapper.getAllList();
    }

    public BoardDto getBoard(int board_seq) {
        return boardMapper.getBoard(board_seq);
    }
}
  1. 파일업로드
    기존 -multipart로 형변환
    스프링부트 - 라이브러리 (spring web에 내장) 따로 의존성추가 필요없음
  2. 화면쪽 데이터를 받을 DTO클래스 생성(Command)-@Data

InsertBoardCommand.java

@Data
public class InsertBoardCommand {
    private int board_seq;
    @NotBlank(message="아이디를 입력하세요")
    private String id;
    @NotBlank(message="제목를 입력하세요")
    private String title;
    @NotBlank(message="내용를 입력하세요")
    private String content;
}
  1. 업로드기능 따로 만들어서 불러서 사용(@Service)

FileService.java

@Service
public class FileService {

    //파일 하나 업로드 하기
    public FileBoardDto uploadFile(String uploadPath,MultipartFile multipartFile) throws IllegalStateException, IOException {
        String origin_filename=multipartFile.getOriginalFilename();
        String stored_filename=UUID.randomUUID()
                        +origin_filename.substring(origin_filename.indexOf("."));
        String fileuploadUrl=uploadPath+"/"+stored_filename;

        multipartFile.transferTo(new File(fileuploadUrl));//upload가 실행
        return new FileBoardDto(0, 0, origin_filename, stored_filename);
    }
}
  1. BoardService에서 InsertBoardCommand객체를 파라미터로 받아서 insertBoard메서드에서 처리
    BoardDto (@Component)해서 객체로 등록 후 service에서 @Autowired로 setter사용

BoardService.java

@Autowired
    private BoardDto boardDto;
@Autowired
    private FileService fileService;    
@Transactional
public boolean insertBoard(InsertBoardCommand insertBoardCommand,
            MultipartFile multiFile ) throws IllegalStateException, IOException {
        boardDto.setId(insertBoardCommand.getId());
        boardDto.setTitle(insertBoardCommand.getTitle());
        boardDto.setContent(insertBoardCommand.getContent());
        //첨부가 되었다면
        if(!multiFile.isEmpty()) {
            //게시글의 첨부파일 경로[절대경로 설정]
            String filePath="C:/workspace_backend/demotest-2/src/main/resources/upload";
            //upload
            FileBoardDto fdto=fileService.uploadFile(filePath, multiFile);
            //DB에 저장
            boardMapper.insertFileBoard(fdto);
        }
        return boardMapper.insertBoard(boardDto);
    }

여러 작업들이 한번에 진행되기 때문에 트랜잭션 처리 필요(@Transactional)

  1. 컨트롤러생성(게시판 관련 처리)
    BoardController.java

글 추가시에 폼이동할 떄 빈 객체를 같이 화면으로 보내서 받아와야함
글 추가시에는 @Validated InsertBoardCommand,BindingResult항상 같이 파라미터로 받아야함
화면으로부터 filename이라는 이름의 파일도 받아야하기 때문에 MultiPartFile 인터페이스필요 (@RequestParam("filename"))
result의 에러발생시 form으로 이동
insertBoard의 예외처리 필요
추가완료시 redirect

@Controller
@RequestMapping(value="/board")
public class BoardController {

    @Autowired
    private BoardService boardService;

    @GetMapping("/boardList")
    public String boardList(Model model) {
        List<BoardDto> list=boardService.getAllList();
        model.addAttribute("list",list);
        return "thymeleaf/board/boardList";
    }

    @GetMapping("/boardDetail")
    public String boardDetail(int board_seq,Model model) {
        BoardDto dto=boardService.getBoard(board_seq);
        model.addAttribute("dto",dto);
        return "thymeleaf/board/boardDetail";
    }

    @GetMapping("/boardInsertForm")
    public String boardInsertForm(Model model) {
        model.addAttribute("insertBoardCommand",new InsertBoardCommand());
        return "thymeleaf/board/boardInsertForm";
    }

    @PostMapping("/boardInsert")
    public String boardInsert(@Validated InsertBoardCommand insertBoardCommand,
            BindingResult result,
            @RequestParam("filename") MultipartFile multiFile
            ,Model model) {
        if(result.hasErrors()) {
            System.out.println("글모두 입력 필요");
            return "thymeleaf/board/boardInsertForm";
        }
        try {
            boardService.insertBoard(insertBoardCommand, multiFile);
            System.out.println("글추가 완료");
        } catch (Exception e) {
            e.printStackTrace();
            return "thymeleaf/board/boardInsertForm";
        }

        return "redirect:/board/boardList";
    }

}
  1. 페이지 생성
    error.html
    boardList.html
    boardInsertForm.html(form의 enctype속성= multipart/form-data)
    오류
    input태그에 th:value에서 session값 넣기

insertBoardCommand에서 id validated해제 후
컨트롤러에서 boardInsert부분 파라미터 HttpServletRequest 받아서 getSession.getAttribute("mdto")


오류
파일업로드 후 글 추가시
Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException:


boardDetail.html페이지생성

  1. 컨트롤러에서 만든 페이지 모두처리

  1. 글추가와 파일추가에 대한 board_seq, file_seq처리 문제
    Last_insert_id() 문제 -board에서의 auto_increment가 있는 열을 사용할 떄는 원하는 값이 아닌 다른 데이터를 가지고 올 가능성이 있음

수정
boardInsertForm.html
파일에 multiple="multiple"추가 후 여러개 업로드 할 때 하나의 파일만 DB에 저장
application.properties환경설정(파일 업로드 사이즈
컨트롤러에서 여러개의 파일 처리 파라미터를 MultipartRequest로 받기
BoardService.java에서 insertBoard 구현
fileService에서 업로드 기능 확인
기능이기 때문에 새로 메서드 만들어서 사용
컨트롤러, 서비스에서 파라미터를 전부 MultipartRequest로 받고 List로 MultipartFile들을받기

FileService.java

public List<FileBoardDto> uploadFiles(String uploadPath,MultipartRequest multipartRequest) throws IllegalStateException, IOException {

        List<MultipartFile> multipartFiles=multipartRequest.getFiles("filename");
        //DB에 저장할 파일 List를 만들어서 전달
        List<FileBoardDto> uploadFileList=new ArrayList<>();
        for(MultipartFile multipartFile:multipartFiles) {
            //원본파일명, 저장파일명,파일저장경로 
            //업로드
            String origin_filename=multipartFile.getOriginalFilename();
            String stored_filename=UUID.randomUUID()
                            +origin_filename.substring(origin_filename.indexOf("."));
            String fileuploadUrl=uploadPath+"/"+stored_filename;

            multipartFile.transferTo(new File(fileuploadUrl));//upload가 실행
            uploadFileList.add(new FileBoardDto(0, 0, origin_filename, stored_filename));
        }

        return uploadFileList;
    }

multipartReqeust를 받는 모든 파일들을 거꾸로 구현하러 이동

BoardService.java

  • board_seq처리가 문제 (fileService에서는 origin,stored만 받아서 처리)
  • 게시글이 추가될 떄의 board_seq를 가져와야하는데 게시글을 추가하기 전에는 boardDto는 id,title,content만 있음
  • boardMapper.xml에서 쿼리 수정 필요
  • board테이블의 insert속성에서 useGeneratedKeys="true" keyProperty="board_seq"설정

<insert id="insertBoard" parameterType="BoardDto" 
                            useGeneratedKeys="true" keyProperty="board_seq">
        INSERT INTO BOARD
        VALUES(NULL,#{id},#{title},#{content},SYSDATE())
    </insert>
  • 이쿼리가 종료될 때 파라미터BoardDto로 board_seq를 전달
  • 이해는안되지만 파라미터로 전달 받았던 코드(BoardService의 boardmapper.insertBoard(BoardDto) 부분에 BoardDto에 board_seq가 저장이됨
@Transactional
    public boolean insertBoard(InsertBoardCommand insertBoardCommand,
//            MultipartFile multiFile ) throws IllegalStateException, IOException {
            MultipartRequest multipartRequest ) throws IllegalStateException, IOException {
            boardDto.setId(insertBoardCommand.getId());
        boardDto.setTitle(insertBoardCommand.getTitle());
        boardDto.setContent(insertBoardCommand.getContent());
        boolean isS=boardMapper.insertBoard(boardDto);```
        System.out.println("멀티파일:"+multipartRequest.getFiles("filename").size());
                if(multipartRequest.getFiles("filename").size()>0) {
                    String filePath="C:/workspace_backend/demotest-2/src/main/resources/upload";

                    //업로드 후에, 업로드한 파일을 만든기능으로부터 가져온다
                    List<FileBoardDto> uploadfileList=fileService.uploadFiles(filePath, multipartRequest);

                    //mapper.xml의 useGe..를 통해 board_seq를 담아서 다시 가져온다
                    for(FileBoardDto fdto: uploadfileList) {
                        boardMapper.insertFileBoard(
                                new FileBoardDto(0,boardDto.getBoard_seq(),fdto.getOrigin_filename(),fdto.getStored_filename()));
                    }
                }
                return isS;
            }
  1. boardDto에서 글하나에 파일 여러개를 리스트로 fileboardDto선언
  • mapper의 하나가 아니고 여러개를 담기위해 association -> collection으로(일대다의 관계 처리)
  • mapper.xml에서 unique나 키값들을 명시해주면 mybatis내에서 처리가 수월

오류

  1. insertBoardFome페이지에서 파일이 있는지 없는지에 대한 처리

  2. multipartRequest로 넘어오는 것들 있는지 없는지 처리 조건
    해결

  3. 컨트롤러에서 download처리
    파일정보를 가지고오는 쿼리 mapper.xml -> BoardMapper.java (xmlm에서 정의한 파라미터, 반환타입과 동일하게 설정) -> 컨트롤러

컨트롤러 -> FileService @Autowired
이전에는 while문을 통해 byte단위로 읽어서 처리했지만 spring내에서 FileCopyUtils라는 클래스를 통해서 쉽게 구현
FileService.java


public void fileDownload(String filePath, String origin_filename, String stored_filename,
            HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {

        //다운로드를 위한 준비 작업

        //읽어들인 파일 정보를 화면에 다운할수 있게 설정
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition",
                "attachment; filename="+URLEncoder.encode(origin_filename, "UTF-8"));
        response.setHeader("Content-transfer", "binary");

        File file=new File(filePath+"/"+stored_filename);

        //시스템에 있을 파일을 가져오기 위한 객체 
        FileInputStream fs=null;
        //웹 브라우저로 내보내기 위한 객체 
        ServletOutputStream out=null;
        try {

            fs=new FileInputStream(file);
            out=response.getOutputStream();

            //전송
            FileCopyUtils.copy(fs, out);
            response.flushBuffer();
            out.flush();
            out.close();

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
            if(fs!=null) {
                try {
                    fs.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    }

오류
Exception 발생:Failed to convert value of type 'java.lang.String' to required type 'int'

  1. 삭제
    com.example.demo.command(@NotEmpty)
    boardList.html(form)
    controller
    • 만약 board의 글을 삭제하면 참조하는 부모가 사라지기 때문에 제약 조건을 걸어줘야한다
    • board테이블에 delflag추가
      mapper.xml -> BoardMapper.java -> BoardService.java-> controller
      삭제후 redirect로 boardlist로 갔을 때 th:object를 빈 객체라도 보내야 되기 때문에 boardlist컨트롤러에서 model로 추가 해야함
      boardlist.html validate처리 후 getallList에서 where 조건으로 delflag='n'인것 찾기

기타참고

  1. thymeleaf ?
    Thymeleaf는 JSP(JavaServer Pages)에서 뷰 렌더링에 일반적으로 사용되는 JSTL(JavaServer Pages Standard Tag Library)의 대안으로 Spring Boot 웹 애플리케이션에서 널리 사용되는 템플릿 엔진입니다. Thymeleaf는 데이터 바인딩, 반복, 조건부 렌더링 및 보기의 데이터 형식 지정을 위한 EL(표현 언어)과 같은 JSTL과 유사한 기능을 제공합니다. 그러나 Thymeleaf는 JSTL에 비해 다음과 같은 몇 가지 이점을 제공합니다.
  • 최신 구문: Thymeleaf는 웹 개발자에게 더 자연스러운 HTML5 친화적인 최신 구문을 사용하여 보기를 더 쉽게 만들고 유지 관리할 수 있습니다.
  • HTML과의 원활한 통합: Thymeleaf 보기는 잘 구성된 HTML이므로 웹 디자이너는 JSTL과 같은 별도의 태그 기반 언어를 배울 필요 없이 템플릿에서 작업할 수 있습니다.
  • 클라이언트 측 및 서버 측 렌더링: Thymeleaf는 클라이언트 측(JavaScript) 및 서버 측(Java) 렌더링을 모두 지원하여 애플리케이션의 요구 사항에 맞는 렌더링 방식을 선택할 수 있는 유연성을 제공합니다.
  • 철저한 문서화 및 커뮤니티 지원: Thymeleaf에는 포괄적인 문서화와 활발한 커뮤니티가 있어 리소스를 쉽게 찾고 필요할 때 도움을 받을 수 있습니다.
  • 확장성: Thymeleaf는 사용자 지정 방언 및 플러그인을 개발할 수 있으므로 필요에 따라 기능을 쉽게 확장할 수 있습니다.

전반적으로 Thymeleaf는 최신 구문, HTML과의 원활한 통합, 렌더링 접근 방식의 유연성, 광범위한 문서 및 커뮤니티 지원으로 인해 Spring Boot 애플리케이션에서 뷰를 렌더링하는 데 널리 사용되는 선택입니다.

  1. 조인시 resultMap 사용법
    association , collection
    조인하는 테이블의 개수에 따라 assocation을 통해 mapper.xml에서 연결필요
  2. 게시판, 회원가입, 파일업로드 쿼리 ,파라미터, 반환타입 조인할 때 마다 새로 구현해야되기 때문에 흐름 파악하는 것 중요
  3. LAST_INSERT_ID()
  4. @Autowird와 그냥 import의 차이????
  5. Getmapping ,Postmapping -> 클라이언트의 요청
  6. thymeleaf 기본 문법

앞에 # -> thymeleaf에서 제공하는 규칙이있음(date format)

if조건문
th:if="${}", th:unless="${}"
반복문
th:each="변수 : ${list}"
button에서 th:onclick
th:onclick="|location.href='@{/board/boardInsertForm}'|"
th:field="*{}"

  1. mapper.xml
  • useGeneratedKeys="" keyProperty=""
  • selectKey
    만약 selectKey를 적용한다면

파일 테이블의 insert에서
<insert id="insertFileBoard" parameterType="FileBoardDto">
  <selectKey resultType="int" order="BEFORE" keyProperty="board_seq" keyColumn="board_seq">
      select Last_insert_id()
  </selectkey>
    insert into fileboard
    values(null,#{board_seq},#{origin_filename},#{stored_filename})
   </insert>
  1. @NotEmpty
반응형

'back-end > Spring' 카테고리의 다른 글

(Spring Boot)회원관리  (0) 2023.04.11
(Spring Boot)세팅  (0) 2023.04.11