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
4 changes: 3 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 🔍️ 이 PR을 통해 해결하려는 문제가 무엇인가요?

- closed #{이슈번호입력}
>관련 이슈 번호를 할당해주세요
>
>어떤 기능을 구현한건지, 이슈 대응이라면 어떤 이슈인지 PR이 열리게 된 계기와 목적을 Reviewer 들이 쉽게 이해할 수 있도록 적어 주세요
>일감 백로그 링크나 다이어그램, 피그마를 첨부해도 좋아요

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/cicd-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
echo "${{ secrets.CD_APPLICATION_NAVER }}" > ./src/main/resources/application-naver.yml
echo "${{ secrets.CD_APPLICATION_OATH }}" > ./src/main/resources/application-oath.yml

- name: Generate Apple p8 key
run: |
mkdir -p ./src/main/resources/key
echo "${{ secrets.APPLE_P8 }}" > ./src/main/resources/key/AuthKey_JDTJBM88K5.p8

- name: Build Project
run: ./gradlew clean build -x test

Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework.boot:spring-boot-starter-aop'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
import com.spoony.spoony_server.domain.user.Platform;
import jakarta.validation.constraints.NotNull;

public record PlatformRequestDTO(@NotNull(message = "플랫폼은 필수 값입니다.") Platform platform) {}
public record PlatformRequestDTO(@NotNull(message = "플랫폼은 필수 값입니다.") Platform platform,
String authCode) {}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public record UserSignupDTO(@NotNull(message = "플랫폼은 필수 값입니다
@NotNull(message = "사용자 이름은 필수 값입니다.") String userName,
LocalDate birth,
Long regionId,
String introduction) {
String introduction,
String authCode) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import com.spoony.spoony_server.adapter.auth.dto.response.LoginResponseDTO;
import com.spoony.spoony_server.adapter.auth.dto.response.UserTokenDTO;
import com.spoony.spoony_server.application.auth.port.in.*;
import com.spoony.spoony_server.domain.user.Platform;
import com.spoony.spoony_server.global.auth.annotation.UserId;
import com.spoony.spoony_server.global.auth.constant.AuthConstant;
import com.spoony.spoony_server.global.dto.ResponseDTO;
import io.micrometer.common.lang.Nullable;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
Expand Down Expand Up @@ -45,9 +43,9 @@ public ResponseEntity<ResponseDTO<UserTokenDTO>> signup(
@Operation(summary = "로그인 API", description = "사용자 로그인 API, 성공 시 Token Set, 실패 시 회원가입이 필요합니다.")
public ResponseEntity<ResponseDTO<LoginResponseDTO>> login(
@NotBlank @RequestHeader(AuthConstant.AUTHORIZATION_HEADER) final String platformToken,
@RequestBody final PlatformRequestDTO platformRequestDTO
@Valid @RequestBody final PlatformRequestDTO platformRequestDTO
) {
return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.success(loginUseCase.login(platformRequestDTO.platform(), platformToken)));
return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.success(loginUseCase.login(platformRequestDTO, platformToken)));
}

@PostMapping("/logout")
Expand All @@ -62,10 +60,9 @@ public ResponseEntity<ResponseDTO<Void>> logout(
@PostMapping("/withdraw")
@Operation(summary = "회원 탈퇴 API", description = "마이페이지 > 설정에서 회원 탈퇴합니다.")
public ResponseEntity<ResponseDTO<Void>> withdraw(
@UserId Long userId,
@Nullable @RequestHeader(value = AuthConstant.APPLE_WITHDRAW_HEADER, required = false) final String authCode
@UserId Long userId
) {
withdrawUseCase.withdraw(userId, authCode);
withdrawUseCase.withdraw(userId);
return ResponseEntity.status(HttpStatus.OK).body(ResponseDTO.success(null));
}

Expand All @@ -74,7 +71,6 @@ public ResponseEntity<ResponseDTO<Void>> withdraw(
public ResponseEntity<ResponseDTO<JwtTokenDTO>> refreshAccessToken(
@NotBlank @RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String refreshToken
) {
System.out.println("초기 요청 토큰 " + refreshToken);
if (refreshToken.startsWith(BEARER_TOKEN_PREFIX)) {
refreshToken = refreshToken.substring(BEARER_TOKEN_PREFIX.length());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.spoony.spoony_server.adapter.auth.out.persistence;

import com.spoony.spoony_server.adapter.out.persistence.user.db.AppleRefreshTokenEntity;
import com.spoony.spoony_server.adapter.out.persistence.user.db.AppleRefreshTokenRepository;
import com.spoony.spoony_server.application.auth.port.out.AppleRefreshTokenPort;
import com.spoony.spoony_server.global.annotation.Adapter;
import com.spoony.spoony_server.global.exception.BusinessException;
import com.spoony.spoony_server.global.message.business.UserErrorMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Adapter
@RequiredArgsConstructor
public class AppleRefreshTokenAdapter implements AppleRefreshTokenPort {

private final AppleRefreshTokenRepository appleRefreshTokenRepository;

@Override
@Transactional
public void upsert(Long userId, String refreshToken) {
appleRefreshTokenRepository.findById(userId)
.ifPresentOrElse(entity -> {
entity.setRefreshToken(refreshToken);
}, () -> {
AppleRefreshTokenEntity newEntity = AppleRefreshTokenEntity.builder()
.userId(userId)
.refreshToken(refreshToken)
.build();
appleRefreshTokenRepository.save(newEntity);
});
}

@Override
@Transactional(readOnly = true)
public Optional<String> findRefreshTokenByUserId(Long userId) {
return appleRefreshTokenRepository.findById(userId).map(AppleRefreshTokenEntity::getRefreshToken);
}

@Override
@Transactional
public void revoke(Long userId) {
AppleRefreshTokenEntity entity = appleRefreshTokenRepository.findById(userId)
.orElseThrow(() -> new BusinessException(UserErrorMessage.USER_NOT_FOUND));
appleRefreshTokenRepository.delete(entity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public String createClientSecret() throws IOException {
.setExpiration(expirationDate) // 만료 시간
.setAudience(AUDIENCE) // aud
.setSubject(clientId) // sub
.signWith(SignatureAlgorithm.ES256, getPrivateKey())
.signWith(getPrivateKey(), SignatureAlgorithm.ES256)
.compact();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import com.spoony.spoony_server.application.port.out.post.PostPort;
import com.spoony.spoony_server.domain.post.*;
import com.spoony.spoony_server.domain.user.AgeGroup;
import com.spoony.spoony_server.domain.user.User;
import com.spoony.spoony_server.global.annotation.Adapter;
import com.spoony.spoony_server.global.exception.BusinessException;
import com.spoony.spoony_server.global.message.business.CategoryErrorMessage;
Expand Down Expand Up @@ -187,22 +186,6 @@ public List<Category> findFoodCategories() {
.toList();
}

@Override
public void saveScoopPost(User user, Post post) {
UserEntity userEntity = userRepository.findById(user.getUserId())
.orElseThrow(() -> new BusinessException(UserErrorMessage.USER_NOT_FOUND));

PostEntity postEntity = postRepository.findById(post.getPostId())
.orElseThrow(() -> new BusinessException(PostErrorMessage.POST_NOT_FOUND));

ScoopPostEntity scoopPostEntity = ScoopPostEntity.builder()
.user(userEntity)
.post(postEntity)
.build();

scoopPostRepository.save(scoopPostEntity);
}

@Override
public void deleteById(Long postId) {
postRepository.deleteById(postId);
Expand Down Expand Up @@ -339,4 +322,14 @@ public List<Post> findPostsByUserId(Long userId, int page, int size) {
public int countPostsByUserId(Long userId) {
return postRepository.countByUserId(userId);
}

@Override
public boolean insertScoopIfAbsent(Long userId, Long postId) {
return scoopPostRepository.insertIfAbsent(userId, postId) == 1;
}

@Override
public void deleteScoop(Long userId, Long postId) {
scoopPostRepository.deleteOne(userId, postId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,6 @@ public void updatePostContent(String description, Double value, String cons) {
this.cons = cons;
}

public void updateZzimCount(long delta) {
if (this.zzimCount == null) {
this.zzimCount = 0L;
} else {
this.zzimCount += delta;
}
}

// 논리적 삭제
public void markDeleted() {
this.isDeleted = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.spoony.spoony_server.adapter.out.persistence.post.db;

import jakarta.persistence.LockModeType;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

Expand All @@ -18,6 +17,17 @@ public interface PostRepository extends JpaRepository<PostEntity, Long> , JpaSpe
Long countByUser_UserId(@Param("userId") Long userId,
@Param("reportedPostIds") List<Long> reportedPostIds);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from PostEntity p where p.postId = :postId")
Optional<PostEntity> findByIdForUpdate(@Param("postId") Long postId);

@Modifying
@Query("update PostEntity p set p.zzimCount = coalesce(p.zzimCount, 0) + 1 where p.postId = :postId")
void incrementZzimCount(@Param("postId") Long postId);

@Modifying
@Query("update PostEntity p set p.zzimCount = case when coalesce(p.zzimCount, 0) > 0 then p.zzimCount - 1 else 0 end where p.postId = :postId")
void decrementZzimCount(@Param("postId") Long postId);

//Long countByUser_UserId(Long userId);
List<PostEntity> findByDescriptionContaining(String query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "scoop_post")
@Table(name = "scoop_post",
uniqueConstraints = @UniqueConstraint(
name = "uk_scoop_user_post",
columnNames = {"user_id", "post_id"}
)
)
public class ScoopPostEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public Activity findActivityByActivityId(Long activityId) {
return ActivityMapper.toDomain(activityEntity);
}

@Override
public boolean decrementIfEnough(Long userId, int amount) {
return spoonBalanceRepository.decrementIfEnough(userId, amount, LocalDateTime.now()) == 1;
}

@Override
public void updateSpoonBalance(User user, int amount) {
SpoonBalanceEntity spoonBalanceEntity = spoonBalanceRepository.findByUser_UserId(user.getUserId())
Expand Down Expand Up @@ -77,15 +82,6 @@ public void updateSpoonHistory(User user, int amount) {
spoonHistoryRepository.save(spoonHistoryEntity);
}

@Override
public void updateSpoonBalanceByActivity(User user, Activity activity) {
SpoonBalanceEntity spoonBalanceEntity = spoonBalanceRepository.findByUser_UserId(user.getUserId())
.orElseThrow(() -> new BusinessException(SpoonErrorMessage.USER_NOT_FOUND));
spoonBalanceEntity.setAmount(spoonBalanceEntity.getAmount() + activity.getChangeAmount());
spoonBalanceEntity.setUpdatedAt(LocalDateTime.now());
spoonBalanceRepository.save(spoonBalanceEntity);
}

@Override
public void updateSpoonHistoryByActivity(User user, Activity activity) {
SpoonBalanceEntity spoonBalanceEntity = spoonBalanceRepository.findByUser_UserId(user.getUserId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

import com.spoony.spoony_server.adapter.out.persistence.post.db.ScoopPostEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ScoopPostRepository extends JpaRepository<ScoopPostEntity, Long> {
boolean existsByUser_UserIdAndPost_PostId(Long userId, Long postId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = "INSERT IGNORE INTO scoop_post (user_id, post_id) VALUES (:uid, :pid)", nativeQuery = true)
int insertIfAbsent(@Param("uid") Long userId, @Param("pid") Long postId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM ScoopPostEntity sp WHERE sp.user.userId = :uid AND sp.post.postId = :pid")
void deleteOne(@Param("uid") Long userId, @Param("pid") Long postId);
}


Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
package com.spoony.spoony_server.adapter.out.persistence.spoon.db;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.Optional;

public interface SpoonBalanceRepository extends JpaRepository<SpoonBalanceEntity, Long> {
Optional<SpoonBalanceEntity> findByUser_UserId(Long userId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE SpoonBalanceEntity sb " + " SET sb.amount = sb.amount - :amount, " + " sb.updatedAt = :now " +
" WHERE sb.user.userId = :userId " + " AND sb.amount >= :amount ")
int decrementIfEnough(@Param("userId") Long userId,
@Param("amount") int amount,
@Param("now") LocalDateTime now);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.spoony.spoony_server.adapter.out.persistence.user.db;

import com.spoony.spoony_server.global.auth.encryptor.AppleRefreshTokenEncryptor;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Table(name = "apple_refresh_token")
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
@Getter
@Setter
@Builder
@AllArgsConstructor
public class AppleRefreshTokenEntity {

@Id
@Column(name = "user_id", nullable = false)
private Long userId;

@Convert(converter = AppleRefreshTokenEncryptor.class)
@Column(name = "refresh_token", nullable = false, length = 2048)
private String refreshToken;

@CreatedDate
private LocalDateTime createdAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.spoony.spoony_server.adapter.out.persistence.user.db;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AppleRefreshTokenRepository extends JpaRepository<AppleRefreshTokenEntity, Long> {
}
Loading