티스토리 뷰

이제 사진첩의 특정 글을 들어가면 상세보기를 할 수 있는 기능과 화면을 구현해보겠습니다.

 

[ 전체코드 ] 

GalleryView.js

더보기
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import 'bootstrap/dist/css/bootstrap.min.css';
import { Button, Card } from 'react-bootstrap';

const GalleryView = () => {
    const { id } = useParams(); // URL에서 id 파라미터를 가져옴
    const [imageData, setImageData] = useState(null); // 이미지 데이터 상태 변수

    useEffect(() => {
        // id를 사용하여 이미지의 상세 정보를 가져오는 API 호출
        axios.get(`http://localhost:8082/api/v1/auth/y/gallery/${id}`)
            .then((response) => {
                setImageData(response.data); // 응답 데이터 설정
            })
            .catch((error) => {
                console.error("Error:", error);
                alert("이미지 상세 정보 불러오기 실패");
            });
    }, [id]); // 의존성 배열에 id를 포함하여 id가 변경될 때마다 useEffect가 실행되도록 설정

    // 이미지 데이터가 로드되지 않았을 경우 로딩 상태 표시
    if (!imageData) {
        return <div>Loading...</div>;
    }

    // 이미지 상세 정보를 보여주는 컴포넌트 반환
    return (
        <>
        <h3 style={{textAlign: 'center', marginTop: '20px'}}>📷사진첩 상세보기</h3>
        <div className="container">
            <Card style={{ width: '18rem', margin: '20px auto' }}>
                <Card.Img variant="top" src={imageData.gall_img} alt="Gallery Image" />
                <Card.Body>
                    <Card.Title>{imageData.gall_date}</Card.Title>
                    <Card.Text>
                        {imageData.fileName}
                    </Card.Text>
                    {/* 상세보기 페이지에서 뒤로 가기 버튼 */}
                    <Button variant="primary" onClick={() => window.history.back()}>뒤로 가기</Button>
                </Card.Body>
            </Card>
        </div>
        </>
    );
}

export default GalleryView;

 

App.js

더보기
<Route exact path='/galleryView/:id' element={<GalleryView/>}></Route>

 

GalleryController.java

더보기
package com.example.demo.controller;

import com.example.demo.dto.GalleryDTO;
import com.example.demo.entity.GalleryEntity;
import com.example.demo.service.GalleryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

@RestController
@RequestMapping("/api/v1/auth/y")
public class GalleryController {

	@Autowired
	GalleryService galleryService;

	/**
	 * 기능 : 사진첩 작성  ( 훈련사 계정 )
	 * url : /createGallery
	 * request data : 날짜(글제목), 글 내용, 작성 날짜, 작성 시간
	 * response data : 사진첩 작성 성공
	 */
	@PostMapping("/createGallery")
	public List<GalleryDTO> createGallery(@RequestPart (value = "file", required = false) List<MultipartFile> uploadFiles) {
		return galleryService.createGallery(uploadFiles);
	}


	
	/**
	  기능 : 사진첩 수정 ( 훈련사 계정 )
	  url : /updateGallery
	  request data : 날짜(글제목), 글 내용, 작성 날짜, 작성 시간
	  response data : 사진첩 수정 성공
	 */
	
	
	/**
	  기능 : 사진첩 삭제 ( 훈련사 계정 )
	  url : /deleteGallery
	  request data : 특정 id의 사진첩 삭제
	  response data : 사진첩 삭제 성공
	 */
	
	
	/**
	  기능 : 사진첩 목록
	  url : /gallery
	  request data : 
	  response data : 사진첩 목록
	 */
	@GetMapping("/gallery")
	public List<GalleryDTO> gallery() {
		return galleryService.galleryList();
	}
	
    /**
	  기능 : 사진첩 상세보기
	  url : /galleryDetail
	  request data : 특정 날짜(글제목), 글 내용, 작성 날짜, 작성 시간
	  response data : 해당 날짜(글제목), 글 내용, 작성 날짜, 작성 시간
	 */
	
	@GetMapping(value = "/galleryView", produces={MediaType.IMAGE_PNG_VALUE, MediaType.IMAGE_JPEG_VALUE})
	public ResponseEntity<byte[]> findById(@RequestParam("id") Long id) throws IOException {
		// id에 해당하는 이미지 정보를 서비스로부터 가져옴
		GalleryEntity galleryEntity = galleryService.findById(id);
		// 파일 경로를 이용하여 이미지 파일을 읽기 위한 InputStream을 생성
		InputStream inputStream = new FileInputStream(galleryEntity.getGallImg());
		//  InputStream에서 모든 바이트를 읽어와 byte 배열에 저장
		byte[] bytes = inputStream.readAllBytes();
		// 스트림을 닫아 리소스를 해제
		inputStream.close();
		// 이미지를 byte 배열로 변환하여 ResponseEntity에 담아서 반환
		return new ResponseEntity<byte[]>(bytes, HttpStatus.OK);
 	}
	
	
}

 

GalleryService.java

더보기
package com.example.demo.service;

import com.example.demo.dto.GalleryDTO;
import com.example.demo.entity.GalleryEntity;
import com.example.demo.mapper.GalleryMapper;
import com.example.demo.repository.GalleryRepository;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Date;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;


@Service
@Log4j2
public class GalleryService {
	
	@Autowired
	private GalleryRepository galleryRepository;

	// 업로드 할 위치
	@Value("${part.upload.path}")
	private String uploadPath;
    private String fileExtension;
    private Path savePath;

    // 사진첩 작성
	public List<GalleryDTO> createGallery(List<MultipartFile> uploadFiles) {
        List<GalleryDTO> galleryDTOList = new ArrayList<>();

       // Path savePath=null;
        //String fileExtension=null;

        for (MultipartFile uploadFile : uploadFiles) {
            // 이미지 파일만 업로드
            if (!Objects.requireNonNull(uploadFile.getContentType()).startsWith("image")) {
                log.warn("this file is not an image type");
                continue;
            }

            // 파일명: 업로드 된 파일의 원래 파일 이름을 추출
            String originalName = uploadFile.getOriginalFilename();

            // 마지막으로 온 "/"부분으로부터 +1 해준 부분부터 출력
            // originalName이 "/path/to/file/example.txt"와 같은 문자열이라면, "example.txt" 부분만 (파일명만) 추출
            String fileName = originalName.substring(originalName.lastIndexOf("//") + 1);
            log.info("fileName: " + fileName);

			// 파일 확장자
            fileExtension = StringUtils.getFilenameExtension(originalName);

            // 날짜 폴더 생성
            String folderPath = makeFolder();

            // UUID
            String uuid = UUID.randomUUID().toString();

            // 저장할 파일 이름 중간에 "_"를 이용하여 구분
            String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + "_" + fileName;

            // Paths.get() 메서드는 특정 경로의 파일 정보를 가져옴 (경로 정의하기)
            savePath = Paths.get(saveName);

            try {
                // transferTo(file) : uploadFile에 파일을 업로드 하는 메서드
                uploadFile.transferTo(savePath);
                galleryDTOList.add(new GalleryDTO(fileName, uuid, folderPath));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        GalleryEntity galleryEntity = GalleryEntity.builder()
                .gallDate(Date.valueOf(LocalDate.now()))
                .gallImg(String.valueOf(savePath))
                .gallExtension(fileExtension)
                .build();

        galleryRepository.save(galleryEntity);

        return galleryDTOList;
    }

	// 날짜 폴더 생성
	public String makeFolder() {
		//LocalDate를 문자열로 포멧
		String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
		String folderPath = str.replace("/", File.separator);

		// make folder --------
		// File newFile= new File(dir,"파일명");
		// -> 부모 디렉토리를 파라미터로 인스턴스 생성
		File uploadPathFolder = new File(uploadPath, folderPath);

		// 만약 uploadPathFolder가 존재하지않는다면 makeDirectory하라는 의미
		// mkdir(): 디렉토리에 상위 디렉토리가 존재하지 않을경우에는 생성이 불가능한 함수
		// mkdirs(): 디렉토리의 상위 디렉토리가 존재하지 않을 경우에는 상위 디렉토리까지 모두 생성하는 함수
		if(!uploadPathFolder.exists()) {
			boolean mkdirs = uploadPathFolder.mkdirs();
			log.info("-------------------makeFolder------------------");
			log.info("uploadPathFolder.exists(): "+uploadPathFolder.exists());
			log.info("mkdirs: "+mkdirs);
		}

		return folderPath;

	}


// 사진첩 목록 조회
	// mapstruct 사용
	public List<GalleryDTO> galleryList() {
		List<GalleryEntity> galleryEntities = galleryRepository.findAll();   // 갤러리엔터티를 리스트에 저장
		return galleryEntities.stream()  // 스트림 사용
				.map(GalleryMapper.instance::galleryToDTO)  // Mapstruct 사용하여 갤러리 entity -> DTO로 변환
				.collect(Collectors.toList());  //  변환된 갤러리 DTO들을 리스트로 수집하여 반환
	}

    // 해당 유저의 사진첩 상세조회
    public GalleryEntity findById(Long id) {
        return galleryRepository.findById(id).get();
    }

}

 

 


 

 

 

[ 💣 403 에러 해결 ] 

💻 오류

상세보기를 들어가면 403 에러가 나고, alert("이미지 상세 정보 불러오기 실패") 알림이 뜨고, Loading...으로 표시되는 화면이 떴습니다.

 

💻 원인

403 에러는 보통 권한 관련 오류라고 생각되어 토큰을 잘 받아와 저장하고 있는지 확인하던 중에 어이없는 실수를 발견했습니다!

바로, 클라이언트 측의 axios 전송 코드에서 헤더에 토큰을 담는 부분을 빠트렸습니다…

(아마 axiosInstance를 구현하다가 빼먹지 않았을까 싶은…)

(참고로 axiosInstace는 아직 해결이 되지않아 적용을 못시켰습니다. 일단 기능들 완성부터 하고 마지막에 수정해서 적용시키는걸로ㅠㅠ)

 

💻 해결

헤더에 토큰 담는 부분을 추가해줬습니다!

 

💻 [ 결과 ]  

상세보기 페이지가 잘 렌더링 됩니다.

하지만 아직 이미지 렌더링은 안되어서 해당 이슈를 해결해야 합니다! 😓

 

 


 

 

[ 💣 이미지 렌더링 이슈 해결 ] 

콘솔창에 외계어 같은 부분이 이미지 입니다!

하지만 화면에는 렌더링이 되지 않고 있습니다.

 

💻 구현(해결)과정

React 컴포넌트에서 Spring에서 반환된 이미지를 Base64 인코딩하고 이를 img 태그의 src 속성에 설정하는 것이 핵심입니다.

 

1️⃣Base64 인코딩된 이미지 데이터를 저장할 상태 변수를 추가

let [imageSrc, setImageSrc] = useState(''); // Base64 인코딩된 이미지 데이터 상태 변수

 

 

2️⃣ 응답 데이터를 바이트 배열로 받기 위해 responseType을 'arraybuffer'로 설정

responseType: 'arraybuffer'

 

 

3️⃣ 바이트 배열 데이터를 Base64 문자열로 인코딩

     const base64 = btoa(
                new Uint8Array(response.data)
                    .reduce((data, byte) => data + String.fromCharCode(byte), '')
            );

 

 

4️⃣ 응답 헤더에서 Content-Type을 가져옵니다.

const contentType = response.headers['content-type'];

 

 

5️⃣ Base64 인코딩된 이미지 데이터 URL을 설정

setImageSrc(`data:${contentType};base64,${base64}`);

 

 

6️⃣ 갤러리 엔티티의 메타 데이터를 설정

  setImageData({
                gall_date: response.headers['gall_date'], // 헤더에서 날짜 정보를 가져옵니다.
                fileName: response.headers['file_name'] // 헤더에서 파일명 정보를 가져옵니다.
            });

 

 

7️⃣ Base64 인코딩된 이미지 데이터를 img 태그의 src 속성에 설정

 <img src={imageSrc} alt="Gallery Image" />

 

이렇게 하면 Spring에서 바이트 코드로 받아온 이미지를 React에서 Base64 인코딩하여 img 태그에 렌더링할 수 있습니다.

 

 

💻 [ 결과 ]  

드디어!!!!!!!

이미지가 렌더링 되었습니다……….😭

저 빨간 이미지 보일 때 진심으로 희열을 느낌……

진짜 별 거 아닌데도 쪼렙은……..ㅠㅠ

이미지 크기 조절할 생각도 안하고 한참을 기뻐했고요..

이제 이미지 크기 조절과 상세보기 UI 수정을 좀 해보겠습니다!

 

 

 

이미지 크기를 조절한 최종 모습입니다!

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함