Skip to content
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

feat: 회원 프로필 이미지 기능 구현 #78

Merged
merged 20 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7c21e9d
feat: 회원 테이블에 프로필 이미지 url 컬럼 추가
junest66 Oct 5, 2024
2cd2c60
feat: 회원 테이블에 프로필 이미지 url 컬럼 추가로 인한 flyway 스크립트 추가
junest66 Oct 5, 2024
06299de
feat: 프로필 수정 DTO에 기본이미지를 사용할지 여부 필드 추가
junest66 Oct 5, 2024
6529730
refactor: imageService uploadImage 함수 리턴값 수정
junest66 Oct 5, 2024
b58e743
refactor: 회원수정에 프로필 이미지 추가로 인한 서비스 함수 수정
junest66 Oct 5, 2024
0add174
test: 회원수정에 프로필 이미지 추가로 인한 서비스 함수 테스트 코드 수정
junest66 Oct 5, 2024
513b9cf
refactor: 회원수정 컨트롤러 요청 값 수정
junest66 Oct 5, 2024
0bce3e2
style: 가독성을 위한 함수 순서 변경
junest66 Oct 5, 2024
22c33c0
delete: 쓰지 않는 파일 제거
junest66 Oct 5, 2024
c90f6f4
refactor: 경매 상세정보 응답에 판매자의 프로필 이미지 필드 추가
junest66 Oct 5, 2024
6773217
refactor: 경매 상세정보 응답에 판매자의 프로필 이미지 필드 추가로 인한 쿼리 수정
junest66 Oct 5, 2024
6ab6987
test: 경매 상세정보 응답에 판매자의 프로필 이미지 필드 추가로 인한 테스트 코드 수정
junest66 Oct 5, 2024
6a573ab
refactor: 사전 경매 제품 상세정보 응답에 판매자의 프로필 이미지 필드 추가
junest66 Oct 5, 2024
1b6c770
refactor: 사전 경매 제품 상세정보 응답에 판매자의 프로필 이미지 필드 추가로 인한 쿼리 수정
junest66 Oct 5, 2024
831806b
refactor: 프로필 조회 응답 DTO에 프로필 이미지 url 추가
junest66 Oct 5, 2024
8ec73b3
style: 가독성을 위한 함수 순서 수정
junest66 Oct 5, 2024
45c4ef3
refactor: 프로필 조회에 providerType 추가
junest66 Oct 5, 2024
c373f8b
refactor: 프로필 조회에 providerType 추가로 인한 서비스 함수 수정
junest66 Oct 5, 2024
78dbe79
chore: jira bot이 트리거하는 github actor 이름을 알아내기 위한 action 출력문 추가
junest66 Oct 5, 2024
9490058
chore: 잘못된 yml 문법 수정
junest66 Oct 5, 2024
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
4 changes: 4 additions & 0 deletions .github/workflows/pr-title-and-branch-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ jobs:
- name: Check out code
uses: actions/checkout@v3

- name: Display GitHub Actor
run: |
echo "Actor: ${{ github.actor }}"

- name: Validate PR Title
uses: Slashgear/[email protected]
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
public class AuctionDetailsResponse {
private final Long productId;
private final String sellerNickname;
private final String sellerProfileImageUrl;
private final String productName;
private final String description;
private final Integer minPrice;
Expand All @@ -26,13 +27,15 @@ public class AuctionDetailsResponse {
private List<String> imageUrls = new ArrayList<>();

@QueryProjection
public AuctionDetailsResponse(Long productId, String sellerNickname, String productName, String description,
public AuctionDetailsResponse(Long productId, String sellerNickname, String sellerProfileImageUrl,
String productName, String description,
Integer minPrice, Category category, Long timeRemaining, AuctionStatus status,
Boolean isSeller,
Long participantCount, Boolean isParticipated, Long bidId, Long bidAmount,
int remainingBidCount) {
this.productId = productId;
this.sellerNickname = sellerNickname;
this.sellerProfileImageUrl = sellerProfileImageUrl;
this.productName = productName;
this.description = description;
this.minPrice = minPrice;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ public Optional<AuctionDetailsResponse> findAuctionDetailsById(Long auctionId, L
.select(new QAuctionDetailsResponse(
product.id,
user.nickname,
user.profileImageUrl,
product.name,
product.description,
product.minPrice,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,17 @@ public List<String> uploadImages(List<MultipartFile> images) {
List<String> uploadedUrls = images.stream()
.map(this::uploadImage)
.toList();

uploadedUrls.forEach(url -> log.info("업로드 된 이미지 : {}", cloudfrontDomain + "/" + url));

log.info("업로드 된 이미지 리스트: {}", uploadedUrls);
return uploadedUrls;
}

/**
* 단일 이미지 파일 업로드 및 CDN 경로 리스트 반환
* 단일 이미지 파일 업로드 및 CDN 전체경로 리스트 반환
*/
private String uploadImage(MultipartFile image) {
public String uploadImage(MultipartFile image) {
String uniqueFileName = createUniqueFileName(Objects.requireNonNull(image.getOriginalFilename()));

return imageUploader.uploadImage(image, uniqueFileName);
String s3Key = imageUploader.uploadImage(image, uniqueFileName);
return cloudfrontDomain + "/" + s3Key;
}

/**
Expand All @@ -66,7 +64,7 @@ private String uploadImage(MultipartFile image) {
public List<Image> saveProductImageEntities(Product product, List<String> cdnPaths) {
List<Image> images = cdnPaths.stream()
.map(cdnPath -> Image.builder()
.cdnPath(cloudfrontDomain + "/" + cdnPath)
.cdnPath(cdnPath)
.product(product)
.build())
.toList();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package org.chzz.market.domain.product.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.List;
import lombok.Getter;
import org.chzz.market.domain.product.entity.Product.Category;

/**
Expand All @@ -15,6 +14,7 @@ public class ProductDetailsResponse {
private final Long productId;
private final String productName;
private final String sellerNickname;
private final String sellerProfileImageUrl;
private final Integer minPrice;
private final LocalDateTime createdAt;
private final String description;
Expand All @@ -26,11 +26,13 @@ public class ProductDetailsResponse {

@QueryProjection
public ProductDetailsResponse(Long productId, String productName, String sellerNickname,
String sellerProfileImageUrl,
Integer minPrice, LocalDateTime createdAt, String description,
Long likeCount, Boolean isLiked, Boolean isSeller, Category category) {
this.productId = productId;
this.productName = productName;
this.sellerNickname = sellerNickname;
this.sellerProfileImageUrl = sellerProfileImageUrl;
this.minPrice = minPrice;
this.createdAt = createdAt;
this.description = description;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public Optional<ProductDetailsResponse> findProductDetailsById(Long productId, L
product.id,
product.name,
user.nickname,
user.profileImageUrl,
product.minPrice,
product.createdAt,
product.description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@
import org.chzz.market.common.util.CookieUtil;
import org.chzz.market.domain.token.entity.TokenType;
import org.chzz.market.domain.token.service.TokenService;
import org.chzz.market.domain.user.dto.response.UpdateProfileResponse;
import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest;
import org.chzz.market.domain.user.dto.request.UserCreateRequest;
import org.chzz.market.domain.user.dto.response.UserProfileResponse;
import org.chzz.market.domain.user.entity.User;
import org.chzz.market.domain.user.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RequiredArgsConstructor
@RestController
Expand All @@ -37,66 +37,70 @@ public class UserController {
private final UserService userService;
private final TokenService tokenService;

/*
* 회원가입 완료
/**
* 내 customerKey 조회
*/
@PostMapping
public ResponseEntity<?> completeRegistration(@LoginUser Long userId,
@Valid @RequestBody UserCreateRequest userCreateRequest,
HttpServletResponse response) {
User user = userService.completeUserRegistration(userId, userCreateRequest);
// 임시토큰 만료
CookieUtil.expireCookie(response, TokenType.TEMP.name());
// 리프레쉬 토큰 발급
CookieUtil.createTokenCookie(response, tokenService.createRefreshToken(user), TokenType.REFRESH);
// 엑세스 토큰 발급
response.setHeader(AUTHORIZATION_HEADER, BEARER_TOKEN_PREFIX + tokenService.createAccessToken(user));
log.info("최종 회원가입 성공 userId = {}", userId);
return ResponseEntity.ok().build();
}

@GetMapping("/customer-key")
public ResponseEntity<?> getCustomerKey(@LoginUser Long userId) {
String customerKey = userService.getCustomerKey(userId);
return ResponseEntity.ok(Map.of("customerKey", customerKey));
}

/**
* 내 프로필 수정
*/
@PostMapping("/profile")
public ResponseEntity<UpdateProfileResponse> updateUserProfile(
@LoginUser Long userId,
@RequestBody @Valid UpdateUserProfileRequest request) {
UpdateProfileResponse response = userService.updateUserProfile(userId, request);
return ResponseEntity.status(HttpStatus.OK).body(response);
}

/*
* 사용자 프로필 조회 (유저 ID 기반)
*/
@GetMapping
public ResponseEntity<UserProfileResponse> getUserProfileById(@LoginUser Long userId) {
return ResponseEntity.ok(userService.getUserProfileById(userId));
}

/*
/**
* 사용자 프로필 조회 (닉네임 기반)
*/
@GetMapping("/{nickname}")
public ResponseEntity<UserProfileResponse> getUserProfileByNickname(@PathVariable String nickname) {
return ResponseEntity.ok(userService.getUserProfileByNickname(nickname));
}

/*
/**
* 닉네임 중복 확인
*/
@GetMapping("/check/nickname/{nickname}")
public ResponseEntity<?> checkNickname(@PathVariable String nickname) {
return ResponseEntity.ok((userService.checkNickname(nickname)));
}

/*
/**
* 회원가입 완료
*/
@PostMapping
public ResponseEntity<?> completeRegistration(@LoginUser Long userId,
@Valid @RequestBody UserCreateRequest userCreateRequest,
HttpServletResponse response) {
User user = userService.completeUserRegistration(userId, userCreateRequest);
// 임시토큰 만료
CookieUtil.expireCookie(response, TokenType.TEMP.name());
// 리프레쉬 토큰 발급
CookieUtil.createTokenCookie(response, tokenService.createRefreshToken(user), TokenType.REFRESH);
// 엑세스 토큰 발급
response.setHeader(AUTHORIZATION_HEADER, BEARER_TOKEN_PREFIX + tokenService.createAccessToken(user));
log.info("최종 회원가입 성공 userId = {}", userId);
return ResponseEntity.ok().build();
}

/**
* 내 프로필 수정
*/
@PostMapping("/profile")
public ResponseEntity<?> updateUserProfile(
@LoginUser Long userId,
@RequestPart(required = false) MultipartFile file,
@RequestPart @Valid UpdateUserProfileRequest request) {
userService.updateUserProfile(userId, file, request);
return ResponseEntity.ok().build();
}

/**
* 토큰 재발급
*/
@PostMapping("/tokens/reissue")
Expand All @@ -110,7 +114,7 @@ public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse
return ResponseEntity.ok().build();
}

/*
/**
* 로그아웃
*/
@PostMapping("/logout")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ public class UpdateUserProfileRequest {

@Pattern(regexp = "^(http|https)://.*$|^$", message = "링크는 유효한 URL 형식이어야 합니다.")
private String link;

@Builder.Default
private Boolean useDefaultImage = false;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
package org.chzz.market.domain.user.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import org.chzz.market.domain.user.entity.User;

public record UserProfileResponse(
String nickname,
String bio,
String link,
String profileImageUrl,
@JsonInclude(JsonInclude.Include.NON_NULL)
String providerType,
ParticipationCountsResponse participantCount,
Long preRegisterCount,
Long registeredAuctionCount
) {
public static UserProfileResponse of(User user,
ParticipationCountsResponse counts,
Long preRegisterCount,
Long registeredAuctionCount) {
Long registeredAuctionCount,
boolean includeProviderType) {
return new UserProfileResponse(
user.getNickname(),
user.getBio(),
user.getLink(),
user.getProfileImageUrl(),
includeProviderType ? user.getProviderType().name() : null,
counts,
preRegisterCount,
registeredAuctionCount
Expand Down
35 changes: 8 additions & 27 deletions src/main/java/org/chzz/market/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,17 @@
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.chzz.market.domain.auction.entity.Auction;
import org.chzz.market.domain.bank_account.entity.BankAccount;
import org.chzz.market.domain.base.entity.BaseTimeEntity;
import org.chzz.market.domain.bid.entity.Bid;
import org.chzz.market.domain.like.entity.Like;
import org.chzz.market.domain.payment.entity.Payment;
import org.chzz.market.domain.product.entity.Product;
import org.chzz.market.domain.user.dto.request.UpdateUserProfileRequest;
import org.chzz.market.domain.user.dto.request.UserCreateRequest;
import org.chzz.market.domain.user.error.exception.UserException;
import org.hibernate.annotations.DynamicUpdate;
Expand Down Expand Up @@ -66,6 +64,8 @@ public class User extends BaseTimeEntity {

private String link;

private String profileImageUrl;

// 구현 방식에 따라 권한 설정이 달라질 수 있어 임의로 열거체 선언 하였습니다
@Column(columnDefinition = "varchar(20)")
@Enumerated(EnumType.STRING)
Expand All @@ -78,10 +78,6 @@ public class User extends BaseTimeEntity {
@Column(columnDefinition = "binary(16)", unique = true, nullable = false)
private UUID customerKey;

@Builder.Default
@OneToMany(mappedBy = "bidder", fetch = FetchType.LAZY)
private List<Bid> bids = new ArrayList<>();

@Builder.Default
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private List<Product> products = new ArrayList<>();
Expand Down Expand Up @@ -128,26 +124,11 @@ public void createUser(UserCreateRequest userCreateRequest) {
}
}

public void updateProfile(String nickname, String bio, String link) {
this.nickname = nickname;
this.bio = bio;
this.link = link;
}

private Stream<Auction> getAuctionsFromBids() {
return bids.stream().map(Bid::getAuction);
}

public long getOngoingAuctionCount() {
return getAuctionsFromBids().filter(Auction::isProceeding).count();
}

public long getSuccessfulBidCount() {
return getAuctionsFromBids().filter(auction -> auction.isSuccessfulBidFor(this.id)).count();
}

public long getFailedBidCount() {
return getAuctionsFromBids().filter(auction -> auction.isFailedBidFor(this.id)).count();
public void updateProfile(UpdateUserProfileRequest request, String profileImageUrl) {
this.nickname = request.getNickname();
this.bio = request.getBio();
this.link = request.getLink();
this.profileImageUrl = profileImageUrl;
}

@Getter
Expand Down
Loading
Loading