@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
@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 메서드는 회원가입을 이미지가 있고 회원가입이 완료가 되면 내가 지정한 경로에 풀더(디렉토리)가 생기면서 이미지가 생성이 될 때 보조해주는 메서드이다
@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"); // 디렉토리 이름은 관계 타입 코드와 현재 날짜로 구성됩니다.
}
}
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 처리를 부드럽게 할 수 있다
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"를 반환
}
}
파일 확장자와 파일이름을 추출하는 메서드와 날짜 관련 메서드를 추가한다
// 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 관련 코드를 추가해준다
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는 별도의 처리 과정(파일 저장, 파일 이름 등)이 필요할 수 있으므로, 분리하여 관리하는 것이 괜찮다
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>
@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 애플리케이션에서는 클래스패스 내의 리소스나, 특정 내장 디렉토리 아래의 리소스에 대해서만 접근을 허용함
}
custom:
genFile:
dirPath: c:/Temp/healMingle
파일을 지정한 경로에 부모풀더까지 생기면서 이미지가 생성이 된다