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
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ public enum ErrorCode {
USER_EMAIL_DUPLICATED_EXCEPTION(HttpStatus.CONFLICT, "U-001", "이미 사용중인 이메일입니다."),
USER_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "U-002", "해당 유저를 찾을 수 없습니다."),
USER_PASSWORD_MISMATCH_EXCEPTION(HttpStatus.UNAUTHORIZED, "U-003", "비밀번호가 일치하지 않습니다."),
USER_COIN_NOT_ENOUGH_EXCEPTION(HttpStatus.BAD_REQUEST, "U-004", "코인이 부족합니다."),
USER_COIN_TRANSFER_SAME_USER_EXCEPTION(HttpStatus.BAD_REQUEST, "U-005", "자신에게 코인을 송금할 수 업습니다."),
USER_COIN_TRANSFER_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "U-006", "코인 송금에 실패하였습니다."),
USER_IMAGE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "U-007", "해당 유저의 프로필 이미지를 찾을 수 없습니다."),
USER_UPDATE_TOKEN_INVALID_EXCEPTION(HttpStatus.BAD_REQUEST, "U-008", "토큰이 유효하지 않습니다."),
WITHDRAWN_USER_EXCEPTION(HttpStatus.BAD_REQUEST, "U-009", "탈퇴한 유저입니다."),
Expand Down Expand Up @@ -111,6 +108,12 @@ public enum ErrorCode {
VOTE_SUMMARY_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "V-007", "투표 집계 정보를 찾을 수 없습니다."),
VOTE_UPDATE_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "V-008", "투표 집계 업데이트에 실패했습니다."),
NO_AVAILABLE_VOTES_EXCEPTION(HttpStatus.NOT_FOUND, "V-009", "참여 가능한 투표가 없습니다."),

// Account
ACCOUNT_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "ACC-001", "해당 계좌을 찾을 수 없습니다."),
COIN_NOT_ENOUGH_EXCEPTION(HttpStatus.BAD_REQUEST, "ACC-002", "코인이 부족합니다."),
COIN_TRANSFER_SAME_USER_EXCEPTION(HttpStatus.BAD_REQUEST, "ACC-003", "자신에게 코인을 송금할 수 업습니다."),
Comment on lines +113 to +115
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

에러 메시지의 오타를 수정해주세요.

에러 메시지에 다음과 같은 오타가 있습니다:

  • Line 113: "계좌을" → "계좌를" (올바른 조사 사용)
  • Line 115: "수 업습니다" → "수 없습니다" (오타 수정)

다음 diff를 적용하여 수정하세요:

-	ACCOUNT_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "ACC-001", "해당 계좌을 찾을 수 없습니다."),
+	ACCOUNT_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "ACC-001", "해당 계좌를 찾을 수 없습니다."),
 	COIN_NOT_ENOUGH_EXCEPTION(HttpStatus.BAD_REQUEST, "ACC-002", "코인이 부족합니다."),
-	COIN_TRANSFER_SAME_USER_EXCEPTION(HttpStatus.BAD_REQUEST, "ACC-003", "자신에게 코인을 송금할 수 업습니다."),
+	COIN_TRANSFER_SAME_USER_EXCEPTION(HttpStatus.BAD_REQUEST, "ACC-003", "자신에게 코인을 송금할 수 없습니다."),
🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/common/error/ErrorCode.java around lines
113 to 115, there are typos in the Korean error messages: change "해당 계좌을 찾을 수
없습니다." to "해당 계좌를 찾을 수 없습니다." on line 113, and change "자신에게 코인을 송금할 수 업습니다." to
"자신에게 코인을 송금할 수 없습니다." on line 115; update the string literals accordingly and
keep rest of the enum entry formatting unchanged.

COIN_TRANSFER_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "ACC-004", "코인 송금에 실패하였습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@

import hanium.modic.backend.common.error.exception.AppException;
import hanium.modic.backend.common.error.exception.LockException;
import hanium.modic.backend.domain.ai.aiChat.entity.AiChatRoomEntity;
import hanium.modic.backend.domain.ai.aiChat.repository.AiChatRoomRepository;
import hanium.modic.backend.domain.notification.dto.NotificationPayload;
import hanium.modic.backend.domain.notification.enums.NotificationType;
import hanium.modic.backend.domain.notification.service.NotificationService;
import hanium.modic.backend.domain.user.entity.UserEntity;
import hanium.modic.backend.domain.user.repository.UserEntityRepository;
import hanium.modic.backend.infra.redis.distributedLock.LockManager;
import hanium.modic.backend.domain.ai.aiChat.entity.AiChatRoomEntity;
import hanium.modic.backend.domain.ai.aiChat.repository.AiChatRoomRepository;
import hanium.modic.backend.domain.post.entity.PostEntity;
import hanium.modic.backend.domain.post.repository.PostEntityRepository;
import hanium.modic.backend.domain.ticket.service.TicketService;
import hanium.modic.backend.domain.user.service.UserCoinService;
import hanium.modic.backend.domain.transaction.service.AccountService;
import hanium.modic.backend.domain.user.entity.UserEntity;
import hanium.modic.backend.domain.user.repository.UserEntityRepository;
import hanium.modic.backend.infra.redis.distributedLock.LockManager;
import hanium.modic.backend.web.ai.aiChat.dto.response.GetRemainingGenerationsResponse;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AiImagePermissionService {

private final UserCoinService userCoinService;
private final AccountService accountService;
private final TicketService ticketService;
private final PostEntityRepository postRepository;
private final AiChatRoomRepository aiChatRoomRepository;
Expand All @@ -48,7 +48,7 @@ public void buyAiImagePermissionByCoin(Long userId, Long postId) {
aiChatRoomRepository.upsertAndIncrease(userId, postId, AI_IMAGE_PERMISSION_COUNT);

// 3) 코인 후차감, 코인 거래는 별도의 트랜잭션으로 동작하여 후처리, 예외는 전파
userCoinService.consumeCoin(userId, post.getNonCommercialPrice());
accountService.consumeCoin(userId, post.getNonCommercialPrice());

// 4) 알림
UserEntity user = userEntityRepository.findById(userId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package hanium.modic.backend.domain.profile.service;

import static hanium.modic.backend.common.error.ErrorCode.*;

import java.util.Optional;

import org.springframework.stereotype.Service;
Expand All @@ -9,6 +11,7 @@
import hanium.modic.backend.common.error.exception.AppException;
import hanium.modic.backend.domain.follow.repository.FollowEntityRepository;
import hanium.modic.backend.domain.post.repository.PostEntityRepository;
import hanium.modic.backend.domain.transaction.repository.AccountRepository;
import hanium.modic.backend.domain.user.entity.UserEntity;
import hanium.modic.backend.domain.user.entity.UserImageEntity;
import hanium.modic.backend.domain.user.repository.UserEntityRepository;
Expand All @@ -21,12 +24,19 @@
@Service
@RequiredArgsConstructor
public class ProfileService {

// 유저 관련
private final UserEntityRepository userRepository;
private final PostEntityRepository postRepository;
private final FollowEntityRepository followRepository;
private final UserImageService userImageService;
private final UserImageEntityRepository userImageEntityRepository;

// 포스트 관련
private final PostEntityRepository postRepository;

// 팔로우 관련
private final FollowEntityRepository followRepository;
private final AccountRepository accountRepository;

// 내 프로필 조회(코인 함께 조회)
@Transactional(readOnly = true)
public GetMyProfileResponse getMyProfile(final UserEntity user) {
Expand All @@ -37,6 +47,9 @@ public GetMyProfileResponse getMyProfile(final UserEntity user) {
final Optional<Long> userImageId = userImageEntityRepository.findByUserId(user.getId())
.map(UserImageEntity::getId);
final boolean hasUserImage = userImageUrl.isPresent();
final long coinAmount = accountRepository.findById(user.getId())
.map(account -> account.getCoin())
.orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION));

return new GetMyProfileResponse(
user.getId(),
Expand All @@ -48,7 +61,7 @@ public GetMyProfileResponse getMyProfile(final UserEntity user) {
postCount,
followerCount,
followingCount,
user.getCoin()
coinAmount
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package hanium.modic.backend.domain.transaction.entity;

import static hanium.modic.backend.common.error.ErrorCode.*;

import hanium.modic.backend.common.error.exception.AppException;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "accounts")
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account {

@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

@Column(name = "coin", nullable = false)
private Long coin = 0L;

/**
* 사용자 아이디로 계좌 생성
* 코인은 기본값 0으로 설정
*/
@Builder
private Account(Long userId) {
this.userId = userId;
}

public void addCoin(Long coin) {
if (this.coin + coin < 0) {
throw new AppException(COIN_NOT_ENOUGH_EXCEPTION);
}
this.coin += coin;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package hanium.modic.backend.domain.transaction.repository;

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

import hanium.modic.backend.domain.transaction.entity.Account;

public interface AccountRepository extends JpaRepository<Account, Long> {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package hanium.modic.backend.domain.user.service;
package hanium.modic.backend.domain.transaction.service;

import static hanium.modic.backend.common.error.ErrorCode.*;

Expand All @@ -11,53 +11,59 @@
import hanium.modic.backend.domain.notification.dto.NotificationPayload;
import hanium.modic.backend.domain.notification.enums.NotificationType;
import hanium.modic.backend.domain.notification.service.NotificationService;
import hanium.modic.backend.domain.transaction.entity.Account;
import hanium.modic.backend.domain.transaction.repository.AccountRepository;
import hanium.modic.backend.infra.redis.distributedLock.LockManager;
import hanium.modic.backend.domain.user.entity.UserEntity;
import hanium.modic.backend.domain.user.repository.UserEntityRepository;
import hanium.modic.backend.web.user.dto.response.GetCoinBalanceResponse;
import hanium.modic.backend.web.transaction.dto.response.GetCoinBalanceResponse;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserCoinService {
public class AccountService {

private final UserEntityRepository userEntityRepository;
private final AccountRepository accountRepository;

private final LockManager lockManager;

private final NotificationService notificationService;

private final UserEntityRepository userEntityRepository;

// 코인 잔액 조회
public GetCoinBalanceResponse getCoinBalance(final Long userId) {
UserEntity user = userEntityRepository.findById(userId)
.orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION));
return new GetCoinBalanceResponse(user.getCoin());
Account account = accountRepository.findById(userId)
.orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION));

return new GetCoinBalanceResponse(account.getCoin());
}

// 코인 충전
public void chargeCoin(final long userId, final long coin) {
try {
lockManager.userLock(userId, () -> {
lockManager.accountLock(userId, () -> {
addCoin(userId, coin);
});
} catch (LockException e) {
// Todo: 추후 결제 포함될 시, 결제 취소 로직 필요
throw new AppException(USER_COIN_TRANSFER_FAIL_EXCEPTION);
throw new AppException(COIN_TRANSFER_FAIL_EXCEPTION);
}
}

// 코인 양도
public void transferCoin(final long fromUserId, final long toUserId, long coin) throws AppException {
// 자기 자신에게 양도 불가
if (fromUserId == toUserId) {
throw new AppException(USER_COIN_TRANSFER_SAME_USER_EXCEPTION);
throw new AppException(COIN_TRANSFER_SAME_USER_EXCEPTION);
}

// 받는 사람 존재 확인
UserEntity toUser = userEntityRepository.findById(toUserId)
.orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION));
accountRepository.findById(toUserId)
.orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION));

// 락 걸고 양도 처리
try {
lockManager.multipleUserLock(List.of(fromUserId, toUserId), () -> {
// 출금
Expand All @@ -66,7 +72,7 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin)
addCoin(toUserId, coin);
});
} catch (LockException e) {
throw new AppException(USER_COIN_TRANSFER_FAIL_EXCEPTION);
throw new AppException(COIN_TRANSFER_FAIL_EXCEPTION);
}

// 알림
Expand All @@ -82,20 +88,20 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin)
// 코인 소비
public void consumeCoin(final long userId, long coin) throws AppException {
try {
lockManager.userLock(userId, () -> {
lockManager.accountLock(userId, () -> {
addCoin(userId, -coin);
});
} catch (LockException e) {
throw new AppException(USER_COIN_TRANSFER_FAIL_EXCEPTION);
throw new AppException(COIN_TRANSFER_FAIL_EXCEPTION);
}
}

// 코인 추가, 트랜잭션은 lockManager에 의해 관리됨
private void addCoin(final long userId, final long coin) {
UserEntity user = userEntityRepository.findById(userId)
.orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION));
Account account = accountRepository.findById(userId)
.orElseThrow(() -> new AppException(ACCOUNT_NOT_FOUND_EXCEPTION));

user.addCoin(coin);
userEntityRepository.save(user);
account.addCoin(coin);
accountRepository.save(account);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ public class UserEntity extends BaseEntity {
@Enumerated(EnumType.STRING)
private UserRole userRole = UserRole.USER;

@Column(name = "coin", nullable = false)
private Long coin = 0L;

@Builder
private UserEntity(String email, String password, String name, String uniqueId) {
this.email = email;
Expand All @@ -61,13 +58,6 @@ private String generateTemporaryPassword() {
return "OAUTH_" + java.util.UUID.randomUUID().toString().replace("-", "");
}

public void addCoin(Long coin) {
if (this.coin + coin < 0) {
throw new AppException(USER_COIN_NOT_ENOUGH_EXCEPTION);
}
this.coin += coin;
}

public UserEntity update(String email, String name) {
this.email = email;
this.name = name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import hanium.modic.backend.domain.auth.service.AuthService;
import hanium.modic.backend.domain.auth.service.component.EmailSender;
import hanium.modic.backend.domain.auth.service.dto.EmailDto;
import hanium.modic.backend.domain.transaction.entity.Account;
import hanium.modic.backend.domain.transaction.repository.AccountRepository;
import hanium.modic.backend.domain.user.entity.UserEntity;
import hanium.modic.backend.domain.user.entity.UserUpdateToken;
import hanium.modic.backend.domain.user.repository.UserEntityRepository;
Expand All @@ -37,6 +39,9 @@ public class UserService {
// 인증 관련
private final AuthService authService;

// 계좌 관련
private final AccountRepository accountRepository;

// 기타
private final BCryptPasswordEncoder passwordEncoder;
private final EmailSender emailSender;
Expand All @@ -52,15 +57,23 @@ public UserCreateResponse createUser(
checkDuplicateEmail(email);
authService.checkEmailCodeAndDelete(email, code);

// 비밀번호 암호화
final String encodedPassword = passwordEncoder.encode(password);

// 유저 엔티티 생성 및 저장
final UserEntity user = UserEntity.builder()
.email(email)
.password(encodedPassword)
.name(name)
.build();
userEntityRepository.save(user);

// 계좌 생성 및 저장
Account account = Account.builder()
.userId(user.getId())
.build();
accountRepository.save(account);

return UserCreateResponse.from(user);
}

Expand All @@ -73,16 +86,18 @@ private void checkDuplicateEmail(final String email) {

// 회원탈퇴
@Transactional
public void deleteAndLogout(final long id, final String refreshToken, final String accessToken) {
public void deleteAndLogout(final long userId, final String refreshToken, final String accessToken) {
// 소프트 삭제 처리
UserEntity user = userEntityRepository.findById(id)
UserEntity user = userEntityRepository.findById(userId)
.orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION));
user.softWithdraw();

userEntityRepository.save(user);

// 관련 토큰 삭제
authService.logout(refreshToken, accessToken);

// 계좌 삭제, 추후 해당 userId로 코인 거래시 not found 예외 발생
accountRepository.deleteById(userId);
}

// 회원 정보 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ public class LockManager {
private final String AI_PERMISSION_PREFIX = "lock:ai:perm:";
private final String VOTE_SUMMARY_PREFIX = "lock:vote:summary:";
private final String VOTE_STREAK_PREFIX = "lock:vote:streak:";
private final String ACCOUNT_PREFIX = "lock:account:";

public void userLock(long userId, Runnable block) throws LockException {
exec.withLock(USER_PREFIX + userId, block);
public void accountLock(long userId, Runnable block) throws LockException {
exec.withLock(ACCOUNT_PREFIX + userId, block);
}

public void multipleUserLock(List<Long> userIds, Runnable block) throws LockException {
Expand Down
Loading