-
Notifications
You must be signed in to change notification settings - Fork 0
20260125 #76 메뉴 이미지 파일 업로드 기능 추가 #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "20260125_#76_\uBA54\uB274_\uC774\uBBF8\uC9C0_\uD30C\uC77C_\uC5C5\uB85C\uB4DC_\uAE30\uB2A5_\uCD94\uAC00"
Changes from all commits
6e140e0
a340bb4
2a231f8
8430fef
6ebcd32
57dfdbc
06ad745
c2f800f
3f38909
15d0e12
202ca11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,13 +8,15 @@ | |||||||||||||||||||||||||||||||||||
| import jakarta.validation.Valid; | ||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||
| import org.springframework.http.HttpStatus; | ||||||||||||||||||||||||||||||||||||
| import org.springframework.http.MediaType; | ||||||||||||||||||||||||||||||||||||
| import org.springframework.http.ResponseEntity; | ||||||||||||||||||||||||||||||||||||
| import org.springframework.web.bind.annotation.*; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||
| import org.springframework.web.multipart.MultipartFile; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @RestController | ||||||||||||||||||||||||||||||||||||
| @RequestMapping("/api/menu") | ||||||||||||||||||||||||||||||||||||
| @RequestMapping("/api") | ||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||
| public class MenuController implements MenuControllerDocs { | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -23,7 +25,7 @@ public class MenuController implements MenuControllerDocs { | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||
| @GetMapping | ||||||||||||||||||||||||||||||||||||
| @GetMapping("/menus") | ||||||||||||||||||||||||||||||||||||
| @LogMonitoringInvocation | ||||||||||||||||||||||||||||||||||||
| public ResponseEntity<List<MenuResponse>> getAllMenus(){ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -35,7 +37,7 @@ public ResponseEntity<List<MenuResponse>> getAllMenus(){ | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||
| @LogMonitoringInvocation | ||||||||||||||||||||||||||||||||||||
| @GetMapping("/category/{category_id}") | ||||||||||||||||||||||||||||||||||||
| @GetMapping("/category/{category_id}/menus") | ||||||||||||||||||||||||||||||||||||
| public ResponseEntity<List<MenuResponse>> getAllMenusByCategoryId( | ||||||||||||||||||||||||||||||||||||
| @PathVariable(name = "category_id") Long categoryId){ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -47,31 +49,46 @@ public ResponseEntity<List<MenuResponse>> getAllMenusByCategoryId( | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||
| @LogMonitoringInvocation | ||||||||||||||||||||||||||||||||||||
| @GetMapping("/{menuId}") | ||||||||||||||||||||||||||||||||||||
| @GetMapping("/menus/{menuId}") | ||||||||||||||||||||||||||||||||||||
| public ResponseEntity<MenuResponse> getMenuById(@PathVariable Long menuId){ | ||||||||||||||||||||||||||||||||||||
| return ResponseEntity.ok(menuService.getMenuById(menuId)); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||
| @LogMonitoringInvocation | ||||||||||||||||||||||||||||||||||||
| @GetMapping("/cafeteria/{cafeteria-id}") | ||||||||||||||||||||||||||||||||||||
| @GetMapping("/menus/cafeteria/{cafeteria-id}") | ||||||||||||||||||||||||||||||||||||
| public ResponseEntity<List<MenuResponse>> getAllMenusByCafeteriaId( | ||||||||||||||||||||||||||||||||||||
| @PathVariable(name = "cafeteria-id") Long cafeteriaId | ||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
| return ResponseEntity.ok(menuService.getAllMenusByCafeteriaId(cafeteriaId)); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
57
to
64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In Spring MVC path patterns, there are two different “naming rules” to keep straight: 1) The path variable name (inside
|
||||||||||||||||||||||||||||||||||||
| @Override | |
| @LogMonitoringInvocation | |
| @GetMapping("/cafeteria/{cafeteria-id}") | |
| @GetMapping("/menus/cafeteria/{cafeteria-id}") | |
| public ResponseEntity<List<MenuResponse>> getAllMenusByCafeteriaId( | |
| @PathVariable(name = "cafeteria-id") Long cafeteriaId | |
| ) { | |
| return ResponseEntity.ok(menuService.getAllMenusByCafeteriaId(cafeteriaId)); | |
| } | |
| `@Override` | |
| `@LogMonitoringInvocation` | |
| `@GetMapping`("/menus/cafeteria/{cafeteriaId}") | |
| public ResponseEntity<List<MenuResponse>> getAllMenusByCafeteriaId( | |
| `@PathVariable` Long cafeteriaId | |
| ) { | |
| return ResponseEntity.ok(menuService.getAllMenusByCafeteriaId(cafeteriaId)); | |
| } |
🤖 Prompt for AI Agents
In `@src/main/java/com/campustable/be/domain/menu/controller/MenuController.java`
around lines 57 - 64, The path variable in
MenuController.getAllMenusByCafeteriaId uses "{cafeteria-id}" which is
nonstandard and inconsistent; change the `@GetMapping` path to use "{cafeteriaId}"
and update the `@PathVariable` annotation to match (remove the explicit name or
use "cafeteriaId") so the method signature and mapping use camelCase
consistently with other endpoints and RFC 6570-friendly variable names.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |||||||||||||||||||||||||
| import com.campustable.be.domain.menu.dto.MenuUpdateRequest; | ||||||||||||||||||||||||||
| import com.campustable.be.domain.menu.entity.Menu; | ||||||||||||||||||||||||||
| import com.campustable.be.domain.menu.repository.MenuRepository; | ||||||||||||||||||||||||||
| import com.campustable.be.domain.s3.service.S3Service; | ||||||||||||||||||||||||||
| import com.campustable.be.global.exception.CustomException; | ||||||||||||||||||||||||||
| import com.campustable.be.global.exception.ErrorCode; | ||||||||||||||||||||||||||
| import java.util.ArrayList; | ||||||||||||||||||||||||||
|
|
@@ -19,6 +20,7 @@ | |||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||||||||
| import org.springframework.web.multipart.MultipartFile; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||
|
|
@@ -29,37 +31,79 @@ public class MenuService { | |||||||||||||||||||||||||
| private final MenuRepository menuRepository; | ||||||||||||||||||||||||||
| private final CategoryRepository categoryRepository; | ||||||||||||||||||||||||||
| private final CafeteriaService cafeteriaService; | ||||||||||||||||||||||||||
| private final S3Service s3Service; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||
| public MenuResponse createMenu(MenuRequest request) { | ||||||||||||||||||||||||||
| public MenuResponse createMenu(MenuRequest request, MultipartFile image) { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Category category = categoryRepository.findById(request.getCategoryId()) | ||||||||||||||||||||||||||
| .orElseThrow(() -> { | ||||||||||||||||||||||||||
| log.warn("createMenu: 유효하지 않은 category id"); | ||||||||||||||||||||||||||
| return new CustomException(ErrorCode.CATEGORY_NOT_FOUND); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Optional<Menu> existingMenu = menuRepository.findByCategoryAndMenuName( | ||||||||||||||||||||||||||
| category, | ||||||||||||||||||||||||||
| request.getMenuName() | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| menuRepository.findByCategoryAndMenuName(category, request.getMenuName()) | ||||||||||||||||||||||||||
| .ifPresent(menu -> { | ||||||||||||||||||||||||||
| log.error("createMenu: 이미 해당 카테고리에 menu가 존재합니다. 생성이 아닌 수정을 통해 진행해주세요."); | ||||||||||||||||||||||||||
| throw new CustomException(ErrorCode.MENU_ALREADY_EXISTS); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Menu menu = request.toEntity(category); | ||||||||||||||||||||||||||
| Menu savedMenu = menuRepository.save(menu); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (existingMenu.isPresent()) { | ||||||||||||||||||||||||||
| log.error("createMenu: 이미 해당 카테고리에 menu가 존재합니다. 생성이 아닌 수정을 통해 진행해주세요."); | ||||||||||||||||||||||||||
| throw new CustomException(ErrorCode.MENU_ALREADY_EXISTS); | ||||||||||||||||||||||||||
| if (image != null && !image.isEmpty()) { | ||||||||||||||||||||||||||
| return uploadMenuImage(savedMenu.getId(), image); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Menu menu = request.toEntity(category); | ||||||||||||||||||||||||||
| return MenuResponse.from(menuRepository.save(menu)); | ||||||||||||||||||||||||||
| return MenuResponse.from(savedMenu); | ||||||||||||||||||||||||||
|
Comment on lines
+55
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이미지 업로드 시 createMenu 응답에 menuUrl 누락 가능 uploadMenuImage가 URL을 저장해도 createMenu는 저장 직후의 엔티티로 응답해 menuUrl이 비어 보일 수 있습니다. 업로드가 발생하면 해당 응답을 그대로 반환하는 편이 안전합니다. ✅ 제안 수정- if (image != null && !image.isEmpty()) {
- uploadMenuImage(savedMenu.getId(), image);
- }
-
- return MenuResponse.from(savedMenu);
+ if (image != null && !image.isEmpty()) {
+ return uploadMenuImage(savedMenu.getId(), image);
+ }
+ return MenuResponse.from(savedMenu);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||
| public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) { | ||||||||||||||||||||||||||
| Menu menu = menuRepository.findById(menuId) | ||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomException(ErrorCode.MENU_NOT_FOUND)); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (image == null || image.isEmpty()) { | ||||||||||||||||||||||||||
| throw new CustomException(ErrorCode.INVALID_FILE_REQUEST); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| String oldUrl = menu.getMenuUrl(); | ||||||||||||||||||||||||||
| String cafeteriaName = menu.getCategory().getCafeteria().getName(); | ||||||||||||||||||||||||||
| String dirName = "menu/" + cafeteriaName; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| String newUrl = s3Service.uploadFile(image, dirName); | ||||||||||||||||||||||||||
| menu.setMenuUrl(newUrl); | ||||||||||||||||||||||||||
| Menu savedMenu; | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| savedMenu = menuRepository.save(menu); | ||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| s3Service.deleteFile(newUrl); | ||||||||||||||||||||||||||
| } catch (Exception ex) { | ||||||||||||||||||||||||||
| log.warn("uploadMenuImage: 신규 이미지 정리 실패. newUrl={}", newUrl, ex); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| log.error("uploadMenuImage: 메뉴 저장 실패. menuId={}", menuId, e); | ||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (oldUrl != null && !oldUrl.isBlank()) { | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| s3Service.deleteFile(oldUrl); | ||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||
| log.warn("uploadMenuImage: 기존 이미지 삭제 실패. oldUrl={}", oldUrl, e); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return MenuResponse.from(savedMenu); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+63
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DB 저장 실패 시 신규 S3 파일이 고아로 남을 수 있습니다. 🧹 제안 수정 String newUrl = s3Service.uploadFile(image, dirName);
menu.setMenuUrl(newUrl);
- Menu savedMenu = menuRepository.save(menu);
+ Menu savedMenu;
+ try {
+ savedMenu = menuRepository.save(menu);
+ } catch (Exception e) {
+ try {
+ s3Service.deleteFile(newUrl);
+ } catch (Exception cleanupEx) {
+ log.warn("uploadMenuImage: 신규 이미지 정리 실패. newUrl={}", newUrl, cleanupEx);
+ }
+ throw e;
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @Transactional(readOnly = true) | ||||||||||||||||||||||||||
| public MenuResponse getMenuById(Long menuId) { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Menu menu = menuRepository.findById(menuId) | ||||||||||||||||||||||||||
| .orElseThrow(()->{ | ||||||||||||||||||||||||||
| .orElseThrow(() -> { | ||||||||||||||||||||||||||
| log.error("getMenuById : 유효하지않은 menuId"); | ||||||||||||||||||||||||||
| return new CustomException(ErrorCode.MENU_NOT_FOUND); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
@@ -137,9 +181,15 @@ public void deleteMenu(Long menuId) { | |||||||||||||||||||||||||
| if (menu.isEmpty()) { | ||||||||||||||||||||||||||
| log.error("menuId not found {}", menuId); | ||||||||||||||||||||||||||
| throw new CustomException(ErrorCode.MENU_NOT_FOUND); | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| menuRepository.delete(menu.get()); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if (menu.get().getMenuUrl() != null && !menu.get().getMenuUrl().isBlank()) { | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| s3Service.deleteFile(menu.get().getMenuUrl()); | ||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||
| log.warn("deleteMenu: 이미지 삭제 실패. menuId={}, url={}", menuId, menu.get().getMenuUrl(), e); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| menuRepository.deleteById(menuId); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Spring Cloud AWS 3.4.2 Spring Boot 3.5.7 compatibility💡 Result:
Yes—Spring Cloud AWS 3.4.2 is compatible with Spring Boot 3.5.7, because Spring Cloud AWS 3.4.x targets Spring Boot 3.5.x (and Spring Cloud 2025.0.x). [1][2]
If you’re on Boot 3.5.7, you should align your BOMs accordingly (i.e., Spring Cloud 2025.0.x + Spring Cloud AWS 3.4.2) to avoid dependency mismatches. [1][2]
Sources:
[1] Spring Cloud AWS GitHub README – compatibility table (3.4.x → Boot 3.5.x)
[2] Spring Cloud project page – release train mapping (2025.0.x → Boot 3.5.x)
🌐 Web query:
io.awspring.cloud spring-cloud-aws-starter-s3 latest version 2024 2025💡 Result:
For
io.awspring.cloud:spring-cloud-aws-starter-s3:As of Feb 1, 2026, Maven Central also shows a newer pre-release: 4.0.0-RC1 (published Jan 14, 2026). The latest stable remains 3.4.2. [1]
Maven coords
🌐 Web query:
spring-cloud-aws-starter-s3 3.4.2 security vulnerabilities release notes💡 Result:
spring-cloud-aws-starter-s3 3.4.2 release notes (security-related)
Official 3.4.2 release notes (awspring)
Spring Cloud AWS 3.4.2 (released Dec 1, 2025) lists only one change, and it is in Core (STS autoconfiguration):
StsWebIdentityTokenFileCredentialsProvideris not configured unless the required properties are provided. [1]No other fixes (including S3-specific or security fixes) are mentioned in the 3.4.2 release notes. [1]
Known vulnerability reported for 3.4.2 (transitive dependency)
MvnRepository flags CVE-2025-14763 as a vulnerability coming from dependencies for Spring Cloud AWS 3.4.2 / its BOM. [2][3]
Per NVD, CVE-2025-14763 affects the Amazon S3 Encryption Client for Java (missing cryptographic key commitment) and AWS recommends upgrading the S3 Encryption Client to 4.0.0+. [4]
Implication: if your dependency tree (directly or transitively) pulls in a vulnerable amazon-s3-encryption-client-java version, you may need to override that dependency (or upgrade to a Spring Cloud AWS release line that pulls a fixed version). [2][4]
Sources: [1] GitHub Releases (awspring/spring-cloud-aws) • [2] MvnRepository spring-cloud-aws 3.4.2 • [3] MvnRepository spring-cloud-aws-dependencies 3.4.2 • [4] NVD CVE-2025-14763
Spring Cloud AWS S3 스타터 3.4.2 버전 확인
Spring Boot 3.5.7과의 호환성 및 최신 안정 버전 여부가 확인되었습니다.
다만 CVE-2025-14763 (Amazon S3 Encryption Client의 암호화 키 커밋 누락)이 전이 의존성으로 플래그되었습니다. S3 암호화 기능을 사용하는 경우, amazon-s3-encryption-client-java 의존성을 4.0.0 이상으로 오버라이드하거나 업스트림 패치를 기다려야 합니다.
🤖 Prompt for AI Agents