Skip to content

Commit

Permalink
Merge pull request #56 from saessagMarket/feat/#50-S3-profile-upload
Browse files Browse the repository at this point in the history
AWS S3를 이용한 프로필 사진 업로드
  • Loading branch information
ahyeonkong authored Jan 31, 2025
2 parents 3ff5e2b + 10d1423 commit a4f5b5b
Show file tree
Hide file tree
Showing 18 changed files with 290 additions and 115 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation 'software.amazon.awssdk:auth:2.20.0'
implementation 'me.paulschwarz:spring-dotenv:3.0.0'
implementation 'org.springframework:spring-web'
implementation 'org.springframework.boot:spring-boot-starter-security'

}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.market.saessag.domain.photo.controller;

import com.market.saessag.domain.photo.service.S3Service;
import com.market.saessag.global.exception.ErrorCode;
import com.market.saessag.global.response.ApiResponse;
import org.springframework.http.ResponseEntity;
import com.market.saessag.global.response.SuccessCode;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -29,24 +29,15 @@ public ApiResponse<List<String>> uploadPhotos(@RequestParam MultipartFile[] file
String fileUrl = s3Service.uploadFile(file);
fileUrls.add(fileUrl);
}
return ApiResponse.<List<String>>builder()
.status("200")
.data(fileUrls)
.build();
return ApiResponse.success(SuccessCode.OK, fileUrls);
} catch (IOException e) {
return ApiResponse.<List<String>>builder()
.status("500")
.data(Collections.singletonList("파일 업로드에 실패했습니다." + e.getMessage()))
.build();
return ApiResponse.error(ErrorCode.FILE_UPLOAD_ERROR);
}
}

@GetMapping()
public ApiResponse<Map<String, String>> getPresignedUrl(@RequestParam List<String> keys) {
Map<String, String> urls = s3Service.getPresignedUrl(keys);
return ApiResponse.<Map<String, String>>builder()
.status("200")
.data(urls)
.build();
return ApiResponse.success(SuccessCode.OK, urls);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.market.saessag.domain.photo.controller;

import com.market.saessag.domain.photo.service.S3Service;
import com.market.saessag.global.exception.ErrorCode;
import com.market.saessag.global.response.ApiResponse;
import com.market.saessag.global.response.SuccessCode;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Map;

@RestController
@RequestMapping("/api/profile")
@RequiredArgsConstructor
public class ProfileImageController {
private final S3Service s3Service;

/*
프로필 사진 업로드
사용자 관점에서는 프로필 사진 업로드와 수정이 동일한 방식으로 진행됨.
따라서 하나의 Patch 메서드에서 동작함.
*/
@PatchMapping("/upload-image")
public ApiResponse<String> uploadProfileImage(@RequestParam("file") MultipartFile file, HttpSession session) {
try {
String email = (String) session.getAttribute("email");
if (email == null) {
return ApiResponse.error(ErrorCode.UNAUTHORIZED);
}

String fileUrl = s3Service.uploadProfileImage(file, email);
return ApiResponse.success(SuccessCode.UPLOAD_SUCCESS, fileUrl);
} catch (IOException e) {
return ApiResponse.error(ErrorCode.FILE_UPLOAD_ERROR);
}
}

// 프로필 사진 조회
@GetMapping
public ApiResponse<Map<String, String>> getProfileImageUrl(HttpSession session) {
String email = (String) session.getAttribute("email");
if (email == null) {
return ApiResponse.error(ErrorCode.UNAUTHORIZED);
}

Map<String, String> urls = s3Service.getProfileImageUrl(email);
return ApiResponse.success(SuccessCode.OK, urls);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.market.saessag.domain.photo.dto;

import com.market.saessag.domain.user.entity.User;
import lombok.Getter;

// 프로필 사진 조회
@Getter
public class UserProfileImageResponse {
private String profileUrl;

public static UserProfileImageResponse from(User user) {
UserProfileImageResponse response = new UserProfileImageResponse();
response.profileUrl = user.getProfileUrl();
return response;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.market.saessag.domain.photo.service;

import com.market.saessag.domain.user.entity.User;
import com.market.saessag.domain.user.repository.UserRepository;
import com.market.saessag.global.exception.CustomException;
import com.market.saessag.global.exception.ErrorCode;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -17,6 +23,7 @@
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand All @@ -34,6 +41,9 @@ public class S3Service {
private S3Client s3Client;
private S3Presigner s3Presigner;

@Autowired
private UserRepository userRepository;

@PostConstruct
public void init() {
String accessKey = System.getenv("AWS_ACCESS_KEY");
Expand Down Expand Up @@ -98,4 +108,27 @@ public Map<String, String> getPresignedUrl(List<String> keys) {
}
));
}

public String uploadProfileImage(MultipartFile file, String email) throws IOException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));

String fileUrl = uploadFile(file);
user.setProfileUrl(fileUrl);
userRepository.save(user);

return fileUrl;
}

public Map<String, String> getProfileImageUrl(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));

if (user.getProfileUrl() == null) {
throw new CustomException(ErrorCode.PROFILE_IMAGE_NOT_FOUND);
}

return getPresignedUrl(Collections.singletonList(user.getProfileUrl()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import com.market.saessag.domain.product.entity.Product;
import com.market.saessag.domain.product.service.ProductService;
import com.market.saessag.domain.user.dto.SignInResponse;
import com.market.saessag.global.exception.ErrorCode;
import com.market.saessag.global.response.ApiResponse;
import com.market.saessag.global.response.SuccessCode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand All @@ -26,86 +28,62 @@ public ApiResponse<Page<ProductResponse>> searchProducts(
@RequestParam(required = false) String nickname,
@RequestParam(required = false) String sort
) {
// 세션 체크는 인터셉터에서 처리되므로, 여기서는 비즈니스 로직만 처리
Page<ProductResponse> product = productService.searchProducts(page, size, title, nickname, sort);

return ApiResponse.<Page<ProductResponse>>builder()
.status("200")
.data(product)
.build();
// success() 메서드를 사용하여 일관된 응답 형식 유지
return ApiResponse.success(SuccessCode.OK, product);
}

//상세 조회
@GetMapping("/{productId}")
public ApiResponse<ProductResponse> getProductDetail(@PathVariable Long productId,
@GetMapping("/{id}")
public ApiResponse<ProductResponse> getProductDetail(@PathVariable Long id,
@SessionAttribute(name = "user", required = false) SignInResponse user) {

ProductResponse productDetail = productService.getProductDetail(productId);
ProductResponse productDetail = productService.getProductDetail(id);
if (user != null) {
productService.incrementView(productId, user.getId());
productService.incrementView(id, user.getId());
}

return ApiResponse.<ProductResponse>builder()
.status("200")
.data(productDetail)
.build();
return ApiResponse.success(SuccessCode.OK, productDetail);
}

//상품 생성
@PostMapping
public ApiResponse<ProductResponse> createProduct(@RequestBody ProductRequest productRequest) {
ProductResponse createdProduct = productService.createProduct(productRequest);
return ApiResponse.<ProductResponse>builder()
.status("201")
.data(createdProduct)
.build();
return ApiResponse.success(SuccessCode.PRODUCT_CREATED, createdProduct);
}

//상품 수정
@PutMapping("/{productId}")
@PutMapping("/{id}")
public ApiResponse<ProductResponse> updateProduct(@PathVariable Long productId,
@RequestBody ProductRequest productRequest) {
ProductResponse updatedProduct = productService.updateProduct(productId, productRequest);
return ApiResponse.<ProductResponse>builder()
.status("200")
.data(updatedProduct)
.build();
return ApiResponse.success(SuccessCode.OK, updatedProduct);
}

//상품 삭제
@DeleteMapping("/{productId}")
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteProduct(@PathVariable Long productId) {
boolean isDeleted = productService.deleteProduct(productId);
if (!isDeleted) {
return ApiResponse.<Void>builder()
.status("404")
.data(null)
.build();
return ApiResponse.error(ErrorCode.PRODUCT_NOT_FOUND);
}

return ApiResponse.<Void>builder()
.status("204")
.data(null)
.build();
return ApiResponse.success(SuccessCode.NO_CONTENT, null);
}

// 상품 좋아요
@PostMapping("/{productId}/like")
@PostMapping("/{id}/like")
public ApiResponse<Void> likeProduct(@PathVariable Long productId, @SessionAttribute(name = "user") SignInResponse user) {
productService.likeProduct(productId, user.getId());

return ApiResponse.<Void>builder()
.status("200")
.data(null)
.build();
return ApiResponse.success(SuccessCode.OK, null);
}

@PostMapping("/bump")

public ApiResponse<?> bumpProduct(@RequestParam Long productId, @SessionAttribute(name = "user") SignInResponse user) {
Product product = productService.bumpProduct(productId, user.getId());
return ApiResponse.builder()
.status("200")
.data(product.getId())
.build();
return ApiResponse.success(SuccessCode.OK, product.getId());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import com.market.saessag.domain.user.entity.User;
import com.market.saessag.domain.user.repository.UserRepository;
import com.market.saessag.domain.user.dto.UserResponse;
import com.market.saessag.global.exception.CustomException;
import com.market.saessag.global.exception.ErrorCode;
import com.market.saessag.util.TimeUtils;
import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -80,21 +82,32 @@ public boolean deleteProduct(Long productId) {
}

public Page<ProductResponse> searchProducts(int page, int size, String title, String nickname, String sort) {
Sort sorting = (sort == null || sort.isEmpty()) ?
Sort.by(
Sort.Order.desc("bumpAt"),
Sort.Order.desc("addedDate")
) : Sort.by(Sort.Order.by(sort));

Pageable pageable = PageRequest.of(page, size, sorting);

if (title != null) {
return productRepository.findByTitleContaining(title, pageable).map(this::convertToDTO);
} else if (nickname != null) {
User user = userRepository.findByNickname(nickname);
return productRepository.findByUser(user, pageable).map(this::convertToDTO);
} else {
return productRepository.findAll(pageable).map(this::convertToDTO);
try {
Sort sorting = (sort == null || sort.isEmpty()) ?
Sort.by(
Sort.Order.desc("bumpAt"),
Sort.Order.desc("addedDate")
) : Sort.by(Sort.Order.by(sort));

Pageable pageable = PageRequest.of(page, size, sorting);

Page<ProductResponse> result;
if (title != null) {
result = productRepository.findByTitleContaining(title, pageable).map(this::convertToDTO);
} else if (nickname != null) {
User user = userRepository.findByNickname(nickname);
if (user == null) {
throw new CustomException(ErrorCode.USER_NOT_FOUND);
}
result = productRepository.findByUser(user, pageable).map(this::convertToDTO);
} else {
result = productRepository.findAll(pageable).map(this::convertToDTO);
}
return result;
} catch (IllegalArgumentException e) {
throw new CustomException(ErrorCode.INVALID_ARGUMENT);
} catch (Exception e) {
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import com.market.saessag.domain.user.dto.SignInRequest;
import com.market.saessag.domain.user.dto.SignInResponse;
import com.market.saessag.domain.user.entity.User;
import com.market.saessag.domain.user.repository.UserRepository;
import com.market.saessag.domain.user.service.SignInService;
import com.market.saessag.global.response.ApiResponse;
import com.market.saessag.global.response.SuccessCode;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

Expand All @@ -16,6 +20,7 @@
public class SignInController {

private final SignInService signInService;
private final UserRepository userRepository;

@PostMapping("/sign-in")
public ResponseEntity<ApiResponse<SignInResponse>> signIn(
Expand All @@ -24,12 +29,10 @@ public ResponseEntity<ApiResponse<SignInResponse>> signIn(
SignInResponse signInResponse = signInService.signIn(signInRequest);
session.setAttribute("user", signInResponse); // 세션에 로그인 정보 저장

ApiResponse<SignInResponse> response = ApiResponse.<SignInResponse>builder()
.status("200")
.data(signInResponse)
.build();
User user = userRepository.findByEmail(signInRequest.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
session.setAttribute("email", user.getEmail());

return ResponseEntity.ok(response);
return ResponseEntity.ok(ApiResponse.success(SuccessCode.OK, signInResponse));
}

}
Loading

0 comments on commit a4f5b5b

Please sign in to comment.