2023. 11. 7. 20:16ㆍSpringboot
1. Requirements
- Spring boot version 2.4.5
2. Stacks
Springboot
Mybatis
BootStrap
- Loombook
- Spring Data JPA
- Spring Boot DevTools
- Spring Security
- Spring Web
- Spring boot version 2.4.5
3. Code
1. host.jsp
- localhost:8080/image/host으로 접속했을 때 나오는 첫 페이지(이는 임시이며 바뀔 예정)
- 해당 밑에 있는 코드는 인증된 정보에 접근하는 방법이며 필자는 모든 jsp파일의 header.jsp에
적어줌으로써 다른 jsp파일은 안 적혀있습니다.
- ${principal}에 해당하는 변수를 사용할 시 꼭 필요합니다.
- 해당 jsp는 form형태를 post방식으로 보냅니다.
- 해당 페이지는 Springboot package com.pjh.company.config부분에 void configure(HttpSecurity http)에
http.authorizeRequest(). antMatchers("/host.**")를 통해 로그인이 되지 않으면 접근 불가능하게 설계되어 있습니다.
- 해당 페이지는 버튼을 누르면 모달이 켜지는 방식으로 설계되어 있습니다.
- 이는 추후에 업로드를 버튼을 누르면 db 저장되어 있는 정보를 카드형식으로 출력될 예정입니다.
- <style></style> 관련하여 추후 변경 예정입니다.
- img태그의 onerror에 해당하는 jpg파일은 커스텀하여 만든 이미지 파일입니다.
이에 defaultFileUploadPicture.jpg는 직접 설정해줘야 하며 src/main/resources/static/images 폴더 밑에 넣어줘야 합니다.
- 해당 이미지 업로드 버튼, Details버튼을 누를 시에 모달창이 비활성화되는 오류가 있었는데,
해당 오류 상황에 대한 해결방법을 링크로 올려놓겠습니다.
https://stackoverflow.com/questions/10636667/bootstrap-modal-appearing-under-background
Bootstrap modal appearing under background
I have used the code for my modal straight from the Bootstrap example, and have included only the bootstrap.js (and not bootstrap-modal.js). However, my modal is appearing underneath the grey fade (
stackoverflow.com
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<sec:authorize access="isAuthenticated()">
<!--인증된 정보에 대한 접근 방법-->
<sec:authentication property="principal" var="principal"/>
</sec:authorize>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>pppp</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
<link rel="shortcut icon" sizes="76x76" type="image/x-icon"
href="https://a0.muscache.com/airbnb/static/logotype_favicon-21cc8e6c6a2cca43f061d2dcabdf6e58.ico">
</head>
<style>
.modal {
display: none;
z-index : 1050;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
/*position: absolute;*/
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.modal-dialog {
/*position: absolute; 화면은 얘떄문에*/
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin: 0; /* Remove any margin for centering */
}
.modal-backdrop {
z-index: -1;
}
.imageBox {
width: 300px;
height: 200px;
}
.popularContainer {
margin-top: 20px;
}
.card-body {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.card-body img {
width: 300px !important;
height: 300px !important;
object-fit: contain;
margin-top: 10px;
margin-bottom: 20px;
}
.card-body button {
width: 100%;
border: 1px solid black;
padding: 5px 10px;
border-radius: 5px;
margin-top: 10px;
}
.row {
margin-right: 0;
margin-left: 0;
}
.col-md-4 {
padding-right: 15px;
padding-left: 15px;
}
</style>
<body>
<!--principalId 담아두는 곳-->
<input type="hidden" id="principalId" value="${principal.user.id}">
<!--principalId는 이후에 layout header에 모두 넣을 예정-->
<button id="openModal">파일 업로드 모달 열기</button>
<div class="popularContainer">
<!-- 인기게시글 갤러리(GRID배치) -->
<div class="popularGallery">
<div class="row">
<c:forEach var="image" items="${images}">
<div class="col-md-4 mb-4"> <!-- Add a margin at the bottom and right of the column -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Title: ${image.title}</h5>
<p class="card-text">Description: ${image.description}</p>
<img class="card-img-top"
src="/upload/${image.postImageUrl}"
onerror="this.src='/images/defaultFIleUploadPicture.png'" alt="Image">
<button type="button" class="btn btn-outline-white" onclick="openImageDetailsModal('${image.id}','${image.user.username}');">Details</button>
<button class="btn btn-outline-white" onclick="openImageDetailsModal('${image.id}','${image.user.username}');">Test</button>
<button class="btn btn-outline-white" onclick="openImageDetailsModal('${image.id}','${image.user.username}');">Test</button>
</div>
</div>
</div>
</c:forEach>
</div>
</div>
</div>
<!-- 이미지 업로드 모달 -->
<div class="modal fade" id="imageUploadModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true" data-backdrop="static">
<div class="modal-dialog" style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 50%;">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="imageUploadModalLabel">Image Upload</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="imageUploadForm" action="/image/host/${principal.user.id}" method="post" enctype="multipart/form-data">
<div class="modal-body">
<div class="mb-3">
<label for="title" class="col-form-label">Title:</label>
</div>
<div class="mb-3">
<label for="description" class="col-form-label">Description:</label>
<textarea name="description" id="description"></textarea>
</div>
<div class="mb-3">
<label for="file" class="col-form-label">Select Image:</label>
<input type="file" name="file" id="file">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
</div>
</div>
</div>
<!--이미지 업로드 모달 끝-->
<!-- 이미지 정보 모달 -->
<div class="modal fade" id="imageDetailsModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog" style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 50%;">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="imageDetailsModalLabel">Image Details</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="imageUsername" class="col-form-label">Username : </label>
<span id="imageUsername"></span>
</div>
<div class="mb-3">
<label for="imageTitle" class="col-form-label" >Title : </label>
<span id="imageTitle"></span>
</div>
<div class="mb-3">
<label for="imageDescription" class="col-form-label">Description :</label>
<span id="imageDescription"></span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeImageDetailsModal();">Close</button>
</div>
</div>
</div>
</div>
<!-- 이미지 정보 모달 끝-->
</body>
</html>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/host.js" ></script>
2. host.js
- javascript에서 받은 form형태를 fetch의 비동기 통신을 통하여 post방법으로 controller로 보냅니다.
- 해당 $. fetch({}) 안에 존재하는 url부분은 홑따옴표('')가 아닌 빽틱(``)입니다.
- openImageDetailsModal() 함수는 이미지의 상세 보기 함수입니다.
$(document).ready(function () {
// HTML 문서를 로드할 때마다 실행합니다.
const principalId = $('#principalId').val();
console.log("principalId : ", principalId);
// 파일 업로드 모달 열기
$("#openModal").click(function () {
$("#imageUploadModal").modal("show");
});
// 모달이 숨겨질 때 form 초기화
$("#imageUploadModal").on("hidden.bs.modal", function () {
$("#imageUploadForm")[0].reset();
});
// 파일 업로드 폼 제출 시
$("#imageUploadForm").submit(function (e) {
e.preventDefault(); // 폼 기본 동작 방지
// FormData 생성
var formData = new FormData(this);
// POST 요청 보내기
$.ajax({
type: "POST",
url: `/image/host/${principalId}`,
data: formData,
processData: false,
contentType: false,
success: function (data) {
// 업로드 성공 시 실행할 코드
console.log("Upload successful", data);
$("#imageUploadModal").modal("hide"); // 모달 닫기
},
error: function (error) {
// 업로드 실패 시 실행할 코드
console.error("Upload failed", error);
},
});
});
// ... (나머지 코드 생략)
});
// 여기는 잘 되니까 냅둠
function openImageDetailsModal(imageId, username) {
console.log("도달 잘하는지?");
// 이미지 ID를 문자열로 전달
console.log("imageid : ", imageId);
console.log("username : ", username);
// ajax통신은 안되고 fetch수행은 됨.
// 이유는 잘모르겠음 cross-origin?일수도
fetch(`/api/user/${imageId}/host`)
.then(response => {
if (response.ok) {
return response.json(); // JSON 데이터를 파싱
}
throw new Error("Network response was not ok");
})
.then(data => {
console.log("Data received:", data);
$('#imageUsername').text(username);
$('#imageTitle').text(data.data.title);
$('#imageDescription').text(data.data.description);
$('#imageDetailsModal').modal('show');
})
.catch(error => {
alert("해당사항 접근 권한 제어 또는 json반환데이터 오류");
// 추후 변경예정
console.error("Error:", error);
});
}
// 모달을 닫기 위한 코드
function closeImageDetailsModal() {
$('#imageDetailsModal').modal('hide');
}
3. application.yml
- springboot를 실행할 때 기본적으로 필요한 요소를 적음
- datasource는 mariadb를 직접 지정해야 사용할 수 있음
- 변경점 없음
server:
port: 8080(자신이 7070으로 설정했다면 localhost:7070으로 들어가야함)
servlet:
context-path: /
encoding:
charset: utf-8
enabled: true
spring:
mvc:
view:
prefix: (src/main밑에 있는 폴더명에 저장)
suffix: .jsp
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:(mybatis 연결포트)/(자신이 연결한 데이터베이스 이름)?serverTimezone=Asia/Seoul
username: (자신이 만든 아이디)
password: (자신이 만든 비밀번호)
jpa:
open-in-view: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
servlet:
multipart:
enabled: true
max-file-size: 2MB
security:
user:
name: test
password: 1234
4. ImageController.java
- ImageController.java에서! imageUploadDto.getFile(). isEmpty()일 때
- 오류상황을 표기하기 위해 exception처리를 합니다.
- showImageList(Model) : 모든 사진리스트의 출력을 위해 만든 메서드입니다.
- model.addAtrribute(String, List <Image>)의 파라미터 String은 jsp에서 받는 파라미터의 이름과 일치해야 합니다.
- imageUpload(ImageUploadDto, PrincipalDetails) : 모든 사진리스트의 출력을 위해 만든 메서드입니다.
- 오류상황을 표기하기 위해 exception처리를 합니다.
package com.pjh.web;
import java.util.List;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.pjh.config.auth.PrincipalDetails;
import com.pjh.domain.image.Image;
import com.pjh.handler.ex.CustomValidationException;
import com.pjh.service.ImageService;
import com.pjh.web.dto.image.ImageUploadDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class ImageController {
private final ImageService imageService;
@GetMapping("/image/host")
public String showImageList(
Model model) {
List<Image> images = imageService.사진리스트();
model.addAttribute("images", images);
return "/image/host";
// userController에서 관리할 필요가 없음
}
@PostMapping("/image/host/{principalId}")
public String imageUpload(ImageUploadDto imageUploadDto,
@AuthenticationPrincipal PrincipalDetails principalDetails) {
// 호스트 페이지의 이미지 첨부
// 추후 변경 예정.. 객체의 역할에 부합하지 않음.
if(imageUploadDto.getFile().isEmpty()) {
throw new CustomValidationException("이미지가 첨부되지 않았습니다.", null);
}
int principalId = principalDetails.getUser().getId();
imageService.사진업로드(imageUploadDto, principalDetails);
return "redirect:/image/host";
// 파일을 리턴하기 때문에 apiContrller메 사용하지 않음.
}
}
5. CustomValidationException.java
- ImageController.java에서! imageUploadDto.getFile(). isEmpty()일 때
오류상황을 표기하기 위해 exception처리를 합니다.
package com.pjh.handler.ex;
import java.util.Map;
public class CustomValidationException extends RuntimeException{
// 객체 구분 할때
private static final long serialVersionUID = 1L;
private Map<String, String> errorMap;
public CustomValidationException(String message, Map<String,String> errorMap){
super(message);
this.errorMap = errorMap;
}
public Map<String, String> getErrorMap(){
return this.errorMap;
}
}
6. ControllerExceptionHandler.java
- 모든 Exception을 처리해 주는 핸들러입니다.
- CustomValidationException을 새로 정의했기에 새롭게 만들어줍니다.
package com.pjh.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import com.pjh.handler.ex.CustomApiException;
import com.pjh.handler.ex.CustomValidationApiException;
import com.pjh.handler.ex.CustomValidationException;
import com.pjh.util.Script;
import com.pjh.web.dto.CMRespDto;
@RestController // 데이터 리턴
@ControllerAdvice // 모든 exception을 낚아챔
public class ControllerExceptionHandler {
@ExceptionHandler(CustomValidationException.class)
// RuntimeException이 발생하면 모든 페이지가 낚아챔
public String validationException(CustomValidationException e) {
return Script.back(e.getErrorMap().toString());
//return new CMRespDto(-1, e.getMessage(), e.getErrorMap());
// 제네릭 타입 리턴은 <?>가 편하다
// 응답에 실패했고 에러메세지와 각각의 fieldError와 defaultMessage 리턴
}
@ExceptionHandler(CustomValidationApiException.class)
public ResponseEntity<?> validationApiException(CustomValidationApiException e){
return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), e.getErrorMap()),HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(CustomApiException.class)
public ResponseEntity<?> apiException(CustomApiException e){
return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(),null), HttpStatus.BAD_REQUEST);
}
}
7. CMRespDto.java
- 해당 파일은 공통적으로 발생하는 응답에 대한 dto입니다.
- 변경점 없음
package com.pjh.comapny.web.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto<T> {
private int code; // 1(성공), -1(실패)
private String message;
private T data;
}
8. ImageUploadDto.java
- host.jsp에서 form형식에 담긴 title, description, multipartfile을 처리하기 위해 만들어진 dto입니다.
package com.pjh.web.dto.image;
import org.springframework.web.multipart.MultipartFile;
import com.pjh.domain.image.Image;
import com.pjh.domain.user.User;
import lombok.Data;
@Data
public class ImageUploadDto {
private MultipartFile file;
private String title;
private String description;
private String postImageUrl;
public Image toEntity(String postImageUrl, User user) {
return Image.builder()
.title(title)
.description(description)
.postImageUrl(postImageUrl) // uuid_getFile().getOriginalFileName()
.user(user)
.build();
}
}
9. ImageService.java
- @Service 비즈니스 로직을 구현을 위해 생성하는 service 어노테이션입니다.
- @Transactional DB에 트랜잭션(일의 최소 단위) 처리를 springboot에서 구현한 형태로 수행할 수 있게 합니다.
- UUID를 지정하여 같은 이미지 파일의 업로드 중복을 엄청나게 높은 확률로 제거합니다.
ex) 1.jpg가 업로드가 중복될 수 있기에 UUID.getRandom()를 활용하여
e5 ab7114-de57-4ae5-9963-93351602 bb72_1.jpg로 저장하게 만듦.
package com.pjh.service;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.pjh.config.auth.PrincipalDetails;
import com.pjh.domain.image.Image;
import com.pjh.domain.image.ImageRepository;
import com.pjh.web.dto.image.ImageUploadDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // DI
@Service
public class ImageService {
private final ImageRepository imageRepository;
@Value("${file.path}")
private String uploadFolder;
// application.yml에 해당하는 file : path :로 들어옴
public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails) {
UUID uuid = UUID.randomUUID();
String imageFileName = uuid+"_"+imageUploadDto.getFile().getOriginalFilename();
// 업로드하려는 파일 네임이 String으로 들어옴
System.out.println("이미지 파일이름 : "+imageFileName);
Path imageFilePath = Paths.get(uploadFolder+imageFileName);
// 실제 저장 장소
try {
Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
}catch(Exception e) {
e.printStackTrace();
}
// 통신이 일어나거나 I/O이 일어날때 -> 예외가 발생할 수 있음.
Image image = imageUploadDto.toEntity(imageFileName, principalDetails.getUser());
// toEntity() -> db에 해당 toEntity화한 imageFileName, princiaplDetails.getUser()를 저장하겠다.
// db에 업로드를 진행한 유저아이디 + 업로드한 파일을 저장
imageRepository.save(image);
// System.out.println(imageEntity.toString());
// 무한 참조 조심, User객체에 Image -> Image객체에 User
}
}
10. ImageRepository
- DB와 직접 맞닿아 있는 인터페이스입니다.
- DB에 처리할 SQL문을 직접 사용할 수 있고 extends 하여 JpaRepository의 crud형태의 기본적인 연산이 지원됩니다.
package com.pjh.domain.image;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ImageRepository extends JpaRepository<Image, Integer>{
}
11.WebMvcConfig.java
- web에 대한 설정 파일
- @Value("${}")에 해당하는 파라미터는 ImageService.java와 동일해야 하며,
application.yml의 파일경로가 존재해야 하고 경로 안에 해당 폴더가 있어야 함.
- jsp파일에서 /upload/**로 시작하는 img src문이 있다면 함수 호출이 됨.
- setCachePeriod(60*10*6) 1시간 캐시
package com.pjh.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer{ // web설정파일
@Value("${file.path}")
private String uploadFolder;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
registry
.addResourceHandler("/upload/**") // jsp페이지에서 /upload/** 패턴이 나오면 함수 호출
.addResourceLocations("file:///"+uploadFolder)
.setCachePeriod(60*10*6) // 1시간 cache
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
// 이거 없으면 이미지 onerror화면 출력
}
오류 해결
1. 이미지 업로드 및 details 버튼을 클릭시에 모달 외부, 내부의 흐린한 화면 및 클릭 비활성화
1) modal에 해당하는 div태그를 맨 밑으로 내린다.
2) modal에 해당하는 style 속성(부모 속성 포함)을 position을 하나하나씩 주석처리하여 확인
3) 이도 안되면 .modal-backdrop에 해당하는 z-index를 절대적으로 낮춘다.(bootstrap 환경만 사용)
.modal-backdrop {
z-index: -1;
}
'Springboot' 카테고리의 다른 글
Spring boot의 인증 방식 (0) | 2023.11.10 |
---|---|
스프링 유저 프로필 사진 (5) | 2023.11.09 |
스프링 메모장 (1) | 2023.11.01 |
스프링 이미지 업로드(1) (1) | 2023.10.30 |
스프링 회원 수정 (1) | 2023.10.23 |