Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,62 @@

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.runimo.runimo.auth.service.dto.UserSignupCommand;
import org.runimo.runimo.user.domain.DevicePlatform;
import org.runimo.runimo.user.domain.Gender;
import org.springframework.web.multipart.MultipartFile;

@Schema(description = "사용자 회원가입 요청 DTO")
public record AuthSignupRequest(
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AuthSignupRequest {

@Schema(description = "회원가입용 임시 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI...")
@NotBlank String registerToken,
@NotBlank(message = "회원가입 토큰은 필수입니다")
private String registerToken;

@Schema(description = "사용자 닉네임", example = "RunimoUser")
@NotBlank String nickname,
@NotBlank
String nickname;

@Schema(description = "성별", example = "FEMALE")
Gender gender
) {
private Gender gender;

@Schema(description = "디바이스 토큰", example = "string")
private String deviceToken;

@Schema(description = "디바이스 플랫폼", example = "FCM / APNS")
private String devicePlatform;

public AuthSignupRequest(String registerToken, String nickname, Gender gender) {
this.registerToken = registerToken;
this.nickname = nickname;
this.gender = gender;
}

public UserSignupCommand toUserSignupCommand(MultipartFile file) {
return new UserSignupCommand(registerToken, nickname, file, gender);
if (hasDeviceToken() && (devicePlatform == null || devicePlatform.trim().isEmpty())) {
throw new IllegalArgumentException("디바이스 토큰이 있으면 플랫폼도 필수입니다.");
}
return new UserSignupCommand(
registerToken,
nickname,
file,
gender,
deviceToken,
devicePlatform != null ? DevicePlatform.fromString(devicePlatform) : null
);
}

private boolean hasDeviceToken() {
return deviceToken != null && !deviceToken.trim().isEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.runimo.runimo.user.repository.AppleUserTokenRepository;
import org.runimo.runimo.user.service.UserRegisterService;
import org.runimo.runimo.user.service.dto.command.DeviceTokenDto;
import org.runimo.runimo.user.service.dto.command.UserRegisterCommand;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -43,13 +44,8 @@ public SignupUserResponse register(UserSignupCommand command) {
SignupToken signupToken = findUnExpiredSignupToken(payload.token());
userRegisterService.validateExistingUser(payload.providerId(), payload.socialProvider());
String imgUrl = fileStorageService.storeFile(command.profileImage());
User savedUser = userRegisterService.registerUser(new UserRegisterCommand(
command.nickname(),
imgUrl,
command.gender(),
payload.providerId(),
payload.socialProvider())
);
User savedUser = userRegisterService.registerUser(
mapToUserCreateCommand(payload, imgUrl, command));
if (payload.socialProvider() == SocialProvider.APPLE) {
createAppleUserToken(savedUser.getId(), signupToken);
}
Expand Down Expand Up @@ -78,4 +74,16 @@ private void createAppleUserToken(Long userId, SignupToken signupToken) {
);
appleUserTokenRepository.save(appleUserToken);
}

private UserRegisterCommand mapToUserCreateCommand(SignupTokenPayload payload, String imgUrl,
UserSignupCommand command) {
return new UserRegisterCommand(
command.nickname(),
imgUrl,
command.gender(),
payload.providerId(),
payload.socialProvider(),
DeviceTokenDto.of(command.deviceToken(), command.devicePlatform())
);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.runimo.runimo.auth.service.dto;


import org.runimo.runimo.user.domain.DevicePlatform;
import org.runimo.runimo.user.domain.Gender;
import org.springframework.web.multipart.MultipartFile;

public record UserSignupCommand(
String registerToken,
String nickname,
MultipartFile profileImage,
Gender gender
Gender gender,
String deviceToken,
DevicePlatform devicePlatform
) {

}
12 changes: 12 additions & 0 deletions src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.runimo.runimo.user.domain;

public enum DevicePlatform {
FCM,
APNS,
NONE;


public static DevicePlatform fromString(String value) {
return DevicePlatform.valueOf(value.toUpperCase());
}
Comment on lines +9 to +11
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null safety to the fromString method.

The method will throw a NullPointerException if a null string is passed, since it calls toUpperCase() on the input without null checking.

 public static DevicePlatform fromString(String value) {
+    if (value == null) {
+        throw new IllegalArgumentException("Device platform value cannot be null");
+    }
     return DevicePlatform.valueOf(value.toUpperCase());
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static DevicePlatform fromString(String value) {
return DevicePlatform.valueOf(value.toUpperCase());
}
public static DevicePlatform fromString(String value) {
if (value == null) {
throw new IllegalArgumentException("Device platform value cannot be null");
}
return DevicePlatform.valueOf(value.toUpperCase());
}
🤖 Prompt for AI Agents
In src/main/java/org/runimo/runimo/user/domain/DevicePlatform.java around lines
8 to 10, the fromString method lacks null safety and will throw a
NullPointerException if a null value is passed. Add a null check at the start of
the method to handle null inputs gracefully, either by returning null, throwing
a custom exception, or handling it according to the application's requirements
before calling toUpperCase().

}
60 changes: 60 additions & 0 deletions src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.runimo.runimo.user.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.runimo.runimo.common.CreateUpdateAuditEntity;

@Table(name = "user_token")
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class UserDeviceToken extends CreateUpdateAuditEntity {

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

@JoinColumn(name = "user_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private User user;

@Column(name = "device_token", nullable = false)
private String deviceToken;

@Column(name = "platform", nullable = false)
private DevicePlatform platform;

@Column(name = "notification_allowed", nullable = false)
private Boolean notificationAllowed;

@Column(name = "last_used_at")
private LocalDateTime lastUsedAt;
Copy link
Contributor

Choose a reason for hiding this comment

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

이거는 언제 필요한 정보일까요? 기록용일까요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

최근 알림 지표로 삼을 것 같아서 추가했어요! 알림 대상 테이블은 빠르게 수가 증가할 것 같아서 디바이스토큰에 넣었습니다.


@Column(name = "deleted_at")
private LocalDateTime deletedAt;

public static UserDeviceToken from(String deviceToken, DevicePlatform platform,
Boolean notificationAllowed) {
return UserDeviceToken.builder()
.deviceToken(deviceToken)
.notificationAllowed(notificationAllowed)
.platform(platform)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.runimo.runimo.user.repository;

import org.runimo.runimo.user.domain.UserDeviceToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserDeviceTokenRepository extends JpaRepository<UserDeviceToken, Long> {

}
18 changes: 18 additions & 0 deletions src/main/java/org/runimo/runimo/user/service/UserCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import org.runimo.runimo.user.domain.OAuthInfo;
import org.runimo.runimo.user.domain.SocialProvider;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.domain.UserDeviceToken;
import org.runimo.runimo.user.repository.LovePointRepository;
import org.runimo.runimo.user.repository.OAuthInfoRepository;
import org.runimo.runimo.user.repository.UserDeviceTokenRepository;
import org.runimo.runimo.user.repository.UserRepository;
import org.runimo.runimo.user.service.dto.command.DeviceTokenDto;
import org.runimo.runimo.user.service.dto.command.UserCreateCommand;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -19,6 +22,7 @@ public class UserCreator {
private final UserRepository userRepository;
private final OAuthInfoRepository oAuthInfoRepository;
private final LovePointRepository lovePointRepository;
private final UserDeviceTokenRepository userDeviceTokenRepository;

@Transactional
public User createUser(UserCreateCommand command) {
Expand Down Expand Up @@ -48,4 +52,18 @@ public LovePoint createLovePoint(Long userId) {
.build();
return lovePointRepository.save(lovePoint);
}

@Transactional
public void createUserDeviceToken(User user, DeviceTokenDto deviceTokenDto) {
if (deviceTokenDto.isEmpty()) {
return;
}
UserDeviceToken userDeviceToken = UserDeviceToken.builder()
.user(user)
.deviceToken(deviceTokenDto.token())
.platform(deviceTokenDto.platform())
.notificationAllowed(true)
.build();
userDeviceTokenRepository.save(userDeviceToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public User registerUser(UserRegisterCommand command) {
userCreator.createUserOAuthInfo(savedUser, command.socialProvider(), command.providerId());
userCreator.createLovePoint(savedUser.getId());
userItemCreator.createAll(savedUser.getId());
userCreator.createUserDeviceToken(savedUser, command.deviceToken());
return savedUser;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.runimo.runimo.user.service.dto.command;

import org.runimo.runimo.user.domain.DevicePlatform;

public record DeviceTokenDto(String token, DevicePlatform platform) {

public static final DeviceTokenDto EMPTY = new DeviceTokenDto("", DevicePlatform.NONE);

public static DeviceTokenDto of(String deviceToken, DevicePlatform devicePlatform) {
if (deviceToken == null || deviceToken.isEmpty() || devicePlatform == null
|| devicePlatform == DevicePlatform.NONE) {
return EMPTY;
}
return new DeviceTokenDto(deviceToken, devicePlatform);
}

public boolean isEmpty() {
return this == EMPTY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public record UserRegisterCommand(
String imgUrl,
Gender gender,
@NotNull String providerId,
@NotNull SocialProvider socialProvider
@NotNull SocialProvider socialProvider,
DeviceTokenDto deviceToken
) {

}
14 changes: 9 additions & 5 deletions src/main/resources/sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ CREATE TABLE `users`

CREATE TABLE `user_token`
(
`user_id` BIGINT NOT NULL,
`device_token` VARCHAR(255) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` TIMESTAMP NULL,
`id` BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL,
`user_id` BIGINT NOT NULL,
`device_token` VARCHAR(255) NOT NULL,
`platform` ENUM ('FCM', 'APNS') NOT NULL DEFAULT 'APNS',
`notification_allowed` BOOLEAN NOT NULL DEFAULT TRUE,
`last_used_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` TIMESTAMP NULL,

FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
);
Expand Down
Loading
Loading