Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/main/java/jombi/freemates/config/FileStorageConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package jombi.freemates.config;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FileStorageConfig {
@Bean
public Path uploadDir(FileStorageProperties props) {
Path path = Paths
.get(props.getUploadDir())
.toAbsolutePath()
.normalize();
try {
Files.createDirectories(path);
} catch (IOException e) {
throw new RuntimeException("업로드 디렉터리 생성 실패", e);
}
return path;
}

}
16 changes: 16 additions & 0 deletions src/main/java/jombi/freemates/config/FileStorageProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package jombi.freemates.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "file")
@Getter
@Setter
public class FileStorageProperties {
private String uploadDir;


}
20 changes: 20 additions & 0 deletions src/main/java/jombi/freemates/config/StaticResourceConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package jombi.freemates.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class StaticResourceConfig implements WebMvcConfigurer {
@Value("${file.upload-dir}") // application.yml 에 정의된 경로
private String uploadDir;

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 클라이언트가 /uploads/** 로 요청하면, 실제 디스크의 uploadDir 경로를 뒤에 매핑
registry
.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadDir + "/");
}
}
5 changes: 5 additions & 0 deletions src/main/java/jombi/freemates/config/WebSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package jombi.freemates.config;

import static org.springframework.security.config.Customizer.withDefaults;

import jombi.freemates.service.CustomUserDetailsService;
import jombi.freemates.util.filter.CustomAuthenticationEntryPoint;
import jombi.freemates.util.JwtUtil;
Expand Down Expand Up @@ -29,12 +31,15 @@ public WebSecurityConfig(JwtUtil jwtUtil, CustomUserDetailsService customUserDet
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http
.cors(withDefaults())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex.authenticationEntryPoint(new CustomAuthenticationEntryPoint()))
.authorizeHttpRequests(auth -> auth
// 허용 URL
.requestMatchers(SecurityUrls.AUTH_WHITELIST.toArray(new String[0])).permitAll()
// 업로드된 이미지 파일은 모두에게 허용
.requestMatchers(HttpMethod.GET,"/uploads/**").permitAll()
// 관리자 URL
.requestMatchers(SecurityUrls.ADMIN_PATHS.toArray(new String[0])).hasRole("ADMIN")
// 회원 관련 예시 URL
Expand Down
173 changes: 173 additions & 0 deletions src/main/java/jombi/freemates/controller/BookmarkController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package jombi.freemates.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.UUID;
import jombi.freemates.model.constant.Author;
import jombi.freemates.model.constant.PinColor;
import jombi.freemates.model.constant.Visibility;
import jombi.freemates.model.dto.BookmarkRequest;
import jombi.freemates.model.dto.BookmarkResponse;
import jombi.freemates.model.dto.CustomUserDetails;
import jombi.freemates.service.BookmarkService;
import jombi.freemates.util.docs.ApiChangeLog;
import jombi.freemates.util.docs.ApiChangeLogs;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Tag(
name = "즐겨찾기 API",
description = "즐겨찾기 관련 API 제공"
)
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/api/bookmark")
public class BookmarkController {
private final BookmarkService bookmarkService;

@ApiChangeLogs({
@ApiChangeLog(
date = "2025-05-26",
author = Author.LEEDAYE,
issueNumber = 96,
description = "즐겨찾기 만들기"
)
})
@Operation(
summary = "즐겨찾기 생성",
description = """
## 인증(JWT): **필요**

## 요청 파라미터 (multipart/form-data)
- **`title`**: 즐겨찾기 제목
- **`description`**: 즐겨찾기 설명
- **`pinColor`**: 핀 색깔 (ENUM, 6가지 중 하나)
- **`visibility`**: 공개 여부 (ENUM: `PUBLIC` 또는 `PRIVATE`)
- **`file`**: 이미지 파일 (MultipartFile)

## 반환값 (`BookmarkResponse`)
- **`memberId`**: 즐겨찾기를 생성한 회원 ID (UUID)
- **`nickname`**: 닉네임
- **`imageUrl`**: 저장된 이미지 URL
- **`title`**, **`description`**, **`pinColor`**, **`visibility`**: 요청값 그대로 반환

## 에러코드
"""

)

@PostMapping(value = "/create",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<BookmarkResponse> create(
@RequestParam String title,
@RequestParam String description,
@RequestParam PinColor pinColor,
@RequestParam Visibility visibility,
@RequestParam(value = "image", required = false)
@Parameter(description = "이미지 파일", content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE))
MultipartFile image,
@AuthenticationPrincipal CustomUserDetails user
) {
BookmarkRequest req = new BookmarkRequest();
req.setTitle(title);
req.setDescription(description);
req.setPinColor(pinColor);
req.setVisibility(visibility);
BookmarkResponse response = bookmarkService.create(user, req, image);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
Comment on lines +75 to +95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

파일 업로드 검증 로직 확인 필요

멀티파트 파일 업로드 엔드포인트가 올바르게 구현되어 있으나, 파일 크기 제한이나 허용 형식에 대한 검증이 컨트롤러나 서비스 레이어에서 처리되고 있는지 확인이 필요합니다.

다음 스크립트로 BookmarkService에서 파일 검증 로직이 구현되어 있는지 확인하겠습니다:


🏁 Script executed:

#!/bin/bash
# BookmarkService의 create 메서드에서 파일 검증 로직 확인
echo "=== BookmarkService 파일 위치 확인 ==="
fd -t f "BookmarkService.java"

echo "=== create 메서드 내용 확인 ==="
rg -A 20 "public.*create.*CustomUserDetails.*BookmarkRequest.*MultipartFile" --type java

echo "=== 파일 검증 관련 코드 검색 ==="
rg -B 5 -A 10 "getSize\(\)|getContentType\(\)|file.*valid|upload.*valid" --type java

Length of output: 456


파일 업로드 검증 로직 추가 필요

서비스 레이어(create 메서드)에서 MultipartFile에 대한 크기·형식 검증이 전혀 구현되어 있지 않습니다. 악성 파일 업로드나 서버 과부하를 방지하기 위해 아래 위치에 검증 로직을 추가해주세요.

• src/main/java/jombi/freemates/service/BookmarkService.java
public BookmarkResponse create(CustomUserDetails user, BookmarkRequest req, MultipartFile image) 내 파일 유효성 검사
• src/main/java/jombi/freemates/controller/BookmarkController.java
– 컨트롤러 단에서도 요청 파라미터 유효성(필수 여부, 최대 크기 등) 재검증

추가 검증 예시:

  • image.getSize()로 maxFileSize 비교
  • image.getContentType()으로 허용된 MIME 타입(image/png, image/jpeg 등) 확인
  • 위반 시 BadRequestException 또는 MultipartException 발생 처리
🤖 Prompt for AI Agents
In src/main/java/jombi/freemates/controller/BookmarkController.java lines 75 to
95 and in src/main/java/jombi/freemates/service/BookmarkService.java within the
create method, add validation logic for the MultipartFile image parameter. In
the controller, re-validate request parameters including checking if the image
size exceeds a defined maxFileSize and if the content type is among allowed MIME
types like image/png or image/jpeg. In the service layer's create method,
implement similar checks on image.getSize() and image.getContentType(), and if
validation fails, throw appropriate exceptions such as BadRequestException or
MultipartException to prevent malicious or oversized file uploads.


@ApiChangeLogs({
@ApiChangeLog(
date = "2025-05-26",
author = Author.LEEDAYE,
issueNumber = 96,
description = "즐겨찾기 유저별로 가져오기"
)
})
@Operation(
summary = "내 즐겨찾기 가져오기",
description = """
## 인증(JWT): **필요**

## 요청 파라미터
- **`없음`**

## 반환값 (`BookmarkResponse`)
- **`memberId`**: 즐겨찾기를 생성한 회원 ID (UUID)
- **`nickname`**: 닉네임
- **`imageUrl`**: 저장된 이미지 URL
- **`title`**, **`description`**, **`pinColor`**, **`visibility`**: 요청값 그대로 반환

## 에러코드
"""
)
@GetMapping("/mylist")
public List<BookmarkResponse> list(
@AuthenticationPrincipal CustomUserDetails customUserDetails
) {
return bookmarkService.listByMember(customUserDetails);
}


/**
* 장소를 즐겨찾기에 추가하는 API입니다.
* */
@ApiChangeLogs({
@ApiChangeLog(
date = "2025-05-26",
author = Author.LEEDAYE,
issueNumber = 96,
description = "장소 즐겨찾기에 추가하기"
)
})
@Operation(
summary = "장소 즐겨찾기에 추가하기",
description = """
## 인증(JWT): **필요**

## 요청 파라미터
- **Path Variable**
- `bookmarkId` (UUID): 장소를 추가할 즐겨찾기 ID
**Request Parameter**
- `placeId` (UUID): 추가할 장소 ID

## 반환값
- **HTTP Status 200 OK** (혹은 204 No Content)

## 에러코드
- `UNAUTHORIZED (401)`: 인증되지 않은 사용자입니다.
- `BOOKMARK_NOT_FOUND (404)`: 존재하지 않는 즐겨찾기입니다.
- `PLACE_NOT_FOUND (404)`: 존재하지 않는 장소입니다.
- `DUPLICATE_PLACE_IN_BOOKMARK (409)`: 이미 즐겨찾기에 추가된 장소입니다.
"""
)
@PostMapping( "/{bookmarkId}/place")
public ResponseEntity<Void> addPlaceToBookmark(
@AuthenticationPrincipal CustomUserDetails customUserDetails,
@PathVariable("bookmarkId") UUID bookmarkId,
@RequestParam UUID placeId
) {
log.debug("placeId:{}", placeId);
bookmarkService.addPlaceToBookmark(customUserDetails, bookmarkId, placeId);
return ResponseEntity.ok().build(); // 혹은 204 No Content
}

}
52 changes: 52 additions & 0 deletions src/main/java/jombi/freemates/controller/PlaceController.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
package jombi.freemates.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import jombi.freemates.model.constant.Author;
import jombi.freemates.model.postgres.Place;
import jombi.freemates.service.PlaceService;
import jombi.freemates.util.docs.ApiChangeLog;
import jombi.freemates.util.docs.ApiChangeLogs;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(
Expand All @@ -26,4 +37,45 @@ public ResponseEntity<Void> refreshPlaces() {
return ResponseEntity.ok().build();
}

@ApiChangeLogs({
@ApiChangeLog(
date = "2025-05-26",
author = Author.LEEDAYE,
issueNumber = 96,
description = "장소가져오기"
)
})
@Operation(
summary = "장소가져오기",
description = """
## 인증(JWT): **필요**

## 요청 파라미터 (multipart/form-data)
- **`page`**: 페이지(0부터 시작, 최대 32)
- **`size`**: 크기

## 반환값 (`ResponseEntity<Page<Place>>`)
- **`content`**: 장소 목록
- **`totalElements`**: 전체 요소 수
- **`totalPages`**: 전체 페이지 수
- **`number`**: 현재 페이지 번호
- **`size`**: 페이지 크기
- **`sort`**: 정렬 정보
- **`numberOfElements`**: 현재 페이지의 요소 수
- **`empty`**: 현재 페이지가 비어있는지 여부

## 에러코드
"""

)
@GetMapping("/list")
public ResponseEntity<Page<Place>> getPagedPlaces(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
// 페이지와 사이즈만으로 Pageable 생성
Pageable pageable = PageRequest.of(page, size);
return ResponseEntity.ok(placeService.getPlaces(pageable));
}

}
31 changes: 31 additions & 0 deletions src/main/java/jombi/freemates/model/dto/BookmarkRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package jombi.freemates.model.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jombi.freemates.model.constant.PinColor;
import jombi.freemates.model.constant.Visibility;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;

@ToString
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookmarkRequest {

private String title;
private String description;
@Schema(implementation = PinColor.class)
private PinColor pinColor;
@Schema(implementation = Visibility.class)
private Visibility visibility;


}
30 changes: 30 additions & 0 deletions src/main/java/jombi/freemates/model/dto/BookmarkResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package jombi.freemates.model.dto;

import java.util.UUID;
import jombi.freemates.model.constant.PinColor;
import jombi.freemates.model.constant.Visibility;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Builder
@ToString
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class BookmarkResponse {

private UUID bookmarkId;
private String imageUrl;
private String title;
private String description;
private PinColor pinColor;
private Visibility visibility;
private UUID memberId;
private String nickname;

}
Loading