Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
de82bcf
:sparkles: feat: User μ—”ν‹°ν‹° 섀계, UserRepository μΆ”κ°€
ojy0903 Jan 15, 2026
bab75a4
:sparkles: feat: BaseEntity μΆ”κ°€ 및 적용
ojy0903 Jan 15, 2026
9c18031
:sparkles: feat: SecurityConfig μΆ”κ°€
ojy0903 Jan 15, 2026
f1be341
:sparkles: feat: μ—λŸ¬ 핸듀링을 μœ„ν•œ global exception, code μΆ”κ°€
ojy0903 Jan 15, 2026
90ba2d1
:sparkles: feat: 곡톡 API 응닡 ν˜•μ‹μ„ μœ„ν•œ ApiResponse, 응닡 성곡 μ½”λ“œ SuccessCode μΆ”κ°€
ojy0903 Jan 15, 2026
a0a6cbe
:recycle: refactor: ApiResponse 주석 μˆ˜μ •
ojy0903 Jan 15, 2026
6239790
:sparkles: feat: νšŒμ›κ°€μž…μ„ μœ„ν•œ Request, Response DTO μΆ”κ°€ & DTO Converter μΆ”κ°€
ojy0903 Jan 16, 2026
5b2d399
:sparkles: feat: νšŒμ›κ°€μž… μ‹œ 이메일 쀑볡 λ°©μ§€λ₯Ό μœ„ν•œ UserRepository 이메일 쑰회 λ©”μ„œλ“œ μΆ”κ°€
ojy0903 Jan 16, 2026
39ccf6c
:sparkles: feat: νšŒμ›κ°€μž… μ‹œ 이메일 쀑볡 μ˜ˆμ™Έ 처리 μ½”λ“œ UserErrorCode μΆ”κ°€
ojy0903 Jan 16, 2026
b343f75
:sparkles: feat: UserService μΆ”κ°€
ojy0903 Jan 16, 2026
78090c7
Merge remote-tracking branch 'origin/develop' into feat/#3
ojy0903 Jan 18, 2026
627321b
:bug: fix: κΈ°μ‘΄ μž„μ˜ 응닡 ν˜•μ‹ 및 μž„μ˜ μ—λŸ¬ 핸듀링 μ‚­μ œ
ojy0903 Jan 18, 2026
93e4e17
:bug: fix: κΈ°μ‘΄ νšŒμ›κ°€μž… ErrorCode μˆ˜μ •, UserSignUpException 으둜 μ˜ˆμ™Έ μˆ˜μ • 적용
ojy0903 Jan 18, 2026
7258d58
:bug: fix: UserService 이메일 쀑볡 μ˜ˆμ™Έ μƒˆλ‘œμš΄ 곡톡 응닡 ν˜•μ‹μ— 맞좰 μˆ˜μ •
ojy0903 Jan 18, 2026
0df7de6
:sparkles: feat: UserController 및 Swagger Docs μΈν„°νŽ˜μ΄μŠ€ μΆ”κ°€
ojy0903 Jan 18, 2026
3954af9
:sparkles: feat: GlobalExceptionHandler μ—μ„œ @Valid 검사 였λ₯˜ 처리 λ©”μ„œλ“œ μΆ”κ°€
ojy0903 Jan 18, 2026
3d4343d
:bug: fix: build.gradle security μ˜μ‘΄μ„± μΆ”κ°€
ojy0903 Jan 18, 2026
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Security
// implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-security'

// Database & Cache
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class WhereYouAdApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.whereyouad.WhereYouAd.domains.user.application.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record SignUpRequest(
@NotBlank(message = "이메일은 ν•„μˆ˜μž…λ‹ˆλ‹€.")
@Email(message = "이메일 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
String email,
@NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.")
String password,
@NotBlank(message = "이름은 ν•„μˆ˜μž…λ‹ˆλ‹€.")
String name,
@NotBlank(message = "μ „ν™”λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€.")
@JsonProperty("phone_number")
String phoneNumber
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.whereyouad.WhereYouAd.domains.user.application.dto.response;

import java.time.LocalDateTime;

public record SignUpResponse(
Long userId,
LocalDateTime createdAt
) { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.whereyouad.WhereYouAd.domains.user.application.mapper;

import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;

public class UserConverter {

public static SignUpResponse toSignInResponse(User user) {
return new SignUpResponse(user.getId(), user.getCreatedAt());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.domains.user.domain.constant;

public enum UserStatus {
ACTIVE, SUSPENDED, DELETED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.whereyouad.WhereYouAd.domains.user.domain.service;

import com.whereyouad.WhereYouAd.domains.user.exception.UserSignUpException;
import com.whereyouad.WhereYouAd.domains.user.exception.code.UserErrorCode;
import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus;
import com.whereyouad.WhereYouAd.domains.user.application.mapper.UserConverter;
import com.whereyouad.WhereYouAd.domains.user.application.dto.request.SignUpRequest;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import com.whereyouad.WhereYouAd.domains.user.persistence.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

//νšŒμ›κ°€μž… λ©”μ„œλ“œ
public SignUpResponse signUpUser(SignUpRequest request) {
if (userRepository.existsByEmail(request.email())) { //이미 μ΄λ©”μΌλ‘œ λ§Œλ“  계정이 μ‘΄μž¬ν•  μ‹œ
throw new UserSignUpException(UserErrorCode.USER_EMAIL_DUPLICATE); //이메일 쀑볡 μ˜ˆμ™Έ
}

//λΉ„λ°€λ²ˆν˜Έ μ•”ν˜Έν™” -> SecurityConfig 클래슀 λ‚΄ μ—μ„œ BCryptPasswordEncoder λ₯Ό Bean λ“±λ‘ν•œκ±°λ‘œ μ‚¬μš©
String encodedPwd = passwordEncoder.encode(request.password());

//User μ—”ν‹°ν‹° 생성
User user = User.builder()
.email(request.email())
.password(encodedPwd)
.name(request.name())
.phoneNumber(request.phoneNumber())
.profileImageUrl(null)
.status(UserStatus.ACTIVE)
.isEmailVerified(false)
.build();

User savedUser = userRepository.save(user);

//Response DTO 둜 λ³€ν™˜ 및 λ°˜ν™˜
return UserConverter.toSignInResponse(savedUser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.whereyouad.WhereYouAd.domains.user.exception;

import com.whereyouad.WhereYouAd.global.exception.AppException;
import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;

public class UserSignUpException extends AppException {
public UserSignUpException(BaseErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.whereyouad.WhereYouAd.domains.user.exception.code;

import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum UserErrorCode implements BaseErrorCode {
USER_EMAIL_DUPLICATE(HttpStatus.BAD_REQUEST, "USER_400_2", "이미 μ‚¬μš©μ€‘μΈ 이메일 μž…λ‹ˆλ‹€."),
;

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.whereyouad.WhereYouAd.domains.user.persistence.entity;

import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;

@Entity
@Table(name = "users")
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class User extends BaseEntity {

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

@Column(nullable = false, name = "email", length = 320)
private String email;

@Column(name = "password", length = 255)
private String password;

@Column(nullable = false, name = "name", length = 50)
private String name;

@Column(name = "profile_image_url", length = 1024)
private String profileImageUrl;

@Column(nullable = false, name = "phone_number", length = 32)
private String phoneNumber;

@Column(nullable = false, name = "is_email_verified")
@ColumnDefault("false") //κΈ°λ³Έκ°’ false
private boolean isEmailVerified; //νšŒμ› κ°€μž…μ‹œ 이메일 인증 μ—¬λΆ€

@Enumerated(EnumType.STRING)
@Column(nullable = false, name = "status")
@ColumnDefault("'ACTIVE'") //κΈ°λ³Έκ°’ ACTIVE
private UserStatus status; //ACTIVE, SUSPENDED, DELETED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.whereyouad.WhereYouAd.domains.user.persistence.repository;

import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.email = :email")
Optional<User> findUserByEmail(@Param("email") String email);

//이메일 쀑볡 μ˜ˆμ™Έ 처리λ₯Ό μœ„ν•œ μ΄λ©”μΌλ‘œ 쑰회 λ©”μ„œλ“œ
boolean existsByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.whereyouad.WhereYouAd.domains.user.presentation;

import com.whereyouad.WhereYouAd.domains.user.domain.service.UserService;
import com.whereyouad.WhereYouAd.domains.user.application.dto.request.SignUpRequest;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse;
import com.whereyouad.WhereYouAd.domains.user.presentation.docs.UserControllerDocs;
import com.whereyouad.WhereYouAd.global.response.DataResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController implements UserControllerDocs {

private final UserService userService;

@PostMapping("/signup")
public ResponseEntity<DataResponse<SignUpResponse>> signUp(@RequestBody @Valid SignUpRequest request)
{
SignUpResponse signUpResponse = userService.signUpUser(request);
return ResponseEntity.ok(
DataResponse.created(signUpResponse)
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.whereyouad.WhereYouAd.domains.user.presentation.docs;

import com.whereyouad.WhereYouAd.domains.user.application.dto.request.SignUpRequest;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse;
import com.whereyouad.WhereYouAd.global.response.DataResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;

public interface UserControllerDocs {
@Operation(
summary = "λ‹¨μˆœ νšŒμ›κ°€μž… API",
description = "이메일, λΉ„λ°€λ²ˆν˜Έ, 이름, μ „ν™”λ²ˆν˜Έλ₯Ό λ°›μ•„ νšŒμ›κ°€μž…μ„ μ§„ν–‰ν•©λ‹ˆλ‹€(이메일 인증 λ―Έμ§„ν–‰μƒνƒœ)"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성곡"),
@ApiResponse(responseCode = "400_2", description = "이메일 쀑볡 νšŒμ› 쑴재")
})
public ResponseEntity<DataResponse<SignUpResponse>> signUp(@RequestBody @Valid SignUpRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.whereyouad.WhereYouAd.global.common;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity { //BaseEntity ν™œμš© 생성 μ‹œκ°„, λ³€κ²½ μ‹œκ°„ 반영

@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import com.whereyouad.WhereYouAd.global.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

import java.util.stream.Collectors;


@Slf4j
@RestControllerAdvice
Expand Down Expand Up @@ -38,4 +41,25 @@ public ResponseEntity<ErrorResponse> handleAppCustomException(AppException e, Ht
.status(e.getErrorCode().getHttpStatus())
.body(errorResponse);
}

//@Valid 검사 μ‹€νŒ¨(ν•„μˆ˜ νŒŒλΌλ―Έν„°κ°€ null λ˜λŠ” 곡백) 인 경우 μ˜ˆμ™Έλ₯Ό μž‘λŠ” ν•Έλ“€λŸ¬
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
// μ˜ˆμ‹œ κ²°κ³Ό: "email: 이메일 ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€, password: λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€"
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining(", "));

log.error("MethodArgumentNotValidException λ°œμƒ: {}", errorMessage);
log.error("μ—λŸ¬κ°€ λ°œμƒν•œ 지점 {}, {}", request.getMethod(), request.getRequestURI());

ErrorResponse errorResponse = ErrorResponse.of(
ErrorCode.INVALID_PARAMETER,
request
);

return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.whereyouad.WhereYouAd.global.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) //CSRF 보호 λΉ„ν™œμ„±ν™”
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() //μš°μ„  λͺ¨λ“  μ ‘κ·Ό ν—ˆμš© -> μΆ”ν›„ 둜그인 κ΅¬ν˜„ 이후 μ ‘κ·Ό μ œν•œ μ˜ˆμ •
);

return http.build();
}

@Bean //νšŒμ› λΉ„λ°€λ²ˆν˜Έ BCrypt μ•”ν˜Έν™”λ₯Ό μœ„ν•œ Bean 등둝
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}