Skip to content

Commit 1a42d72

Browse files
committed
[add] 이미지 경로 저장 구현, 테스트 컨트롤러 생성.
1 parent 29ccf8b commit 1a42d72

10 files changed

+271
-26
lines changed

build.gradle

+6-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ dependencies {
6666

6767
// 실시간 채팅을 위한 필요 라이브러리들
6868
implementation 'org.springframework.boot:spring-boot-starter-websocket'
69-
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
70-
implementation 'it.ozimov:embedded-redis:0.7.2'
7169
implementation 'org.webjars:sockjs-client:1.1.2'
7270
implementation 'org.webjars:stomp-websocket:2.3.3-1'
7371

@@ -89,6 +87,12 @@ dependencies {
8987
implementation 'org.locationtech.jts:jts-core:1.19.0'
9088
implementation 'com.querydsl:querydsl-spatial'
9189

90+
//썸네일 의존성
91+
implementation 'net.coobird:thumbnailator:0.4.20'
92+
93+
//thymeleaf 의존성
94+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
95+
9296
}
9397

9498
tasks.named('test') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.mallangs.domain.member.controller;
2+
3+
import com.mallangs.domain.member.util.UploadUtil;
4+
import com.mallangs.global.exception.ErrorCode;
5+
import com.mallangs.global.exception.MallangsCustomException;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.log4j.Log4j2;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.access.prepost.PreAuthorize;
11+
import org.springframework.web.bind.annotation.*;
12+
import org.springframework.web.multipart.MultipartFile;
13+
14+
import java.util.Objects;
15+
16+
import static com.mallangs.global.exception.ErrorCode.UNSUPPORTED_FILE_TYPE;
17+
18+
19+
@Log4j2
20+
@RestController
21+
//@PreAuthorize("hasRole('USER')")
22+
@RequiredArgsConstructor
23+
@RequestMapping("/api/member/file")
24+
public class MemberFileController {
25+
26+
private final UploadUtil uploadUtil;
27+
28+
@DeleteMapping("/{profileImage}")
29+
public ResponseEntity<?> fileDelete(@PathVariable String profileImage) {
30+
log.info("--- fileDelete() : " + profileImage);
31+
uploadUtil.deleteFile(profileImage);
32+
return ResponseEntity.ok().build();
33+
}
34+
35+
@PostMapping("/upload")
36+
@Operation(summary = "프로필 사진 등록", description = "회원 가입 시 프로필 사진을 등록할 때 사용하는 API")
37+
public ResponseEntity<String> uploadFile(@RequestParam("profileImage") MultipartFile profileImage) {
38+
log.info("--- uploadFile() : " + profileImage);
39+
40+
//업로드 파일이 없는 경우
41+
if (profileImage == null) {
42+
throw new MallangsCustomException(ErrorCode.NOT_FOUND_PROFILE_IMAGE); //UploadNotSupportedException 예외 발생 시키기 - 메시지 : 업로드 파일이 없습니다.
43+
}
44+
log.info("------------------------------");
45+
log.info("name : " + profileImage.getName());
46+
log.info("origin name : " + profileImage.getOriginalFilename());
47+
log.info("type : " + profileImage.getContentType());
48+
49+
checkFileExt(Objects.requireNonNull(profileImage.getOriginalFilename()));
50+
return ResponseEntity.ok(uploadUtil.upload(profileImage));
51+
}
52+
53+
//업로드 파일 확장자 체크
54+
public void checkFileExt(String imageName) throws MallangsCustomException {
55+
String ext = imageName.substring(imageName.lastIndexOf(".") + 1); //2.이미지 파일 확장자
56+
String regExp = "^(jpg|jpeg|JPG|JPEG|png|PNG|gif|GIF|bmp|BMP)";
57+
58+
//업로드 파일의 확장자가 위에 해당하지 않는 경우 예외
59+
if (!ext.matches(regExp)) {
60+
throw new MallangsCustomException(UNSUPPORTED_FILE_TYPE);
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//package com.mallangs.domain.member.controller.test;
2+
//
3+
//import org.springframework.stereotype.Controller;
4+
//import org.springframework.web.bind.annotation.GetMapping;
5+
//
6+
//@Controller
7+
//public class MemberFileTestPageController {
8+
// /**
9+
// * 파일 업로드 테스트 페이지 접속 컨트롤러
10+
// * */
11+
// @GetMapping("/api/member-file-test")
12+
// public String getMemberFileTestPage() {
13+
// return "member-file-test"; // Thymeleaf template name
14+
// }
15+
//}

src/main/java/com/mallangs/domain/member/dto/MemberCreateRequest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public class MemberCreateRequest {
2828
private String nickname;
2929
@Pattern(regexp = Email.REGEX, message = Email.ERR_MSG)
3030
private String email;
31-
@Pattern(regexp = "([^\\s]+(\\.(?i)(jpg|jpeg|png|gif|bmp|tiff|webp|svg|ico|heic|heif|avif))$)",
32-
message = "유효한 이미지 파일을 업로드해주세요. (jpg, jpeg, png, gif, bmp, tiff, webp, svg, ico, heic, heif, avif)")
31+
@Pattern(regexp = "^(jpg|jpeg|JPG|JPEG|png|PNG|gif|GIF|bmp|BMP)",
32+
message = "유효한 이미지 파일을 업로드해주세요. (jpg/jpeg/JPG/JPEG/png/PNG/gif/GIF/bmp/BMP)")
3333
private String profileImage;
3434
@NotNull(message = "반려동물 유무는 필수 입력입니다.")
3535
private Boolean hasPet;

src/main/java/com/mallangs/domain/member/entity/Member.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class Member extends BaseTimeEntity {
4444
@Builder.Default
4545
private MemberRole memberRole = MemberRole.ROLE_USER;
4646

47-
@Column(name = "profile_image")
47+
@Column(name = "profile_image", columnDefinition = "TEXT")
4848
private String profileImage;
4949

5050
@Column(name = "has_pet", nullable = false)
@@ -96,4 +96,7 @@ public void addAddress(Address address){
9696
public void recordLoginTime(){
9797
this.lastLoginTime = LocalDateTime.now();
9898
}
99+
public void changeProfileImage(String profileImage){
100+
this.profileImage = profileImage;
101+
}
99102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.mallangs.domain.member.util;
2+
3+
import com.mallangs.domain.member.repository.MemberRepository;
4+
import jakarta.annotation.PostConstruct;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.log4j.Log4j2;
7+
import net.coobird.thumbnailator.Thumbnails;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.web.multipart.MultipartFile;
11+
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import java.util.UUID;
17+
18+
@Log4j2
19+
@Component
20+
@RequiredArgsConstructor
21+
public class UploadUtil {
22+
23+
@Value("${edu.example.upload.path}")
24+
private String uploadPath;
25+
26+
@PostConstruct
27+
public void init() {
28+
File tempDir = new File(uploadPath);
29+
//업로드 디렉토리 생성
30+
if (!tempDir.exists()) {
31+
log.info("--- tempDir : " + tempDir);
32+
tempDir.mkdir();
33+
}
34+
35+
uploadPath = tempDir.getAbsolutePath();
36+
log.info("--- getPath() : " + tempDir.getPath());
37+
log.info("--- uploadPath : " + uploadPath);
38+
log.info("-------------------------------------");
39+
}
40+
41+
//File 업로드
42+
public String upload(MultipartFile file) {
43+
44+
String uuid = UUID.randomUUID().toString();
45+
String saveFilename = uuid + "_" + file.getOriginalFilename();
46+
String savePath = uploadPath + File.separator;
47+
48+
try {
49+
//파일 업로드
50+
file.transferTo(new File(savePath + saveFilename));
51+
52+
//썸네일 파일 생성
53+
Thumbnails.of(new File(savePath + saveFilename))
54+
.size(150, 150)
55+
.toFile(savePath + "s_" + saveFilename);
56+
57+
} catch (IOException e) {
58+
throw new RuntimeException(e);
59+
}
60+
return saveFilename;
61+
}
62+
63+
//업로드 파일 삭제
64+
public void deleteFile(String filename) {
65+
File file = new File(uploadPath + File.separator + filename);
66+
File thumbFile = new File(uploadPath + File.separator + "s_" + filename);
67+
68+
try {
69+
if (file.exists()) file.delete();
70+
if (thumbFile.exists()) thumbFile.delete();
71+
} catch (Exception e) {
72+
log.error(e.getMessage());
73+
}
74+
}
75+
}

src/main/java/com/mallangs/global/config/SecurityConfig.java

+12-21
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
2727
import org.springframework.web.cors.CorsConfiguration;
2828

29+
import java.util.Arrays;
2930
import java.util.Collections;
3031

3132
@Configuration
@@ -60,9 +61,8 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws
6061
// cors 필터
6162
http
6263
.cors(corsCustomizer -> corsCustomizer.configurationSource(request -> {
63-
6464
CorsConfiguration configuration = new CorsConfiguration();
65-
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
65+
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
6666
configuration.setAllowedMethods(Collections.singletonList("*"));
6767
configuration.setAllowCredentials(true);
6868
configuration.setAllowedHeaders(Collections.singletonList("*"));
@@ -90,6 +90,14 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws
9090
.csrf(AbstractHttpConfigurer::disable)
9191
.formLogin(AbstractHttpConfigurer::disable)
9292
.httpBasic(AbstractHttpConfigurer::disable);
93+
// oauth2
94+
http
95+
.oauth2Login((oauth2) -> oauth2
96+
.userInfoEndpoint((userInfo) -> userInfo
97+
.userService(customOAuth2MemberService))
98+
.successHandler(customSuccessHandler)
99+
.failureHandler(customFailureHandler)
100+
);
93101

94102
// 경로별 인가 작업
95103
http
@@ -98,9 +106,11 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws
98106
.requestMatchers("/api/member/register", "/api/member/login",
99107
"/api/member/logout","/api/member/find-user-id",
100108
"/api/member/find-password").permitAll()
109+
.requestMatchers("/api/member/oauth2/**").permitAll()
101110
.requestMatchers("/api/member/admin").hasRole("ADMIN")
102111
.requestMatchers("/api/member/**").hasAnyRole("USER","ADMIN")
103112
.requestMatchers("/api/address/**").permitAll()
113+
.requestMatchers("/api/member-file-test").permitAll()
104114
// Swagger UI 관련 경로 허용
105115
.requestMatchers("/swagger-ui/**").permitAll()
106116
.requestMatchers("/v3/api-docs/**").permitAll()
@@ -116,24 +126,5 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws
116126
.addFilterBefore(logoutFilter, JWTFilter.class);
117127
return http.build();
118128
}
119-
@Bean
120-
public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
121-
http
122-
.csrf(AbstractHttpConfigurer::disable)
123-
.formLogin(AbstractHttpConfigurer::disable)
124-
.httpBasic(AbstractHttpConfigurer::disable)
125-
.authorizeHttpRequests((auth) -> auth
126-
.requestMatchers("/oauth2/**").permitAll()
127-
.anyRequest().denyAll() // OAuth2 경로 외에는 이 체인에서 거부
128-
)
129-
.oauth2Login((oauth2) -> oauth2
130-
.userInfoEndpoint((userInfo) -> userInfo
131-
.userService(customOAuth2MemberService))
132-
.successHandler(customSuccessHandler)
133-
.failureHandler(customFailureHandler)
134-
);
135-
136-
return http.build();
137-
}
138129
}
139130

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<!DOCTYPE html>
2+
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>회원 파일 테스트 페이지</title>
7+
</head>
8+
<body>
9+
<h1>회원 파일 테스트 페이지</h1>
10+
11+
<section>
12+
<h2>프로필 이미지 업로드</h2>
13+
<form id="uploadForm" enctype="multipart/form-data">
14+
<label for="profileImage">프로필 이미지 선택:</label>
15+
<input type="file" id="profileImage" name="profileImage" accept="image/*" required>
16+
<button type="button" onclick="uploadFile()">업로드</button>
17+
</form>
18+
<p id="uploadResponse"></p>
19+
</section>
20+
21+
<section>
22+
<h2>프로필 이미지 삭제</h2>
23+
<form id="deleteForm">
24+
<label for="deleteImageName">프로필 이미지 이름:</label>
25+
<input type="text" id="deleteImageName" name="profileImage" placeholder="이미지 이름 입력" required>
26+
<button type="button" onclick="deleteFile()">삭제</button>
27+
</form>
28+
<p id="deleteResponse"></p>
29+
</section>
30+
31+
<script>
32+
// 하드코딩된 액세스 토큰
33+
const accessToken = "Bearer eyJhbGciOiJIUzI1NiIsInR5cGUiOiJKV1QifQ.eyJyb2xlIjoiUk9MRV9VU0VSIiwiY2F0ZWdvcnkiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiJhenNBZDQxMjEiLCJlbWFpbCI6InJrcmt3bmoxMDQ2MjExQGdtYWlsLmNvbSIsImlhdCI6MTczMjY2ODUzNCwiZXhwIjoxNzMyNjcwMzM0fQ.vUsHAYuHNZhxmppKqgm6hzHsiniD-9acJAN9qzRV3Zg";
34+
35+
// 리프레시 토큰을 쿠키에 설정
36+
document.cookie = "RefreshToken=eyJhbGciOiJIUzI1NiIsInR5cGUiOiJKV1QifQ.eyJyYW5kb21VVUlEIjoiMWI3MTlmYzUtZDFiMC00ZTFhLWFhODItNmRlZThjYzQ2N2Y3IiwidXNlcklkIjoiYXpzQWQ0MTIxIiwiaWF0IjoxNzMyNjY4NTM0LCJleHAiOjE3MzI5Mjc3MzR9.RK5XRW1oxmdUN2mou6fVF4BKVPlROvKv5_xoFLXyU3I; path=/; secure; HttpOnly";
37+
38+
// API 기본 URL
39+
const apiBaseUrl = '/api/member/file';
40+
41+
// 파일 업로드 함수
42+
async function uploadFile() {
43+
const formData = new FormData(document.getElementById('uploadForm'));
44+
const responseElement = document.getElementById('uploadResponse');
45+
46+
try {
47+
const response = await fetch(`${apiBaseUrl}/upload`, {
48+
method: 'POST',
49+
headers: {
50+
'Authorization': accessToken
51+
},
52+
body: formData,
53+
credentials: 'include' // 쿠키 전송을 위해 포함
54+
});
55+
56+
if (response.ok) {
57+
const data = await response.text();
58+
responseElement.textContent = `업로드 성공: ${data}`;
59+
} else {
60+
const errorData = await response.json();
61+
responseElement.textContent = `업로드 실패: ${errorData.message}`;
62+
}
63+
} catch (error) {
64+
responseElement.textContent = `에러 발생: ${error.message}`;
65+
}
66+
}
67+
68+
// 파일 삭제 함수
69+
async function deleteFile() {
70+
const imageName = document.getElementById('deleteImageName').value;
71+
const responseElement = document.getElementById('deleteResponse');
72+
73+
try {
74+
const response = await fetch(`${apiBaseUrl}/${encodeURIComponent(imageName)}`, {
75+
method: 'DELETE',
76+
headers: {
77+
'Authorization': accessToken
78+
},
79+
credentials: 'include' // 쿠키 전송을 위해 포함
80+
});
81+
82+
if (response.ok) {
83+
responseElement.textContent = `삭제 성공`;
84+
} else {
85+
const errorData = await response.json();
86+
responseElement.textContent = `삭제 실패: ${errorData.message}`;
87+
}
88+
} catch (error) {
89+
responseElement.textContent = `에러 발생: ${error.message}`;
90+
}
91+
}
92+
</script>
93+
</body>
94+
</html>
Loading
Loading

0 commit comments

Comments
 (0)