[spring boot code] 프로필 이미지 업데이트

인철·2024년 3월 3일
1

Spring boot Code

목록 보기
3/3
post-thumbnail

회원가입할 때 프로필 사진도 같이 업데이트 하기


기본 Entity에 부모가 되는 BaseEntity.java 생성

@Getter
@SuperBuilder // 빌더 패턴을 구현하는 코드를 자동으로 생성
@MappedSuperclass // JPA의 어노테이션으로 이 클래스가 다른 클래스의 상위 클래스임을 지정
@NoArgsConstructor (access = PROTECTED) // 인자가 없는 생성자 자동 생성, PROTECTED로 하는 이유는 상속 구조에서 안전하게 사용하기 위함
@EntityListeners ( AuditingEntityListener.class)
@ToString //
@EqualsAndHashCode
public class BaseEntity {
	@Id
	@GeneratedValue (strategy = IDENTITY)
	@EqualsAndHashCode.Include
	private Long id;
	@CreatedDate
	private LocalDateTime createDate;
	@LastModifiedDate
	private LocalDateTime modifyDate;

	@Transient // 아래 필드가 DB 필드가 되는 것을 막는다.
	@Builder.Default
	private Map <String, Object> extra = new LinkedHashMap <> ();

	// 현재 객체의 클래스 이름을 가져와서 , 그 이름의 첫글자를 소문자로 변환한 후 나머지 이름을 그대로 붙어서 반환
	public String getModelName(){

		String simpleName = this.getClass ().getSimpleName ();
		// 현재 객체의 'class' 객체를 반환, class 객체로부터 단순한 이름을 문자열로 반환
		// ex) com.example.UserProfileImg 인 경우 UserProfileImg 을 반환한다
		return Character.toLowerCase ( simpleName.charAt ( 0 )) + simpleName.substring ( 1 ) ;
		// 첫 문자를 소문자로 반환한 후 클래스 이름의 첫 번째 문자를 제외한 나머지 문자열을 반환
		// ex) UserProfileImg > userProfileImg

	}

BaseEntity는 기본 Entity에서 공통적으로 가지고 있는 필드들을 모아놓은 것이고 부모 Entity가 되는 역할을 한다


이미지 관련 Entity인 GenFile.java 생성

@Entity
@Getter
@AllArgsConstructor(access = PROTECTED)
@NoArgsConstructor(access = PROTECTED)
@SuperBuilder // 상속받은 클래스의 필드를 포함하는 빌더 패턴을 구현
@ToString(callSuper = true)
@Table(indexes = {
		@Index ( name = "idx1", columnList = "relId, relTypeCode, typeCode, type2Code")
})
// JPA 엔티티에 대한 데이터베이스 테이블을 생성할 때 인덱스를 정의하는데 사용
// 엔티티 클래스에 정의 필들를 기반으로 데이터베이스 테이블에 인덱스를 추가할 수 있다
// name : 인덱스의 이름을 설정
// columList : 인덱스에 포함될 열의 이름을 쉼표로 구분하여 나열
public class GenFile extends BaseEntity {
	private String relTypeCode; // 타입 코드
	private long relId; // 식별자
	private String typeCode; // 파일 타입 코드(ex: img, png 등)
	private String type2Code; // 보조 파일 타입 코드
	private String fileExtTypeCode; // 파일 확장자 타입 코드
	private String fileExtType2Code; // 파일 확장자 보조 타입 코드
	private int fileSize; // 파일 크기
	private int fileNo; // 파일 번호
	private String fileExt; // 파일 확장자
	private String fileDir; // 파일이 저장된 디렉토리 경로
	private String originFileName; // 원본 파일명

	public String getFileName(){
		return getId () + "." + getFileExt ();
	}
	// 파일의 고유 식별자와 파일 확장자를 결합하여 파일 이름을 생성
	// ex) ID 가 123, 확장자가 jpg면 getFileName : 123.jpg 반환

	public String getUrl(){
		return "/gen/" + getFileDir () + "/" + getFileName ();
	}
	// 파일이 웹에서 접근 가능한 URL 경로 생성
	// 파일이 저장된 디렉토리와 파일 이름을 사용하여 URL 구성
	// ex) 파일 디렉토리 : healMingle , 파일 이름 : 123.jpg > /gen/healMingle/123.jpg 반환

	public String getDownloadUrl(){
		return "/download/gen/" + getId ();
	}
	// 파일을 다운로드하기 위한 URL 생성
	// 파일의 고육 식별자를 사용하여 다운로드 URL 구성
	// ex) 파일 ID : 123 이면 /download/gen/123 반환

	public String getFilePath(){
		return AppConfig.getGenFileDirPath () + "/" + getFileDir () + "/" + getFileName ();
	}
	// 파일의 시스템에서 실제 경로 생성
	// 애플리케이션의 설정에서 정의된 기본 파일 저장 경로 + 파일이 저장된 디렉토리 + 파일이름을 사용하여 경로를 구성
	// 기본 파일 저장 경로가 c:/files, 파일 디렉토리가 images, 파일 이름이 123.jpg라면, 이 메서드는 c:/files/images/123.jpg를 반환
}

getFileName, getUrl, getDownloadUrl, getFilePath 메서드는 회원가입을 이미지가 있고 회원가입이 완료가 되면 내가 지정한 경로에 풀더(디렉토리)가 생기면서 이미지가 생성이 될 때 보조해주는 메서드이다


GenFileService.java 생성

@Service
// 이 클래스를 스프링 서비스 계층의 컴포넌트로 선언합니다.
@RequiredArgsConstructor
// Lombok 라이브러리를 사용해 필수 생성자를 자동으로 생성합니다. 여기서는 genFileRepository 필드에 대한 생성자가 생성됩니다.
@Transactional(readOnly = true)
// 클래스 수준에서 트랜잭션 설정을 읽기 전용으로 설정합니다. 데이터 조회 작업에서 사용됩니다.
public class GenFileService {
	private final GenFileRepository genFileRepository;
	// GenFile 엔티티에 대한 CRUD 작업을 수행하는 레포지토리를 주입받습니다.

	@Transactional // save 메서드에 대한 트랜잭션을 설정합니다. 클래스 수준에서 설정한 readOnly=true를 오버라이드하여 이 메서드가 데이터 변경 작업을 포함한다는 것을 나타냅니다.
	public GenFile save(String relTypeCode, Long relId, String typeCode, String type2Code, int fileNo, MultipartFile multipartFile) {
		String originFileName = multipartFile.getOriginalFilename(); // 업로드된 파일의 원본 파일 이름을 가져옵니다.
		String fileExt = Ut.file.getExt(originFileName); // 파일 확장자를 추출합니다.
		String fileExtTypeCode = Ut.file.getFileExtTypeCodeFromFileExt(fileExt); // 파일 확장자로부터 파일 확장자 타입 코드를 가져옵니다.
		String fileExtType2Code = Ut.file.getFileExtType2CodeFromFileExt(fileExt); // 파일 확장자로부터 파일 확장자 타입2 코드를 가져옵니다.
		int fileSize = (int) multipartFile.getSize(); // 파일 크기를 가져옵니다.
		String fileDir = getCurrentDirName(relTypeCode); // 파일이 저장될 디렉토리 이름을 생성합니다.

		GenFile genFile = GenFile.builder() // GenFile 엔티티의 빌더를 사용하여 인스턴스를 생성합니다.
				.relTypeCode(relTypeCode)
				.relId(relId)
				.typeCode(typeCode)
				.type2Code(type2Code)
				.fileExtTypeCode(fileExtTypeCode)
				.fileExtType2Code(fileExtType2Code)
				.originFileName(originFileName)
				.fileSize(fileSize)
				.fileNo(fileNo)
				.fileExt(fileExt)
				.fileDir(fileDir)
				.build();

		genFileRepository.save(genFile); // 생성된 GenFile 엔티티를 데이터베이스에 저장합니다.

		File file = new File ( genFile.getFilePath() ); // GenFile 객체에서 파일이 저장될 전체 경로를 가져와 File 객체를 생성
		// genFile.getFilePath() : 메서드를 호출하여 파일이 저장될 경로 얻는다

		file.getParentFile ().mkdirs (); // 파잉리 저장되기 전에 파일이 저장될 부모 디렉터리(풀더)가 존재하는지 확인하고
		// 없으면 해당 경로에 풀더 생성
		// mkdirs : java.io.file 클래스의 메서드 중 하나로 지정된 경로에 풀더를 생성하는데 사용
		// 지정된 경로의 마지막 요소에 해당하는 다렉터리 뿐만 아니라 필요한 모든 부모 디렉토리도 함께 생성이 가능하다

		try {
			multipartFile.transferTo ( file );
			// multipartFile 객체를 사용하여 클라이언트로부터 받은 파일을 서버에 저장
		} catch ( IOException e ){
			throw new RuntimeException ( e );
			// 파일 전송 중 IOException 발생할 경우 런타임 에외로 포장하여 다시 던진다
		}

		return genFile; // 저장된 GenFile 엔티티를 반환합니다.
	}

	private String getCurrentDirName(String relTypeCode) { // 파일이 저장될 디렉토리 이름을 생성하는 메서드입니다.
		return relTypeCode + "/" + Ut.date.getCurrentDateFormatted("yyyy_MM_dd"); // 디렉토리 이름은 관계 타입 코드와 현재 날짜로 구성됩니다.
	}
}

GenFileRepository.java 생성

public interface GenFileRepository extends JpaRepository<GenFile, Long > {

	List < GenFile > findByRelTypeCodeAndRelIdOrderByTypeCodeAscType2CodeAscFileNoAsc ( String relTypeCode, Long relId );
	// 여러 개의 리스트 형태로 오름차순으로 정렬하여 조회후 반환

	Optional < GenFile > findByRelTypeCodeAndRelIdAndTypeCodeAndType2CodeAndFileNo ( String relTypeCode, long relId, String typeCode, String type2Code, int fileNo );
	// relTypeCode, relId, typeCode, type2Code, fileNo에 정확히 일치하는 하나의 GenFile 엔티티 조회

	List < GenFile > findAllByRelTypeCodeAndRelIdInOrderByTypeCodeAscType2CodeAscFileNoAsc ( String relTypeCode, long[] relIds );
	// relTypeCode와 relId 배열에 포함된 아이디들에 해당하는 GenFile 엔티티들을 typeCode, type2Code, fileNo의 오름차순으로 정렬하여 조회


}

List와 Optional의 차이를 알아야 한다
List는 복수를 다룰 때 사용하고 Optional는 단일 데이터를 다룰 때 사용하고 null 처리를 부드럽게 할 수 있다


Ut.java에 file, date class 추가하기

public static class date{
		// 'date' 정적 내부 클래스느 날짜 관련 유틸리티 메서드를 제공

		public static String getCurrentDateFormatted(String pattern){
			// 주어진 패턴에 따라 현재 날짜를 포맷하여 문자열로 반환하는 메서드
			SimpleDateFormat simpleDateFormat = new SimpleDateFormat ( pattern );
			// simpleDateFormat 객체 생성
			return simpleDateFormat.format ( new Date (  ) );
			// 현재 날짜를 주어진 패턴으로 포멧
		}

	}

	public static class file{
	// 파일 처리 관련 유틸리티 메서드

		public static String getExt(String filename) {
			// 주어진 파일 이름에서 확장자를 추출하여 반환하는 메서드
			return Optional.ofNullable ( filename ) // filename을 Optional 객체로 변환
					.filter ( f -> f.contains ( "." ) ) // 파일 이름에 "."이 포함되어 있는지 확인
					.map ( f -> f.substring ( filename.lastIndexOf ( "." ) + 1 ).toLowerCase () )
					// 마지막 '.' 이후의 문자열(확장자)를 추출하고 소문자로 변환합니다.
					.orElse ( "" );
					// 확장자를 찾을 수 없는 경우 빈 문자열을 반환합니다.
		}

		public static String getFileExtTypeCodeFromFileExt(String ext) {
			// 파일 확장자에 따라 파일 유형 코드를 반환하는 메서드

			switch ( ext ){
				case "jpeg":
				case "jpg":
				case "gif":
				case "png":
					return "img";   // 이미지 파일인 경우 "img"를 반환합니다.
				case "mp4":
				case "avi":
				case "mov":
					return "video"; // 비디오 파일인 경우 "video"를 반환합니다.
				case "mp3":
					return "audio"; // 오디오 파일인 경우 "audio"를 반환합니다
			}
			return "etc";   // 위의 경우에 해당하지 않는 다른 확장자인 경우 "etc"를 반환합니다
		}

		public static String getFileExtType2CodeFromFileExt(String ext){
			// 파일 확장자에 따라 또 다른 파일 유형 코드를 반환하는 메서드

			switch ( ext ){
				case "jpeg":
				case "jpg":
					return "jpg"; // "jpeg" 또는 "jpg" 확장자인 경우 "jpg"를 반환
				case "gif", "png", "mp4", "mov", "avi", "mp3":
					return ext; // 주어진 확장자를 그대로 반환
			}
			return "etc"; // 위의 경우에 해당하지 않는 다른 확장자인 경우 "etc"를 반환
		}

	}

파일 확장자와 파일이름을 추출하는 메서드와 날짜 관련 메서드를 추가한다


MemberController.java에 profileImg 관련 코드 추가하기

// joinForm에 profileImg 추가하기
	@Getter
	@AllArgsConstructor
	public static class JoinForm {
		@NotBlank
		private String username;
		@NotBlank
		private String password;
		@NotBlank
		private String nickname;
		@NotBlank
		private String email;
		private MultipartFile profileImg;
		@NotNull
		private Jop jop; // 직업을 나타내는 열거형

	}
    
// join 메서드에 profileImg 추가하기
	@PostMapping("/join") // usr/member/join 으로 POST 요청이오면 실행
	public String join( @Valid JoinForm joinForm) { // 클라이언트로부터 전달받은 JoinForm 객체 검증
		RsData<Member> joinRs = memberService.join(joinForm.getUsername(), joinForm.getPassword(), joinForm.getNickname(), joinForm.getEmail (), joinForm.getProfileImg (),joinForm.getJop ());
		// MemberService를 통해 가입을 수행하고 결과를 받는다
		if (joinRs.isFail ()) { // 가입이 실패할 시 historyBack 실행
			return rq.historyBack ( joinRs.getMsg () ); // 실패 메시지 표현
		}

		return rq.redirect ( "/",joinRs.getMsg ());
		// 성공후 메인페이지로 이동후 성공 메시지 반환
	}

controller에 profileImg 관련 코드를 추가해준다


MemberService.java에 profileImg 관련 코드 추가하기

private final GenFileService genFilrService; // 의존성 추가히기

// join 메서드에 profileImg 추가히기
@Transactional // 이 메서드에서 수행되는 데이터베이스 작업을 트랜잭션으로 관리
	public RsData<Member> join( String username, String password, String nickname, String email, MultipartFile profileImg, Jop jop ) {

		if(findByUsername ( username ).isPresent ()) // 제공된 사용자 이름으로 기존회원을 검색
			return RsData.of ( "F-1", "%s(은)는 사용중인 아이디 입니다" .formatted ( username ));
		// 존재할 시 에러메시지 구현

		Member member = Member // 빌더 형식으로 mebmer 객체 생성
				.builder()
				.username(username) // 사용자 이름 설정
				.password(passwordEncoder.encode(password)) // 비밀번호 암호화하여 설정
				.nickname(nickname) // 별명 설정
				.email ( email ) // 이메일 설정
				.jop ( jop ) // 직업 설정
				.build(); // member 객체를 빌드

		member = memberRepository.save(member); // 생성된 mebmer객체를 데이터베이스에 저장

		if (profileImg != null) {
			genFileService.save(member.getModelName(), member.getId(), "common", "profileImg", 0, profileImg);
		}
        // 사용자가 파일을 업로드 안할 시 이 코드는 실행이 되지 않고
			// 업로드 할 시 save에 메서드가 실행
			// save(현재 저장되는 파일과 관련된 모데일 이름 member의 모델이름, 데이터베이스에 저장된 객체의 ID, 
            // 파일의 종류나 카테코리를 지정하는 문자열, 저장되는 파일의 구체적인 용도나 타입을 지정하는 문자열,
            // 파일의 순서나 버번을 지정하는 숫자, 실제로 저장할 파일 객체)
		}

		return RsData.of ( "S-1", "회원가입이 완료되었습니다", member ); // 성공 응답과 함께 member 객체 반환
	}
    // MultipartFile은 스프링 프레임워크 일부로, HTTP 요청을 통해 전송된 파일을 나타내는 인터페이스
    // 주로 스프링 MVC에서 파일 업로드 기능을 구현할 때 사용이 된다
    // 주요 메서드
    String getName() : 폼 데이터에서 파일 파라미터의 이름을 반환
    String getOriginalFilename() : 클라이언트가 업로드한 파일의 실제이름을 반환, 경로 정보를 제거한 순수한 파일 이름
    String getContentType() : 파일의 MIME 타입을 반환 ex) imgage/jpeg
    boolean isEmpty() : 파일이 비어있거나 크기가 0인 경우 'true)를 반환
    long getSize() : 파일의 크기를 바이트 단위로 반환
    byte[] getByte() throws IOException : 파일의 내용을 읽기 위한 InputStream을 반환
    void transferTo (File dest ) throws IOException, IllegalStateException :
    업로드된 파일을 지정된 대상 파일로 저장, 파일을 디스크에 효율적으로 저장할 때 사용

profileImg를 빌더형식으로 같이 안하고 따로 코드를 작성한 이유는
Member Entity는 사용자의 핵심 데이터를 담당하며 관리에 집중을 하고 있는 반면, profileImg는 별도의 처리 과정(파일 저장, 파일 이름 등)이 필요할 수 있으므로, 분리하여 관리하는 것이 괜찮다


join.html profileImg 관련 코드 추가

form 태그에 enctype="multipart/form-data"추가하기 
// 파일을 문자열이 아닌 data타입으로 바꿔준다

				<!--	프로필	-->
<div class="w-full preview-image">
	<input type="file" id="profileImg" name="profileImg" class="w-full file-input file-input-bordered" accept="image/jpeg, image/png, image/gif">
</div>
// accept 속성은 사용자가 파일을 선택할 수 있는 타입을 지정하는 속성이다
// 그냥 내가 알려준거만 추가해도 되고 미리보기 이미지를 하고 싶다면 script를 추가해주면 된다
<!-- 파일 미리보기 스크립트 -->
	<script>
		document.addEventListener('DOMContentLoaded', function(){
			const imgInput = document.getElementById('profileImg');

			imgInput.addEventListener('change', function(){
				const parent = this.closest('.preview-image');
				const selectedFile = this.files[0];

				// 기존에 표시된 이미지 미리보기가 있으면 제거
				const existingDisplay = parent.querySelector('.upload-display');
				if (existingDisplay) {
					parent.removeChild(existingDisplay);
				}

				if (selectedFile && selectedFile.type.match('image.*')) {
<!--	img파일을 확인할 때 img/jpeg, img/png 등 모든 img파일 확인	-->
					const reader = new FileReader();

					reader.onload = function(e) {
						// 파일 읽기 성공 시, 이미지 미리보기 생성 및 표시
						const src = e.target.result;
						const displayDiv = document.createElement('div');
						displayDiv.className = 'upload-display';

						const img = document.createElement('img');
						img.src = src;
						img.className = 'upload-thumb-wrap'; // 클래스 이름을 설정합니다.
						// 직접 스타일 속성을 추가합니다.
						img.style.margin = '10px 0px';
						img.style.border = '2px solid #ddd';
						img.style.width = 'auto'; // 또는 '100px'와 같이 특정 크기를 지정할 수 있습니다.
						img.style.height = 'auto'; // 높이를 자동으로 조정하거나, '100px'와 같이 특정 높이를 지정할 수 있습니다.

						displayDiv.appendChild(img);
						parent.appendChild(displayDiv); // 미리보기 이미지를 .preview-image 내부에 추가
					};

					reader.readAsDataURL(selectedFile); // 선택된 파일을 Data URL로 읽어들임
				}
			});
		});
	</script>

파일저장경로와 파일을 URL로 볼 수 있게 도와주는 CustomWebMvcConfig.java, AppConfig.java 생성

@Configuration
public class AppConfig {
	// 파일 저장 경로와 관련된 설정을 처리
	@Getter
	public static String genFileDirPath;

	@Value("${custom.genFile.dirPath}") // yml 에 지정한 경로에 있는 데이터를 사용
	public void setFileDirPath(String genFileDirPath) { // 프로퍼티 값을 주입받아 클래스 내부에서 사용할 수 있도록 하는 설정자 메서드 역할을 함
		// 정적 필드에 값을 설정하기 위해 메서드 자체를 정적 메서드로 선언하지 않고, 대신 메서드 내에서 정적 필드 값을 직정 할당중
		AppConfig.genFileDirPath = genFileDirPath;
	}
}
@Configuration
// spring 구성 클래스 선언
// 클래스 내에서 정의된 빈을 spring의 애플리케이션 컨텍스트에 등록
public class CustomWebMvcConfig implements WebMvcConfigurer {
	// CustomWebMvcConfig 클래스는 WebMvcConfigurer 인터페이스를 구현합니다.
	// 이를 통해 Spring MVC의 웹 구성을 사용자 정의할 수 있습니다.

	@Override
	public void addResourceHandlers( ResourceHandlerRegistry registry ){
		// WebMvcConfigurer 인터페이스의 addResourceHandlers 메서드를 오버라이드합니다.
		// 이 메서드는 애플리케이션의 정적 리소스를 처리하기 위한 리소스 핸들러를 등록하는 데 사용됩니다.

		registry.addResourceHandler ( "/gen/**" )
				// WebMvcConfigurer 인터페이스의 addResourceHandlers 메서드를 오버라이드합니다.
				// 이 메서드는 애플리케이션의 정적 리소스를 처리하기 위한 리소스 핸들러를 등록하는 데 사용됩니다.

				.addResourceLocations ( "file:///" + AppConfig.getGenFileDirPath () + "/" );
				// 실제 리소스가 위치하는 디렉토리를 지정합니다.
				// 여기서는 AppConfig.getGenFileDirPath() 메서드를 통해 얻은 경로를 사용합니다.
				// file:/// 프리픽스는 파일 시스템의 절대 경로를 나타냅니다.

	}
	// 등록된 리소스 핸들러는 '/gen/**' 패턴에 매칭되는 URL 요청이 들어왔을 때, 해당 요청을 처리하기 위한 정적 리소스 위치를 지정
	// 애플리케이션 외부에 위치한 파일에 접근할 수 있도록 한다
	// Spring MVC 애플리케이션에서는 클래스패스 내의 리소스나, 특정 내장 디렉토리 아래의 리소스에 대해서만 접근을 허용함
}

yml에 value에 지정한 경로를 추가하기

custom:
  genFile:
    dirPath: c:/Temp/healMingle

파일을 지정한 경로에 부모풀더까지 생기면서 이미지가 생성이 된다


파일 Input 태그 추가

파일 선택시 미리보기

파일 업로드시 데이터베이스 저장

파일 업로드 후 웹에서 이미지 보기

파일 업로드 후 내가 지정한 풀더에 이미지 생성

profile
같은글이있어도양해부탁드려요(킁킁)

0개의 댓글