Skip to content
Open
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 @@ -14,10 +14,10 @@ public static ResponseCookie createAccessTokenCookie(String accessToken) {
return ResponseCookie.from("accessToken", accessToken)
.path("/")
.httpOnly(true)
.secure(false) // HTTPS 환경에서는 true
.secure(false)
.maxAge(Duration.ofDays(1).toSeconds())
.sameSite("Lax")
.domain("comatching.site")
// .domain("comatching.site")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

accessToken 쿠키의 domain 설정을 주석 처리한 것은 로컬 개발 환경을 지원하기 위한 좋은 변경입니다. 하지만 refreshToken 쿠키(32행)에는 여전히 .domain("comatching.site") 설정이 남아있어 설정이 일관되지 않습니다. 이로 인해 로컬 환경에서 refreshToken 쿠키가 정상적으로 설정되지 않을 수 있습니다. 두 쿠키의 domain 설정을 일관되게 관리하는 것을 권장합니다.

.build();
}

Expand All @@ -26,7 +26,7 @@ public static ResponseCookie createRefreshTokenCookie(String refreshToken) {
return ResponseCookie.from("refreshToken", refreshToken)
.path("/api/auth")
.httpOnly(true)
.secure(false) // HTTPS 환경에서는 true
.secure(false)
.maxAge(Duration.ofDays(7).toSeconds())
.sameSite("Lax")
.domain("comatching.site")
Expand Down
3 changes: 2 additions & 1 deletion gateway-service/src/main/resources/application-aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ spring:
allowedOrigins:
- "https://comatching.site"
- "http://localhost:3000"
- "http://localhost:5173"
allowedMethods: [GET, POST, PUT, DELETE, OPTIONS]
allowedHeaders: "*"
allowCredentials: true
Expand All @@ -26,7 +27,7 @@ spring:
- id: user-service-public
uri: http://user-service:9000
predicates:
- Path=/api/auth/login, /api/auth/signup, /api/auth/email/**, /oauth2/**, /login/**, /default-ui.css
- Path=/api/auth/login, /api/auth/signup, /api/auth/signup/nickname/availability, /api/auth/participants, /api/auth/email/**, /oauth2/**, /login/**, /default-ui.css

- id: user-service-protected
uri: http://user-service:9000
Expand Down
2 changes: 1 addition & 1 deletion gateway-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ spring:
- id: user-service-public
uri: http://localhost:9000
predicates:
- Path=/api/auth/login, /api/auth/signup, /api/auth/email/**, /oauth2/**, /login/**, /default-ui.css
- Path=/api/auth/login, /api/auth/signup, /api/auth/signup/nickname/availability, /api/auth/participants, /api/auth/email/**, /oauth2/**, /login/**, /default-ui.css

- id: user-service-protected
uri: http://localhost:9000
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.comatching.user.domain.auth.dto;

public record NicknameAvailabilityResponse(
boolean available
) {
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.comatching.common.domain.enums.ContactFrequency;
import com.comatching.common.domain.enums.Gender;
import com.comatching.common.domain.enums.HobbyCategory;
import com.comatching.common.domain.enums.ProfileTagCategory;
import com.comatching.common.domain.enums.SocialAccountType;
import com.comatching.common.exception.BusinessException;
import com.comatching.user.global.exception.UserErrorCode;
Expand Down Expand Up @@ -161,7 +158,7 @@ public void update(
}

public void addHobbies(List<ProfileHobby> newHobbies) {
if (newHobbies == null || newHobbies.isEmpty() || newHobbies.size() > 10 || newHobbies.size() < 1) {
if (newHobbies == null || newHobbies.isEmpty() || newHobbies.size() > 10 || newHobbies.size() < 2) {
throw new BusinessException(UserErrorCode.INVALID_HOBBY_COUNT);
}

Expand All @@ -179,17 +176,8 @@ public List<HobbyCategory> getHobbyCategories() {
}

public void addTags(List<ProfileTag> newTags) {
if (newTags != null) {
Map<ProfileTagCategory, Long> countByCategory = newTags.stream()
.collect(Collectors.groupingBy(
t -> t.getTag().getGroup().getCategory(),
Collectors.counting()
));
countByCategory.forEach((cat, count) -> {
if (count > 3) {
throw new BusinessException(UserErrorCode.TAG_LIMIT_PER_CATEGORY_EXCEEDED);
}
});
if (newTags != null && newTags.size() > 5) {
throw new BusinessException(UserErrorCode.TAG_LIMIT_PER_CATEGORY_EXCEEDED);
}
Comment on lines +179 to 181
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

프로필 태그의 최대 개수 정책이 '카테고리별' 제한에서 '전체' 제한으로 변경되었습니다. 하지만 여기서 사용된 UserErrorCode.TAG_LIMIT_PER_CATEGORY_EXCEEDED는 이름이 현재 로직과 맞지 않아 코드를 읽을 때 오해를 줄 수 있습니다. 코드의 명확성과 유지보수성을 위해, UserErrorCode에서 해당 enum 상수의 이름을 TOTAL_TAG_LIMIT_EXCEEDED 와 같이 현재 정책을 더 잘 나타내는 이름으로 변경하는 것을 제안합니다.

References
  1. 의미 없는 줄임말이나 모호한 네이밍을 지적합니다. (link)
  2. 작은 개선이라도 팀 전체 코드 품질을 높일 수 있다면 적극적으로 제안합니다. (link)

this.tags.clear();
if (newTags != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
public interface ProfileRepository extends JpaRepository<Profile, Long> {

Optional<Profile> findByMemberId(Long memberId);
boolean existsByNickname(String nickname);
boolean existsByNicknameAndMemberIdNot(String nickname, Long memberId);

@Query("SELECT DISTINCT p FROM Profile p " +
"JOIN FETCH p.member m " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
public interface ProfileManageService {

ProfileResponse getProfile(Long memberId);
boolean isNicknameAvailable(String nickname);

List<ProfileResponse> getProfilesByIds(List<Long> memberIds);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.comatching.user.domain.member.service;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -15,8 +17,8 @@
import com.comatching.common.dto.member.ProfileResponse;
import com.comatching.common.dto.member.ProfileTagDto;
import com.comatching.common.exception.BusinessException;
import com.comatching.common.service.S3Service;
import com.comatching.user.domain.event.UserEventPublisher;
import com.comatching.user.domain.member.component.RandomNicknameGenerator;
import com.comatching.user.domain.member.dto.ProfileUpdateRequest;
import com.comatching.user.domain.member.entity.Member;
import com.comatching.user.domain.member.entity.Profile;
Expand All @@ -34,11 +36,18 @@
@Transactional
public class ProfileServiceImpl implements ProfileCreateService, ProfileManageService {

private static final String DEFAULT_IMAGE_VALUE = "default";
private static final String DEFAULT_IMAGE_PREFIX = "default_";
private static final String DEFAULT_IMAGE_EXTENSION = ".png";
private static final Set<String> DEFAULT_IMAGE_ANIMALS = Set.of(
"dog", "cat", "dinosaur", "otter", "bear", "fox", "penguin", "wolf", "rabbit", "snake", "horse", "frog"
);

private final MemberRepository memberRepository;
private final ProfileRepository profileRepository;
private final UserEventPublisher eventPublisher;
private final ProfileImageProperties profileImageProperties;
private final RandomNicknameGenerator nicknameGenerator;
private final S3Service s3Service;

@Override
public ProfileResponse createProfile(Long memberId, ProfileCreateRequest request) {
Expand Down Expand Up @@ -70,6 +79,13 @@ public ProfileResponse getProfile(Long memberId) {
return toProfileResponse(profile);
}

@Override
@Transactional(readOnly = true)
public boolean isNicknameAvailable(String nickname) {
String normalizedNickname = normalizeNickname(nickname);
return !profileRepository.existsByNickname(normalizedNickname);
}

@Override
@Transactional(readOnly = true)
public List<ProfileResponse> getProfilesByIds(List<Long> memberIds) {
Expand All @@ -87,11 +103,14 @@ public ProfileResponse updateProfile(Long memberId, ProfileUpdateRequest request
Profile profile = profileRepository.findByMemberId(memberId)
.orElseThrow(() -> new BusinessException(UserErrorCode.PROFILE_NOT_EXISTS));

String normalizedNickname = normalizeNicknameForUpdate(request.nickname(), profile.getNickname(), memberId);
String profileImageUrl = resolveProfileImageUrlForUpdate(request.profileImageUrl());

profile.update(
request.nickname(),
normalizedNickname,
request.intro(),
request.mbti(),
request.profileImageUrl(),
profileImageUrl,
request.gender(),
request.birthDate(),
request.socialType(),
Expand Down Expand Up @@ -139,13 +158,10 @@ private void publishMatchingEvent(Profile profile) {

private Profile saveProfile(ProfileCreateRequest request, Member member) {

String finalNickname = normalizeNickname(request.nickname());
validateNicknameDuplicateOnCreate(finalNickname);
String finalProfileImageUrl = resolveProfileImageUrl(request.profileImageKey());

String finalNickname = request.nickname();
if (!StringUtils.hasText(finalNickname)) {
finalNickname = nicknameGenerator.generate();
}

Profile profile = Profile.builder()
.member(member)
.nickname(finalNickname)
Expand All @@ -167,20 +183,72 @@ private Profile saveProfile(ProfileCreateRequest request, Member member) {
return profileRepository.save(profile);
}

private String resolveProfileImageUrl(String inputImageKey) {
if (StringUtils.hasText(inputImageKey)) {
return profileImageProperties.baseUrl() + inputImageKey;
private String normalizeNickname(String nickname) {
if (!StringUtils.hasText(nickname)) {
throw new BusinessException(UserErrorCode.INVALID_NICKNAME);
}

List<String> defaults = profileImageProperties.filenames();
if (defaults == null || defaults.isEmpty()) {
return nickname.trim();
}

private String normalizeNicknameForUpdate(String nickname, String currentNickname, Long memberId) {
if (nickname == null) {
return null;
}

int randomIndex = ThreadLocalRandom.current().nextInt(defaults.size());
String selectedFilename = defaults.get(randomIndex);
String normalizedNickname = normalizeNickname(nickname);
if (!Objects.equals(normalizedNickname, currentNickname)
&& profileRepository.existsByNicknameAndMemberIdNot(normalizedNickname, memberId)) {
throw new BusinessException(UserErrorCode.DUPLICATE_NICKNAME);
}

return normalizedNickname;
}

private void validateNicknameDuplicateOnCreate(String nickname) {
if (profileRepository.existsByNickname(nickname)) {
throw new BusinessException(UserErrorCode.DUPLICATE_NICKNAME);
}
}

return profileImageProperties.baseUrl() + selectedFilename;
private String resolveProfileImageUrlForUpdate(String profileImageValue) {
if (profileImageValue == null) {
return null;
}
return resolveProfileImageUrl(profileImageValue);
}

private String resolveProfileImageUrl(String profileImageValue) {
if (!StringUtils.hasText(profileImageValue)) {
return buildDefaultProfileImageUrl(DEFAULT_IMAGE_VALUE + DEFAULT_IMAGE_EXTENSION);
}

String normalizedValue = profileImageValue.trim();
String loweredValue = normalizedValue.toLowerCase(Locale.ROOT);

if (DEFAULT_IMAGE_VALUE.equals(loweredValue)) {
return buildDefaultProfileImageUrl(DEFAULT_IMAGE_VALUE + DEFAULT_IMAGE_EXTENSION);
}

if (loweredValue.startsWith(DEFAULT_IMAGE_PREFIX)) {
String animalName = loweredValue.substring(DEFAULT_IMAGE_PREFIX.length());
if (DEFAULT_IMAGE_ANIMALS.contains(animalName)) {
return buildDefaultProfileImageUrl(animalName + DEFAULT_IMAGE_EXTENSION);
}
}
Comment on lines +233 to +238
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

resolveProfileImageUrl 메서드에서 default_ 접두사를 가진 이미지 값을 처리할 때, DEFAULT_IMAGE_ANIMALS Set에 포함되지 않은 동물 이름(예: default_lion)이 들어오는 경우에 대한 처리가 없습니다. 현재 로직에서는 이 경우 S3 key로 간주하여 s3Service.getFileUrl()을 호출하게 되는데, 이는 의도치 않은 동작이나 오류를 발생시킬 수 있습니다. DEFAULT_IMAGE_ANIMALS에 없는 값이 들어왔을 때 기본 이미지(default.png)로 대체하거나, 명시적으로 예외를 발생시키는 등 예외 케이스에 대한 처리를 추가하여 코드의 안정성을 높이는 것이 좋겠습니다.

if (loweredValue.startsWith(DEFAULT_IMAGE_PREFIX)) {
	String animalName = loweredValue.substring(DEFAULT_IMAGE_PREFIX.length());
	if (DEFAULT_IMAGE_ANIMALS.contains(animalName)) {
		return buildDefaultProfileImageUrl(animalName + DEFAULT_IMAGE_EXTENSION);
	} else {
		// 정의되지 않은 동물 이름이 입력된 경우, 기본 이미지로 fallback 합니다.
		return buildDefaultProfileImageUrl(DEFAULT_IMAGE_VALUE + DEFAULT_IMAGE_EXTENSION);
	}
}
References
  1. 미래의 변경에 얼마나 잘 버틸 수 있는 코드인지를 기준으로 판단합니다. (link)


if (normalizedValue.startsWith("http://") || normalizedValue.startsWith("https://")) {
return normalizedValue;
}

return s3Service.getFileUrl(normalizedValue);
}

private String buildDefaultProfileImageUrl(String filename) {
if (!StringUtils.hasText(profileImageProperties.baseUrl())) {
return null;
}
return profileImageProperties.baseUrl() + filename;
}

private static List<ProfileHobby> getProfileHobbies(List<HobbyDto> hobbies) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public AuthenticationManager authenticationManager() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public LoginSuccessHandler loginSuccessHandler() {
return new LoginSuccessHandler(jwtUtil, objectMapper, refreshTokenRepository);
}

@Bean
public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter() throws
Exception {
Expand All @@ -67,7 +72,7 @@ public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePassword
filter.setAuthenticationManager(authenticationManager());

// 핸들러 설정
filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(jwtUtil, objectMapper, refreshTokenRepository));
filter.setAuthenticationSuccessHandler(loginSuccessHandler());
filter.setAuthenticationFailureHandler(new LoginFailureHandler(objectMapper));

return filter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ public enum UserErrorCode implements ErrorCode {
PROFILE_ALREADY_EXISTS("MEM-003", HttpStatus.BAD_REQUEST, "프로필이 이미 존재합니다."),
PROFILE_NOT_EXISTS("MEM-004", HttpStatus.BAD_REQUEST, "프로필이 존재하지 않습니다."),
INVALID_SOCIAL_INFO("MEM-005", HttpStatus.BAD_REQUEST, "소셜 정보는 타입과 ID가 함께 입력되어야 합니다."),
INVALID_HOBBY_COUNT("MEM-006", HttpStatus.BAD_REQUEST, "취미는 최소 2개 이상 최대 5개 이하를 등록해야 합니다."),
TAG_LIMIT_PER_CATEGORY_EXCEEDED("MEM-007", HttpStatus.BAD_REQUEST, "카테고리별 태그는 최대 3개까지 선택 가능합니다."),
INVALID_HOBBY_COUNT("MEM-006", HttpStatus.BAD_REQUEST, "취미는 최소 2개 이상 최대 10개 이하를 등록해야 합니다."),
TAG_LIMIT_PER_CATEGORY_EXCEEDED("MEM-007", HttpStatus.BAD_REQUEST, "장점 태그는 전체 최대 5개까지 선택 가능합니다."),
DUPLICATE_NICKNAME("MEM-008", HttpStatus.BAD_REQUEST, "이미 사용 중인 닉네임입니다."),
INVALID_NICKNAME("MEM-009", HttpStatus.BAD_REQUEST, "닉네임은 공백일 수 없습니다."),
;

private final String code;
Expand Down
Loading