저번시간에는 주문하기, 주문목록보기, 주문상세보기 기능을 구현했었습니다.
이번에는 주문과 연관된 기능중 하나인 리뷰 기능에 대해 구현해보도록 하겠습니다
일반적으로 리뷰는 주문이 완료된후 작성이 가능해야하므로 주문목록페이지에서
리뷰작성버튼을 통해 작성이 가능하도록 하겠습니다
제일 먼저 DB에 리뷰데이터를 저장할 테이블을 하나 생성해주도록 합시다
CREATE TABLE DL_REVIEW (
ORDER_NUM NUMBER PRIMARY KEY,
STORE_ID NUMBER NOT NULL,
REVIEW_CONTENT VARCHAR2(3000) NOT NULL,
BOSS_COMMENT VARCHAR2(3000),
REGI_DATE TIMESTAMP DEFAULT SYSDATE,
USER_ID NUMBER NOT NULL,
SCORE NUMBER NOT NULL,
REVIEW_IMG VARCHAR2(200)
);
ALTER TABLE DL_REVIEW
ADD CONSTRAINT REVIEW
FOREIGN KEY (ORDER_NUM)
REFERENCES DL_ORDER_USER(ORDER_NUM)
on delete cascade;
BOSS_COMMENT는 사장님 댓글로 유저의 리뷰가 댓글이라면 대댓글이라고 생각하시면 됩니다
REVIEW_IMG에는 리뷰작성시 업로드한 이미지의 주소가 저장됩니다
테이블을 생성했으니 사용자로부터 입력받은 데이터를 이 테이블에 저장할수 있도록
Dto를 하나 생성해 주겠습니다
@Data
public class ReviewDto {
private String orderNum;
private long storeId;
private String storeName;
private String reviewContent;
private String bossComment;
private Date regiDate;
private float score;
private String reviewImg;
private MultipartFile file;
private long userId;
private String username;
private String nickname;
}
리뷰작성시 사용자로부터 입력받는 데이터는 별점과 리뷰내용, 사진이며 이 데이터와 함께
주문번호와 가게번호가 Dto에 담아져 넘어오게 됩니다. 즉 bossComment와 username
nickname은 필요없지만 이 Dto는 리뷰작성뿐만 아니라 화면에 리뷰들을 출력해줄때도
사용할것이기에 미리 포함시켰습니다. 리뷰에는 사진을 올릴수 있기 때문에 Dto에
MultipartFile를 반드시 포함시켜야 합니다
이제 이 Dto에 담긴 데이터를 DB에 저장시키기 전에 한가지 해야할 작업이 있습니다
우리는 현재 사용자가 업로드한 사진의 대한 정보를 MultipartFile file
에
저장하고 있습니다 하지만 우리가 DB에 저장할 정보는 파일이 저장된 주소인
String reviewImg
입니다 따라서 MultipartFile file
에 담긴 파일을
특정 폴더안에 저장시킨후 그 파일이 저장된 주소를 String reviewImg
에 넣어줘야합니다
저는 사용자가 올린 리뷰사진들을 c:/delivery/upload/ 안에 저장할겁니다 하지만 이때
문제가 하나 있는데 스프링부트는 기본적으로 정적파일을 불러올때 현재 패키지의
static폴더를 참조하기 때문에 view에서 img src="C:/delivery/upload/image.jpg"
식으로 불러오려고 해도 static폴더안에서의 해당주소를 찾기 때문에 해당 이미지 파일을
불러올수가 없습니다 따라서 프로젝트 외부폴더를 참조할수 있도록 설정을 해줘야합니다
이 작업을 위해 config패키지안에 설정파일을 하나 추가해주겠습니다
@Configuration
public class WebMvcConfig implements WebMvcConfigurer{
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**").addResourceLocations("file:///C:/delivery/upload/");
}
}
addResourceHandler는 스프링에서 확인할 위치를 나타내고 addResourceLocations는
실제 시스템의 폴더 위치를 나타내는데 예를들어 실제 파일은
c:/insta/upload/image.jpg" 라면 view에서 이를 불러오기 위해서는
img src="/upload/image.jpg" 라고 써줘야 합니다
이제 MultipartFile file
에 저장된 파일정보를 외부폴더에 저장하기 위해
utils패키지안에 클래스를 하나 추가해주겠습니다
@Component
public class FileUpload {
public boolean uploadReviewImg(ReviewDto reviewDto) {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"));
String uploadForder= Paths.get("C:", "delivery", "upload").toString();
String imageUploadForder = Paths.get("reviewImg", today).toString();
String uploadPath = Paths.get(uploadForder, imageUploadForder).toString();
File dir = new File(uploadPath);
if (dir.exists() == false) {
dir.mkdirs();
}
UUID uuid = UUID.randomUUID();
String reviewImgName = uuid+"_"+reviewDto.getFile().getOriginalFilename();
try {
File target = new File(uploadPath, reviewImgName);
reviewDto.getFile().transferTo(target);
} catch (Exception e) {
return false;
}
reviewDto.setReviewImg("upload\\"+imageUploadForder+"\\"+reviewImgName);
return true;
}
}
저는 C:\delivery\upload\reviewImg 폴더안에 리뷰사진을 저장할것입니다
이때 한폴더안에 모든 파일을 다 넣어버리면 파일들이 점점 쌓이면서 이 폴더를 참조할시
시간이 오래 걸리게 됩니다. 그러므로 현재 날짜를 기준으로 폴더를 만들어 그안에 파일을
저장하면 각각의 폴더를 참조할때 시간을 최소화 할수가 있습니다
LocalDate.now()와 pattern을 통해 현재 날짜를 기준으로 폴더 이름을 생성합니다
uploadForder에는 업로드 파일이 저장될 최상위주소인 c:\delivery\upload가
imageUploadForder는 현재날짜를 기준으로 리뷰이미지가 저장될 주소가 들어가게 됩니다
paths.get을 사용하면 경로구분자를 넣어주지 않아도 알아서 경로로 만들어줍니다
이제 uploadForder 와 imageUploadForder를 합쳐주면
C:\delivery\upload\reviewImg\220712와 같은 주소가 만들어집니다
위의 주소를 가진 폴더가 존재하지 않으면 dir.mkdirs()를 통해 폴더를 생성합니다
그 후 해당 경로에 파일을 저장하면 되는데 이때 파일의 이름으로 저장하게 되면
이미 같은 이름의 파일이 있을시 그 파일을 덮어쓰게 되므로
랜덤값을 만들어내는 uuid를 통해서 랜덤값+파일이름으로 해당 파일을 저장해줍니다
파일I/O는 예외가 발생할수 있으므로 try catch문으로 감싸줘야하며 만약 예외 발생시에는
false를 반환하여 작업에 실패하였음을 알려주겠습니다
이제 파일 저장이 완료되면 Dto안에 ReviewImg에 파일이 저장된 주소를 저장하면 되는데
우리가 필요한건 c:\delivery\upload\..\..
중에 upload부터이므로
"upload\\"+imageUploadForder+"\\"+reviewImgName
를 저장해주면 됩니다
//리뷰 작성
@PostMapping("/api/review")
public ResponseEntity<String> reviewWrite(ReviewDto reviewDto, @AuthenticationPrincipal CustomUserDetails principal){
reviewDto.setUserId(principal.getId());
if(storeService.reviewWrite(reviewDto))
return ResponseEntity.ok().body("리뷰 작성 완료");
return ResponseEntity.badRequest().body("파일 저장 실패");
}
//리뷰 수정
@PutMapping("/api/review")
public ResponseEntity<String> reviewModify(ReviewDto reviewDto, @AuthenticationPrincipal CustomUserDetails principal){
System.out.println(reviewDto.toString());
reviewDto.setUserId(principal.getId());
if(storeService.reviewModify(reviewDto))
return ResponseEntity.ok().body("리뷰 수정 완료");
return ResponseEntity.badRequest().body("파일 저장 실패");
}
@AuthenticationPrincipal를 통해 현재 로그인된 사용자의 ID를 Dto에 넣어줍니다
만약 File I/O 작업에 실패할경우 false값이 넘어올테고 이때 badRequest()를
성공시에는 ok()를 응답해줍니다 badRequest()의 경우 body에 파일 저장 실패 메시지를
심어주었는데 File I/O의 실패시에만 해당되며 DB작업에 실패할경우는 해당되지 않습니다
그 이유에 대해서는 나중에 예외처리에 대한 포스팅에서 설명하도록 하겠습니다
@Autowired
FileUpload fileUpload;
//리뷰작성
@Transactional
public boolean reviewWrite(ReviewDto reviewDto) {
if(reviewDto.getFile() == null) {
reviewDto.setReviewImg("");
}
else {
if(!fileUpload.uploadReviewImg(reviewDto))
return false;
}
storeMapper.reviewWrite(reviewDto);
return true;
}
//리뷰수정
@Transactional
public boolean reviewModify(ReviewDto reviewDto) {
if(reviewDto.getFile() == null) {
reviewDto.setReviewImg("");
}
else {
if(!fileUpload.uploadReviewImg(reviewDto))
return false;
}
storeMapper.reviewModify(reviewDto);
return true;
}
리뷰를 작성할때 사용자는 사진을 올릴수도 있고 올리지 않을수도 있기 때문에
만약 아무 사진도 올리지 않았을경우에는 ReviewImg에 빈값을 입력해줍니다
사용자가 업로드한 파일이 존재할 경우에는 위에서 우리가 작성한 uploadReviewImg를
통해 파일을 외부폴더에 저장하게 됩니다
//리뷰 작성
public void reviewWrite(ReviewDto reviewDto);
//리뷰 수정
public void reviewModify(ReviewDto reviewDto);
<insert id="reviewWrite">
INSERT INTO DL_REVIEW (
ORDER_NUM
,STORE_ID
,REVIEW_CONTENT
,USER_ID
,SCORE
,REVIEW_IMG
) VALUES (
${orderNum }
,#{storeId }
,#{reviewContent }
,#{userId}
,#{score}
,#{reviewImg }
)
</insert>
<update id="reviewModify">
UPDATE DL_REVIEW SET
REVIEW_CONTENT = #{reviewContent }
,SCORE = #{score}
<if test="reviewImg != null">
,REVIEW_IMG = #{reviewImg }
</if>
WHERE
ORDER_NUM = ${orderNum }
</update>
여기까지 하면 리뷰 작성과 수정에 대한 구현이 모두 완료되었습니다
이제 매장화면에서 해당 매장에 작성한 모든 리뷰를 확인할수 있도록
리뷰탭에 모든 데이터를 뿌려줘야 합니다 기존 StoreDetail에 관련된
모든 부분에 리뷰정보를 추가해주도록 합시다
private List<ReviewDto> reviewList;
@Transactional
public StoreDetailDto storeDetail(long storeId) {
StoreDto storeDto = storeMapper.storeDetail(storeId);
List<FoodDto> foodList = storeMapper.foodList(storeId);
//추가
List<ReviewDto> reviewList = storeMapper.reviewList(storeId);
//수정
return new StoreDetailDto(storeDto, foodList, reviewList);
}
//리뷰 목록
public List<ReviewDto> reviewList(long id);
<select id="reviewList" resultType="com.han.delivery.dto.ReviewDto">
SELECT
r.order_num,
r.store_id,
r.review_content,
r.boss_comment,
r.regi_date,
r.score,
r.review_img,
r.user_id,
u.nickname
FROM
dl_review r
LEFT JOIN
dl_user u
ON
r.user_id = u.id
WHERE
r.store_id = #{id}
ORDER BY
regi_date DESC
</select>
이제 리뷰목록을 뿌려주기 위해 DB로부터 리뷰정보를 가져오는 부분은 끝이났습니다
마지막으로 매장의 상세정보 화면에서 현재 리뷰가 몇개인지 평점 평균이 몇점인지
사장님 댓글은 몇개인지등을 나타내기 위한 작업을 해야합니다
기존 StoreDto에 다음의 코드를 추가해줍시다
private float score;
private int orderCount;
private int reviewCount;
private int bossCommentCount;
private int likesCount;
private int score1; // 리뷰 1점
private int score2; // 리뷰 2점
private int score3; // 리뷰 3점
private int score4; // 리뷰 4점
private int score5; // 리뷰 5점
//영업중
private String isOpen;
isOpen
는 현재 선택한 매장이 영업중인지 아닌지를 판단하기 위한 변수입니다
<select id="storeDetail" resultType="com.han.delivery.dto.StoreDto">
SELECT RESULT.*
,CASE WHEN TO_CHAR(SYSDATE,'HH24') BETWEEN OPENING_TIME AND CLOSING_TIME THEN 'true' ELSE 'false' END IS_OPEN
FROM (SELECT S.*,
C.*
FROM DL_STORE S
,(SELECT * FROM
(SELECT ROUND(AVG(SCORE),1) SCORE
,COUNT(REVIEW_CONTENT) REVIEW_COUNT
,COUNT(BOSS_COMMENT) BOSS_COMMENT_COUNT
,COUNT(CASE WHEN SCORE=1 THEN 1 END) SCORE1
,COUNT(CASE WHEN SCORE=2 THEN 1 END) SCORE2
,COUNT(CASE WHEN SCORE=3 THEN 1 END) SCORE3
,COUNT(CASE WHEN SCORE=4 THEN 1 END) SCORE4
,COUNT(CASE WHEN SCORE=5 THEN 1 END) SCORE5
FROM DL_REVIEW WHERE STORE_ID = #{id } )
,(SELECT SUM(ORDER_COUNT) ORDER_COUNT FROM (
SELECT COUNT(*) ORDER_COUNT FROM DL_ORDER_USER WHERE STORE_ID = #{id }
UNION ALL
SELECT COUNT(*) ORDER_COUNT FROM DL_ORDER_NON_USER WHERE STORE_ID = #{id } ))
) C
WHERE ID = #{id }
) RESULT
</select>
가게, 리뷰, 주문 테이블을 조인 후 리뷰의 각 별점 수, 총점의 평균, 주문횟수를 출력합니다
CASE WHEN TO_CHAR...
부분은 현재시간이 영업시작시간과 영업종료시간 사이에 있을경우
true 아닐경우 false를 반환합니다