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 @@ -40,11 +40,12 @@ public void mergeGuestToUser(String deviceUuid, Long userId) {
User targetUser = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));

if (device.getLanguage() != null) {
// 게스트에서 선택한 언어를 최초 1회만 회원에게 승계
if (targetUser.getLanguage() == null && device.getLanguage() != null) {
targetUser.updateLanguage(device.getLanguage());
}

// 무조건 최신 유저로 업데이트
// 현재 디바이스는 최신 로그인 유저에 연결
device.updateUser(targetUser);

// 데이터 이전
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// src/main/java/com/example/konnect_backend/domain/user/controller/UserController.java
package com.example.konnect_backend.domain.user.controller;

import com.example.konnect_backend.domain.user.dto.ChildDto;
import com.example.konnect_backend.domain.user.dto.ChildUpdateDto;
import com.example.konnect_backend.domain.user.dto.UserInfoDto;
import com.example.konnect_backend.domain.user.dto.*;
import com.example.konnect_backend.domain.user.service.LanguagePreferenceService;
import com.example.konnect_backend.domain.user.service.UserService;
import com.example.konnect_backend.global.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -21,6 +20,7 @@
public class UserController {

private final UserService userService;
private final LanguagePreferenceService languagePreferenceService;

@PostMapping("/children")
@Operation(summary = "자녀 추가", description = "현재 로그인한 사용자에게 자녀를 추가합니다.")
Expand Down Expand Up @@ -53,4 +53,37 @@ public ApiResponse<UserInfoDto> getUserInfo() {
return ApiResponse.onSuccess(userService.getUserInfo());
}

@PatchMapping("/language")
@Operation(
summary = "사용자 언어 변경",
description = """
로그인 상태면 User.language를 변경합니다.
비로그인 상태면 X-Device-Id를 기준으로 Device.language를 변경합니다.
로그인 상태에서 X-Device-Id를 함께 보내면 Device.language도 함께 동기화합니다.
"""
)
public ApiResponse<LanguageResponse> updateLanguage(
@RequestHeader(value = "X-Device-Id", required = false) String deviceUuid,
@Valid @RequestBody UpdateLanguageRequest request
) {
return ApiResponse.onSuccess(
languagePreferenceService.updateLanguage(deviceUuid, request.getLanguage())
);
}

@GetMapping("/language")
@Operation(
summary = "사용자 언어 조회",
description = """
로그인 상태면 User.language를 조회합니다.
비로그인 상태면 X-Device-Id를 기준으로 Device.language를 조회합니다.
"""
)
public ApiResponse<LanguageResponse> getLanguage(
@RequestHeader(value = "X-Device-Id", required = false) String deviceUuid
) {
return ApiResponse.onSuccess(
languagePreferenceService.getLanguage(deviceUuid)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.konnect_backend.domain.user.dto;

import com.example.konnect_backend.domain.user.entity.status.Language;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@AllArgsConstructor
public class LanguageResponse {

private Language language;
private boolean loggedIn;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.konnect_backend.domain.user.dto;

import com.example.konnect_backend.domain.user.entity.status.Language;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class UpdateLanguageRequest {

@NotNull
private Language language;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import com.example.konnect_backend.global.code.status.ErrorStatus;
import com.example.konnect_backend.global.exception.GeneralException;
import lombok.RequiredArgsConstructor;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
public class DeviceService {
Expand All @@ -20,14 +21,11 @@ public class DeviceService {

@Transactional
public void registerDevice(String deviceUuid, Language language) {
if (deviceUuid == null || deviceUuid.isBlank()) {
throw new GeneralException(ErrorStatus.INVALID_DEVICE);
}
validateDeviceUuid(deviceUuid);

deviceRepository.findById(deviceUuid)
.map(device -> {
// 이미 존재하면 language 업데이트
if ( language != null) {
if (language != null) {
device.updateLanguage(language);
}
return device;
Expand All @@ -36,24 +34,50 @@ public void registerDevice(String deviceUuid, Language language) {
deviceRepository.save(
Device.builder()
.deviceUuid(deviceUuid)
.language(language) // 추가
.language(language)
.createdAt(LocalDateTime.now())
.lastUsedAt(LocalDateTime.now())
.build()
)
);
}


@Transactional
public Device findOrCreateDevice(String deviceUuid) {
validateDeviceUuid(deviceUuid);

return deviceRepository.findById(deviceUuid)
.orElseGet(() ->
deviceRepository.save(
Device.builder()
.deviceUuid(deviceUuid)
.createdAt(LocalDateTime.now())
.lastUsedAt(LocalDateTime.now())
.build()
)
);
}

@Transactional
public void updateLanguage(String deviceUuid, Language language) {
validateDeviceUuid(deviceUuid);

Device device = findOrCreateDevice(deviceUuid);
device.updateLanguage(language);
}

@Transactional(readOnly = true)
public Language getLanguage(String deviceUuid) {
validateDeviceUuid(deviceUuid);

return deviceRepository.findById(deviceUuid)
.map(Device::getLanguage)
.orElse(null);
}

private void validateDeviceUuid(String deviceUuid) {
if (deviceUuid == null || deviceUuid.isBlank()) {
throw new GeneralException(ErrorStatus.INVALID_DEVICE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.example.konnect_backend.domain.user.service;

import com.example.konnect_backend.domain.user.dto.LanguageResponse;
import com.example.konnect_backend.domain.user.entity.status.Language;
import com.example.konnect_backend.global.code.status.ErrorStatus;
import com.example.konnect_backend.global.exception.GeneralException;
import com.example.konnect_backend.global.security.SecurityUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LanguagePreferenceService {

private final UserService userService;
private final DeviceService deviceService;

@Transactional
public LanguageResponse updateLanguage(String deviceUuid, Language language) {
Long userId = SecurityUtil.getCurrentUserIdOrNull();

if (language == null) {
throw new GeneralException(ErrorStatus._BAD_REQUEST);
}

// 로그인 사용자
if (userId != null) {
userService.updateLanguage(userId, language);

// 선택적으로 디바이스 언어도 동기화
if (deviceUuid != null && !deviceUuid.isBlank()) {
deviceService.updateLanguage(deviceUuid, language);
}

return LanguageResponse.builder()
.language(language)
.loggedIn(true)
.build();
}

// 비로그인 사용자
if (deviceUuid == null || deviceUuid.isBlank()) {
throw new GeneralException(ErrorStatus.INVALID_DEVICE);
}

deviceService.updateLanguage(deviceUuid, language);

return LanguageResponse.builder()
.language(language)
.loggedIn(false)
.build();
}

public LanguageResponse getLanguage(String deviceUuid) {
Long userId = SecurityUtil.getCurrentUserIdOrNull();

// 로그인 상태면 무조건 User.language 우선
if (userId != null) {
Language language = userService.getLanguage(userId);
return LanguageResponse.builder()
.language(language)
.loggedIn(true)
.build();
}

if (deviceUuid == null || deviceUuid.isBlank()) {
throw new GeneralException(ErrorStatus.INVALID_DEVICE);
}

Language language = deviceService.getLanguage(deviceUuid);
return LanguageResponse.builder()
.language(language)
.loggedIn(false)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.example.konnect_backend.domain.user.dto.UserInfoDto;
import com.example.konnect_backend.domain.user.entity.Child;
import com.example.konnect_backend.domain.user.entity.User;
import com.example.konnect_backend.domain.user.entity.status.Language;
import com.example.konnect_backend.domain.user.repository.ChildRepository;
import com.example.konnect_backend.domain.user.repository.UserRepository;
import com.example.konnect_backend.global.code.status.ErrorStatus;
Expand Down Expand Up @@ -127,4 +128,20 @@ public UserInfoDto getUserInfo(){
.language(user.getLanguage())
.build();
}

@Transactional
public void updateLanguage(Long userId, Language language) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));

user.updateLanguage(language);
}

@Transactional(readOnly = true)
public Language getLanguage(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));

return user.getLanguage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/swagger-resources/**",
"/webjars/**"
).permitAll()
.requestMatchers("/api/auth/**", "/api/schools/**", "/api/device/**", "/api/ai/**", "/api/usage/**", "/api/message/**").permitAll()
.requestMatchers("/api/admin/**").permitAll() // Todo 관리자만 허용해야 함, 테스트 위해 모두 허용
.requestMatchers("/api/auth/**", "/api/schools/**", "/api/device/**", "/api/ai/**", "/api/usage/**", "/api/message/**", "/api/users/language").permitAll()
.requestMatchers("/api/admin/**").denyAll() // Todo 관리자만 허용 필요
.requestMatchers("/api/ws/**", "/ws/**").permitAll()
.requestMatchers("/login/oauth2/**", "/oauth2/**").permitAll()
.requestMatchers("/public/**").permitAll()
Expand Down
Loading