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
46 changes: 26 additions & 20 deletions src/main/java/com/joycrew/backend/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package com.joycrew.backend.controller;

import com.joycrew.backend.dto.PasswordChangeRequest;
import com.joycrew.backend.dto.UserProfileResponse;
import com.joycrew.backend.entity.Employee;
import com.joycrew.backend.entity.Wallet;
import com.joycrew.backend.repository.EmployeeRepository;
import com.joycrew.backend.repository.WalletRepository;
import com.joycrew.backend.service.EmployeeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
Expand All @@ -28,47 +31,50 @@ public class UserController {

private final EmployeeRepository employeeRepository;
private final WalletRepository walletRepository;
private final EmployeeService employeeService;

@Operation(summary = "사용자 프로필 조회", description = "JWT 토큰으로 인증된 사용자의 프로필 정보를 반환합니다.")
@Operation(summary = "사용자 프로필 조회", description = "JWT 토큰으로 인증된 사용자의 프로필 정보를 반환합니다.",
security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공",
content = @Content(schema = @Schema(implementation = UserProfileResponse.class))),
@ApiResponse(responseCode = "401", description = "토큰 없음 또는 유효하지 않음",
content = @Content(mediaType = "application/json",
schema = @Schema(example = "{\"message\": \"유효하지 않은 토큰입니다.\"}")))
@ApiResponse(responseCode = "401", description = "인증 필요")
})
@GetMapping("/profile")
public ResponseEntity<?> getProfile(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("message", "유효하지 않은 토큰입니다."));
}

String userEmail = authentication.getName();

Employee employee = employeeRepository.findByEmail(userEmail)
.orElseThrow(() -> new RuntimeException("인증된 사용자를 찾을 수 없습니다."));

Optional<Wallet> walletOptional = walletRepository.findByEmployee_EmployeeId(employee.getEmployeeId());
int totalBalance = 0;
int giftableBalance = 0;
if (walletOptional.isPresent()) {
Wallet wallet = walletOptional.get();
totalBalance = wallet.getBalance();
giftableBalance = wallet.getGiftablePoint();
}
int totalBalance = walletOptional.map(Wallet::getBalance).orElse(0);
int giftableBalance = walletOptional.map(Wallet::getGiftablePoint).orElse(0);

UserProfileResponse response = UserProfileResponse.builder()
.employeeId(employee.getEmployeeId())
.name(employee.getEmployeeName())
.email(employee.getEmail())
.role(employee.getRole())
.department(employee.getDepartment() != null ? employee.getDepartment().getName() : null) // 부서 이름 추가
.department(employee.getDepartment() != null ? employee.getDepartment().getName() : null)
.position(employee.getPosition())
.totalBalance(totalBalance)
.giftableBalance(giftableBalance)
.build();

return ResponseEntity.ok(response);
}
}

@Operation(summary = "비밀번호 변경 (첫 로그인 시)", description = "초기 비밀번호를 받은 사용자가 자신의 비밀번호를 새로 설정합니다.",
security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "비밀번호 변경 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 (비밀번호 정책 위반)"),
@ApiResponse(responseCode = "401", description = "인증 필요")
})
@PostMapping("/password")
public ResponseEntity<Map<String, String>> forceChangePassword(Authentication authentication, @Valid @RequestBody PasswordChangeRequest request) {
String userEmail = authentication.getName();
employeeService.forcePasswordChange(userEmail, request);
return ResponseEntity.ok(Map.of("message", "비밀번호가 성공적으로 변경되었습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.joycrew.backend.dto;

import com.joycrew.backend.entity.enums.UserRole;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class AdminEmployeeUpdateRequest {
private String name;
private Long departmentId;
private String position;
private UserRole role;
private String status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.joycrew.backend.dto;

import com.joycrew.backend.entity.enums.UserRole;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class EmployeeRegistrationRequest {
@NotBlank(message = "이름은 필수입니다.")
private String name;

@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "유효한 이메일 형식이 아닙니다.")
private String email;

@NotBlank(message = "초기 비밀번호는 필수입니다.")
@Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다.")
private String initialPassword;

@NotNull(message = "회사 ID는 필수입니다.")
private Long companyId;

private Long departmentId;

@NotBlank(message = "직책은 필수입니다.")
private String position;

@NotNull(message = "역할은 필수입니다.")
private UserRole role;
}
15 changes: 15 additions & 0 deletions src/main/java/com/joycrew/backend/dto/PasswordChangeRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.joycrew.backend.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PasswordChangeRequest {
@NotBlank
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,20}$",
message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8자 이상 20자 이하이어야 합니다.")
private String newPassword;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.joycrew.backend.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.URL;

@Getter
@Setter
@Schema(description = "사용자 프로필 수정 요청 DTO")
public class UserProfileUpdateRequest {

@Schema(description = "새로운 사용자 이름 (선호하는 이름)", example = "김조이", nullable = true)
@Size(min = 2, max = 20, message = "이름은 2자 이상 20자 이하로 입력해주세요.")
private String name;

@Schema(description = "새로운 비밀번호 (영문, 숫자, 특수문자 포함 8~20자)", example = "newPassword123!", nullable = true)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,20}$",
message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8자 이상 20자 이하이어야 합니다.")
private String password;

@Schema(description = "프로필 사진 이미지 URL", example = "https://example.com/profile.jpg", nullable = true)
@URL(message = "유효한 URL 형식이 아닙니다.")
private String profileImageUrl;

@Schema(description = "개인 이메일 주소", example = "joy@personal.com", nullable = true)
@Email(message = "유효한 이메일 형식이 아닙니다.")
private String personalEmail;

@Schema(description = "휴대폰 번호", example = "010-1234-5678", nullable = true)
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "유효한 휴대폰 번호 형식이 아닙니다. (예: 010-1234-5678)")
private String phoneNumber;

@Schema(description = "배송 주소 (리워드 배송 시 필요)", example = "서울시 강남구 테헤란로 123", nullable = true)
@Size(max = 255, message = "주소는 255자를 초과할 수 없습니다.")
private String shippingAddress;

@Schema(description = "이메일 알림 수신 여부", example = "true", nullable = true)
private Boolean emailNotificationEnabled;

@Schema(description = "앱 내 알림 수신 여부", example = "true", nullable = true)
private Boolean appNotificationEnabled;

@Schema(description = "선호 언어 설정", example = "ko-KR", nullable = true)
private String language;

@Schema(description = "시간대 설정", example = "Asia/Seoul", nullable = true)
private String timezone;
}
30 changes: 20 additions & 10 deletions src/main/java/com/joycrew/backend/entity/Employee.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import com.joycrew.backend.entity.enums.UserRole;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Entity
@Table(name = "employee")
Expand Down Expand Up @@ -45,6 +45,17 @@ public class Employee implements UserDetails {
@Column(nullable = false)
private UserRole role;

// 사용자 셀프 서비스 필드
@Column(length = 2048) // URL은 길 수 있으므로 길이 확장
private String profileImageUrl;
private String personalEmail;
private String phoneNumber;
private String shippingAddress;
private Boolean emailNotificationEnabled;
private Boolean appNotificationEnabled;
private String language;
private String timezone;

private LocalDateTime lastLoginAt;
@Column(nullable = false)
private LocalDateTime createdAt;
Expand All @@ -66,19 +77,18 @@ public class Employee implements UserDetails {
@PrePersist
protected void onCreate() {
this.createdAt = this.updatedAt = LocalDateTime.now();
if (this.status == null) {
this.status = "ACTIVE";
}
if (this.role == null) {
this.role = UserRole.EMPLOYEE;
}
if (this.status == null) this.status = "ACTIVE";
if (this.role == null) this.role = UserRole.EMPLOYEE;
if (this.emailNotificationEnabled == null) this.emailNotificationEnabled = true;
if (this.appNotificationEnabled == null) this.appNotificationEnabled = true;
}

@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}

// UserDetails 구현 메서드들...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + this.role));
Expand Down Expand Up @@ -113,4 +123,4 @@ public boolean isCredentialsNonExpired() {
public boolean isEnabled() {
return "ACTIVE".equals(this.status);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.joycrew.backend.repository;

import com.joycrew.backend.entity.Department;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {
}
Loading