Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
ea9e53f
[#107] feat(profile): Profile 엔티티 추가
shinheekim Dec 20, 2025
331648d
[#107] feat(profile): ProfileRepository 추가
shinheekim Dec 20, 2025
8b1d623
[#107] refactor(user): User 엔티티에서 프로필 관련 필드 제거
shinheekim Dec 20, 2025
45f5cef
[#107] refactor(profile): user 패키지에서 profile 패키지로 클래스 이동 및 리팩토링
shinheekim Dec 20, 2025
8e9beb5
[#107] feat(auth): OauthAuthService에서 User 생성 시 Profile도 함께 생성
shinheekim Dec 20, 2025
33704ca
[#125] feat(survey): 이전에 사용하던 방식 제거
jaejoong0529 Dec 21, 2025
f8a2141
[#125] feat(survey): 아바타 타입 및 설문 선택지 Enum 정의
jaejoong0529 Dec 21, 2025
50b0293
[#125] feat(survey): 설문 응답 및 성향 점수 엔티티 생성
jaejoong0529 Dec 21, 2025
af29c1a
[#125] feat(survey): 설문 응답 기반 성향 점수 계산 서비스 구현
jaejoong0529 Dec 21, 2025
eed431c
[#125] feat(survey): 성향 점수 기반 아바타 매칭 서비스 구현
jaejoong0529 Dec 21, 2025
6253302
[#125] feat(survey): 설문조사 DTO 클래스 및 Converter 정의
jaejoong0529 Dec 21, 2025
622dfd2
[#125] feat(survey): 설문조사 서비스 비즈니스 로직 구현
jaejoong0529 Dec 21, 2025
fe59a43
[#125] feat(survey): 설문조사 예외 처리 및 에러 코드 추가
jaejoong0529 Dec 21, 2025
8c1e090
[#125] feat(survey): 설문조사 API 컨트롤러 및 Swagger 문서화
jaejoong0529 Dec 21, 2025
dca4fa6
[#125] feat(survey): 아바타 매칭 조건 간략화
jaejoong0529 Dec 22, 2025
ee4da12
[#125] feat(survey): static factory 메서드 방식으로 일관성
jaejoong0529 Dec 22, 2025
019dc2b
[#125] feat(survey): 헬퍼메서드로 중복성 최소화
jaejoong0529 Dec 22, 2025
55bc9fd
[#125] feat(survey): 람다함수 개선
jaejoong0529 Dec 22, 2025
38d8301
[#125] test(survey): 테스트 코드 추가
jaejoong0529 Dec 22, 2025
c8d6a37
[#125] feat(survey): 설문조사 생성 200 -> 201로 변경
jaejoong0529 Dec 29, 2025
1370904
[#125] feat(survey): @Getter 대신 @Override 사용으로 가독성 향상
jaejoong0529 Dec 29, 2025
cb34f81
[#125] feat(survey): survey 메서드명 변경
jaejoong0529 Dec 29, 2025
2596851
[#125] feat(survey): 아바타 태그 내용 추가
jaejoong0529 Dec 29, 2025
a98f797
[#125] feat(survey): 선호활동 최대3개 고르는걸로 수정
jaejoong0529 Dec 29, 2025
e9e388f
[#107] refactor(profile): 다른 도메인에서 Profile 사용하도록 변경
shinheekim Dec 20, 2025
31f81fc
[#107] test(profile): Profile 도입에 따른 테스트 파일 업데이트
shinheekim Dec 20, 2025
d360165
[#107] feat(profile): 전화번호 인증 토큰 검증 메서드 추가
shinheekim Dec 28, 2025
cb15fc4
[#107] feat(profile): Profile 엔티티에 프로필 초기 설정 메서드 추가
shinheekim Dec 28, 2025
257ffe7
[#107] feat(profile): 프로필 설정 요청 DTO 생성
shinheekim Dec 28, 2025
eca6f09
[#107] feat(profile): 프로필 초기 설정 서비스 로직 구현
shinheekim Dec 28, 2025
7327f5c
[#107] feat(profile): 프로필 초기 설정 API 엔드포인트 추가
shinheekim Dec 28, 2025
a40c6f1
[#107] feat(profile): 테스트 코드 서비스명 변경 반영
shinheekim Dec 28, 2025
21a2cf1
[#107] refactor(profile): UserInfo DTO null 체크 제거
shinheekim Dec 28, 2025
e3eddca
[#107] docs(profile): 프로필 초기 설정 API docs swagger 추가
shinheekim Dec 28, 2025
d39b6fe
[#107] chore(profile): 현재는 불필요한 메서드 삭제
shinheekim Dec 29, 2025
edb026a
[#107] chore(profile): 기획상 변경으로 띄어쓰기 없는 랜덤 닉네임으로 생성
shinheekim Dec 29, 2025
1f8e5d3
[#107] chore(profile): UserId가 아닌 User객체로 Profile 조회
shinheekim Dec 29, 2025
2816dcf
[#107] chore(profile): 프로필 최초 저장 request 유효성 추가
shinheekim Dec 29, 2025
c3bf3fe
[#107] feat(profile): 닉네임 Race condition을 방지하기 위해 비관적 잠금 적용
shinheekim Dec 29, 2025
39810b8
[#107] test(profile): 프로필 최초 생성 단위 테스트 작성
shinheekim Dec 29, 2025
700cc6c
[#107] docs(profile): 프로필 관련 API 응답 구체적으로 작성
shinheekim Dec 29, 2025
dd5de71
[#127] docs(profile): 에러 응답 커스터마이징 및 프로필 에러 응답 수정(구체화)
shinheekim Dec 29, 2025
9a7eb3a
[#107] chore(profile): 에러 코드 enum 위치 일부 수정
shinheekim Dec 30, 2025
283f866
[#107] chore(profile): 불필요한 주석 제거
shinheekim Dec 30, 2025
b3523d6
[#107] chore(profile): 닉네임 검증 시 동시성 락으로 제어
shinheekim Dec 30, 2025
bcc7ceb
[#107] chore(profile): 프로필 초기 세팅 request dto에서 검증 강화
shinheekim Dec 30, 2025
97bf1cd
[#107] test(profile): 제거된 코드에 대한 테스트 코드 제거
shinheekim Dec 30, 2025
bbb491b
Merge pull request #128 from GIL-DONG-MU/feat/#107-signup-final-api
jaejoong0529 Dec 30, 2025
58a2baf
[#127] docs(swagger): 기존 CommonApiResponses.java 삭제
shinheekim Dec 30, 2025
6f89f11
Merge branch 'dev' into feat/#127-swagger-error-custiomize
shinheekim Dec 30, 2025
ad2e89e
[#127] docs(swagger): 기존 CommonApiResponses.java 삭제
shinheekim Dec 30, 2025
5715e77
Merge pull request #129 from GIL-DONG-MU/feat/#127-swagger-error-cust…
jaejoong0529 Dec 30, 2025
429537a
Merge branch 'feat/#125-survey-avatar-matching' of github.com:GIL-DON…
shinheekim Dec 30, 2025
11709ea
[#125] chore(survey): merge 해결
shinheekim Dec 30, 2025
553ef89
[#125] test(survey): user, profile 분리로 변경된 테스트 코드 수정
shinheekim Dec 30, 2025
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 @@ -11,6 +11,8 @@
import com.dduru.gildongmu.common.exception.BusinessException;
import com.dduru.gildongmu.common.exception.ErrorCode;
import com.dduru.gildongmu.common.jwt.JwtTokenProvider;
import com.dduru.gildongmu.profile.domain.Profile;
import com.dduru.gildongmu.profile.repository.ProfileRepository;
import com.dduru.gildongmu.user.domain.User;
import com.dduru.gildongmu.user.enums.OauthType;
import com.dduru.gildongmu.user.enums.Role;
Expand All @@ -30,6 +32,7 @@ public class OauthAuthService {

private final OauthFactory oauthFactory;
private final UserRepository userRepository;
private final ProfileRepository profileRepository;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;

Expand Down Expand Up @@ -145,22 +148,27 @@ private record UserCreationResult(User user, boolean isNewUser) {
}

private User createNewUser(OauthUserInfo oauthUserInfo) {
// 추가 정보는 회원가입 이후 별도 입력으로 변경
// 닉네임은 온보딩에서 설정하므로 null로 초기화

User newUser = User.builder()
.email(oauthUserInfo.email())
.name(oauthUserInfo.name())
.nickname(null)
.profileImage(oauthUserInfo.profileImage())
.oauthId(oauthUserInfo.oauthId())
.oauthType(oauthUserInfo.loginType())
.role(Role.USER)
.build();

User savedUser = userRepository.save(newUser);

Profile profile = Profile.builder()
.user(savedUser)
.profileImage(null)
.nickname(null)
.gender(null)
.phoneNumber(null)
.birthday(null)
.build();

return userRepository.save(newUser);
profileRepository.save(profile);

return savedUser;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ public static CommentResponse from(Comment comment) {
authorProfileImage = null;
} else {
content = comment.getContent();
author = comment.getUser().getNickname();
authorProfileImage = comment.getUser().getProfileImage();
author = comment.getUser().getProfile().getNickname();
authorProfileImage = comment.getUser().getProfile().getProfileImage();
}

return new CommentResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.dduru.gildongmu.common.annotation;

import com.dduru.gildongmu.common.exception.ErrorCode;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* API 엔드포인트에 자동으로 에러 응답을 추가하기 위한 어노테이션
* ErrorCode enum을 지정하면 해당 ErrorCode의 HTTP 상태 코드가 자동으로 사용됩니다.
*
* 사용 예시:
* <pre>
* {@code @ApiErrorResponses({ErrorCode.NICKNAME_ALREADY_TAKEN, ErrorCode.PROFILE_NOT_FOUND})}
* ResponseEntity<ApiResult<Response>> method();
* </pre>
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorResponses {
/**
* 추가할 ErrorCode enum 목록 (예: ErrorCode.NICKNAME_ALREADY_TAKEN, ErrorCode.PROFILE_NOT_FOUND)
* 각 ErrorCode의 HTTP 상태 코드가 자동으로 사용됩니다.
*/
ErrorCode[] value();
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.dduru.gildongmu.common.exception;

import com.dduru.gildongmu.common.annotation.ApiErrorResponses;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* OpenAPI 에러 응답 자동 추가 커스터마이저
*
* @ApiErrorResponses 어노테이션에 ErrorCode enum을 지정하면,
* 해당 ErrorCode의 HTTP 상태 코드와 메시지가 자동으로 사용됩니다.
*/
@Slf4j
@Configuration
public class ApiErrorResponseDocsCustomizer {

/**
* ErrorResponse 스키마를 Components에 등록
*/
@Bean
public OpenApiCustomizer errorResponseSchemaCustomizer() {
return openApi -> {
Components components = openApi.getComponents();
if (components == null) {
components = new Components();
openApi.setComponents(components);
}

// ErrorResponse 스키마 정의
Schema<?> errorResponseSchema = new Schema<>()
.type("object")
.addProperty("status", new Schema<>().type("integer").format("int32").example(400))
.addProperty("data", new Schema<>()
.type("object")
.addProperty("errorCode", new Schema<>().type("string").example("INVALID_INPUT_VALUE"))
.addProperty("field", new Schema<>().type("string").nullable(true))
.addProperty("message", new Schema<>().type("string").example("잘못된 입력 값입니다.")));

components.addSchemas("ErrorResponse", errorResponseSchema);
};
}

/**
* @ApiErrorResponses 어노테이션에 지정된 ErrorCode enum을 기반으로 에러 응답 자동 추가
*/
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
ErrorCode[] errorCodes = new ErrorCode[0];

Method method = handlerMethod.getMethod();
Class<?> declaringClass = method.getDeclaringClass();
String methodName = method.getName();

// 모든 인터페이스에서 메서드를 찾아 어노테이션 확인
for (Class<?> iface : declaringClass.getInterfaces()) {
// 인터페이스의 모든 메서드 검색 (파라미터 타입이 다를 수 있으므로)
for (Method interfaceMethod : iface.getMethods()) {
// 메서드 이름이 같고 어노테이션이 있으면 사용
if (interfaceMethod.getName().equals(methodName)
&& interfaceMethod.isAnnotationPresent(ApiErrorResponses.class)) {
errorCodes = interfaceMethod.getAnnotation(ApiErrorResponses.class).value();
break;
}
}
if (errorCodes.length > 0) {
break;
}

// 인터페이스 레벨 어노테이션 확인
if (iface.isAnnotationPresent(ApiErrorResponses.class)) {
errorCodes = iface.getAnnotation(ApiErrorResponses.class).value();
break;
}
}

// 클래스 레벨 어노테이션 확인
if (errorCodes.length == 0 && declaringClass.isAnnotationPresent(ApiErrorResponses.class)) {
errorCodes = declaringClass.getAnnotation(ApiErrorResponses.class).value();
}

// 메서드 레벨 어노테이션 확인 (메서드 레벨이 우선)
if (method.isAnnotationPresent(ApiErrorResponses.class)) {
errorCodes = method.getAnnotation(ApiErrorResponses.class).value();
}

if (errorCodes.length > 0) {
addErrorResponses(operation, errorCodes);
}

return operation;
};
}

/**
* ErrorCode enum 배열을 기반으로 에러 응답 추가
* 각 ErrorCode에 대해 해당하는 HTTP 상태 코드의 응답을 추가하고, 예시를 포함합니다.
* 같은 HTTP 상태 코드를 가진 여러 ErrorCode가 있으면 모두 examples에 추가합니다.
*/
private void addErrorResponses(io.swagger.v3.oas.models.Operation operation, ErrorCode[] errorCodes) {
// ErrorResponse 스키마 참조
Schema<?> errorResponseSchema = new Schema<>()
.$ref("#/components/schemas/ErrorResponse");

// HTTP 상태 코드별로 ErrorCode들을 그룹화
Map<String, java.util.List<ErrorCode>> statusCodeToErrorCodes = new HashMap<>();

for (ErrorCode errorCode : errorCodes) {
String statusCode = String.valueOf(errorCode.getStatus());
statusCodeToErrorCodes.computeIfAbsent(statusCode, k -> new java.util.ArrayList<>()).add(errorCode);
}

// 각 상태 코드에 대해 에러 응답 추가
for (Map.Entry<String, java.util.List<ErrorCode>> entry : statusCodeToErrorCodes.entrySet()) {
String statusCode = entry.getKey();
java.util.List<ErrorCode> errorCodeList = entry.getValue();

// 이미 해당 상태 코드의 응답이 있으면 스킵
if (operation.getResponses().containsKey(statusCode)) {
continue;
}

// 첫 번째 ErrorCode를 기본으로 사용 (description)
ErrorCode primaryErrorCode = errorCodeList.get(0);

// 각 ErrorCode에 대한 예시 생성
Map<String, Example> examples = new HashMap<>();
for (ErrorCode errorCode : errorCodeList) {
Map<String, Object> errorResponseMap = new LinkedHashMap<>();
errorResponseMap.put("status", errorCode.getStatus());

Map<String, Object> dataMap = new LinkedHashMap<>();
dataMap.put("errorCode", errorCode.name());
dataMap.put("field", null);
dataMap.put("message", errorCode.getMessage());
errorResponseMap.put("data", dataMap);

Example example = new Example();
example.setValue(errorResponseMap);
example.setSummary(errorCode.getMessage());
examples.put(errorCode.name(), example);
}

// MediaType 생성 (여러 예시가 있으면 examples 사용, 하나만 있으면 example 사용)
MediaType mediaType = new MediaType().schema(errorResponseSchema);
if (examples.size() > 1) {
mediaType.setExamples(examples);
} else if (examples.size() == 1) {
Example singleExample = examples.values().iterator().next();
mediaType.setExample(singleExample.getValue());
}

// 에러 응답 생성
ApiResponse apiResponse = new ApiResponse()
.description(primaryErrorCode.getMessage())
.content(new Content().addMediaType("application/json", mediaType));

operation.getResponses().addApiResponse(statusCode, apiResponse);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public enum ErrorCode {
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 다른 소셜 계정으로 가입된 이메일입니다."),

// 닉네임 (NICKNAME) - 이전 '온보딩 (ONBD)'
// 닉네임 (NICKNAME)
NICKNAME_NOT_BLANK(HttpStatus.BAD_REQUEST, "닉네임은 공백일 수 없습니다."),
NICKNAME_INVALID_LENGTH(HttpStatus.BAD_REQUEST, "닉네임은 2자 이상 14자 이하로 입력해주세요."),
NICKNAME_INVALID_CHARACTERS(HttpStatus.BAD_REQUEST, "닉네임은 한글, 영어, 숫자만 사용 가능합니다."),
Expand All @@ -25,6 +25,7 @@ public enum ErrorCode {

// 사용자 (USER)
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저의 프로필을 찾을 수 없습니다."),

// 게시글 (POST)
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다."),
Expand Down Expand Up @@ -61,7 +62,7 @@ public enum ErrorCode {
INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "허용되지 않는 파일 확장자입니다."),

// 설문 (SURVEY)
UNKNOWN_SURVEY_ANSWER_CODE(HttpStatus.BAD_REQUEST, "알 수 없는 설문조사 답변 코드입니다."),
SURVEY_RESULT_NOT_FOUND(HttpStatus.NOT_FOUND, "설문 결과를 찾을 수 없습니다."),

// 휴대폰 인증 (VERIFICATION)
SMS_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "SMS 발송에 실패했습니다."),
Expand Down
Loading