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
Binary file modified .DS_Store
Binary file not shown.
Binary file modified src/.DS_Store
Binary file not shown.
Binary file modified src/main/.DS_Store
Binary file not shown.
Binary file modified src/main/java/.DS_Store
Binary file not shown.
Binary file modified src/main/java/org/.DS_Store
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.ezcode.codetest.application.usermanagement.user.service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
Expand Down Expand Up @@ -56,14 +57,18 @@ public class UserService {
private final RedisTemplate<String, String> redisTemplate;
private final S3Uploader s3Uploader;

@Transactional(readOnly = true)
@Transactional
public UserInfoResponse getUserInfo(AuthUser authUser) {
log.info("authUserEmail: {}, authUserID : {}", authUser.getEmail(), authUser.getId());
User user = userDomainService.getUserById(authUser.getId());
int userSubmissionCount = submissionDomainService.findSubmissionCountByUserId(user.getId());
List<UserAuthType> userAuthTypes = userDomainService.getUserAuthTypesByUser(user);
List<AuthType> authTypes = userAuthTypes.stream()
.map(UserAuthType::getAuthType).toList();
if (user.getLanguage() == null) {
Language userLanguage = languageDomainService.getLanguage(1L);
user.setLanguage(userLanguage);
}

return UserInfoResponse.builder()
.username(user.getUsername())
Expand All @@ -87,6 +92,12 @@ public UserInfoResponse getUserInfo(AuthUser authUser) {
public UserInfoResponse modifyUserInfo(AuthUser authUser, ModifyUserInfoRequest request, MultipartFile image) {
User user = userDomainService.getUserById(authUser.getId());
Language findLangauge = languageDomainService.getLanguage(request.languageId());
if (request.nickname() != null && !request.nickname().equals(user.getNickname())) {
if (userDomainService.existsByNickname(request.nickname())) {
log.info("중복 닉네임");
throw new UserException(UserExceptionCode.ALREADY_EXIST_NICKNAME);
}
}
Comment on lines +95 to +100
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

닉네임 중복 선검사만으로는 부족 — DB 유니크 제약과 예외 매핑 병행 필요

현재 선검사로는 경쟁 상태를 막지 못합니다. 유니크 제약 추가 후 DataIntegrityViolationExceptionALREADY_EXIST_NICKNAME으로 매핑하는 방식을 병행하세요. 또한 공백/대소문자 정규화도 고려 바랍니다.

정규화 예시:

-        if (request.nickname() != null && !request.nickname().equals(user.getNickname())) {
-            if (userDomainService.existsByNickname(request.nickname())) {
+        if (request.nickname() != null) {
+            String newNickname = request.nickname().trim();
+            if (!newNickname.equals(user.getNickname()) && userDomainService.existsByNickname(newNickname)) {
                 log.info("중복 닉네임");
                 throw new UserException(UserExceptionCode.ALREADY_EXIST_NICKNAME);
             }
         }
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java
around lines 95-100, the current pre-check for duplicate nicknames is
insufficient for race conditions; add DB-level unique constraint on the nickname
column (entity and migration) and keep the pre-check, but also catch
DataIntegrityViolationException (or the specific persistence exception your
stack throws) where the user is saved and map it to new
UserException(UserExceptionCode.ALREADY_EXIST_NICKNAME). Additionally normalize
nicknames (trim and a consistent case, e.g., toLowerCase()) before checking and
persisting so checks and DB values are consistent. Ensure your
global/transactional save path wraps persistence exceptions to translate them to
the ALREADY_EXIST_NICKNAME business exception.


user.modifyUserInfo(
request.nickname(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package org.ezcode.codetest.domain.submission.dto;

import java.time.LocalDate;
import java.util.Set;

public record DailyCorrectCount(
LocalDate date,
int count
) {
public DailyCorrectCount(java.sql.Date date, int count) {
this(date.toLocalDate(), count);
}
}
Long count,
Set<Long> problemIds
) { }
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.ezcode.codetest.domain.submission.service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -120,4 +121,5 @@ private void modifyUserProblemResult(UserProblemResult userProblemResult, boolea
public int findSubmissionCountByUserId(Long userId) {
return submissionRepository.findSubmissionCountByUserId(userId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ public enum UserExceptionCode implements ResponseCode {
NO_GITHUB_INFO(false, HttpStatus.BAD_REQUEST, "깃허브 정보가 없습니다."),
NO_GITHUB_REPO(false, HttpStatus.BAD_REQUEST, "해당하는 Repository를 찾을 수 없습니다."),


;
ALREADY_EXIST_NICKNAME(false, HttpStatus.BAD_REQUEST, "이미 존재하는 닉네임입니다");
private final boolean success;
private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,8 @@ public void modifyProfileImage(String profileImageUrl) {
public void modifyUserRole(UserRole userRole) {
this.role = userRole;
}

public void setLanguage(Language userLanguage) {
this.language = userLanguage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ public interface UserRepository {

void updateUserGithubAccessToken(User loginUser);

List<String> getUserNicknames();
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,11 @@ public List<UserAuthType> getUserAuthTypesByUser(User user) {
return userAuthTypeRepository.getUserAuthTypesByUser(user);
}

public List<String> getUserNicknames() {
return userRepository.getUserNicknames();
}

public boolean existsByNickname(String nickname) {
return userRepository.existsByNickname(nickname);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package org.ezcode.codetest.infrastructure.persistence.repository.submission.query;


import java.time.LocalDate;
import java.util.List;
import java.util.Set;

import org.ezcode.codetest.domain.submission.dto.DailyCorrectCount;
import org.ezcode.codetest.domain.submission.model.entity.QUserProblemResult;
import org.springframework.stereotype.Repository;

import com.querydsl.core.group.GroupBy;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;

Expand All @@ -24,20 +28,25 @@ public List<DailyCorrectCount> countCorrectByUserGroupedByDate(Long userId) {

QUserProblemResult upr = QUserProblemResult.userProblemResult;

var date = Expressions.dateTemplate(java.sql.Date.class, "DATE({0})", upr.modifiedAt);
var date = Expressions.dateTemplate(LocalDate.class, "DATE({0})", upr.modifiedAt);
NumberExpression<Long> countDistinctProblem = upr.problem.id.countDistinct();

return queryFactory
.select(Projections.constructor(DailyCorrectCount.class,
date,
upr.count().intValue()
))
.from(upr)
.where(
upr.user.id.eq(userId),
upr.isCorrect.eq(true)
)
.groupBy(date)
.orderBy(date.asc())
.fetch();
.transform(
GroupBy.groupBy(date).list(
Projections.constructor(
DailyCorrectCount.class,
date,
countDistinctProblem,
GroupBy.set(upr.problem.id)
)
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ void updateReviewTokens(
@Param("ids") List<Long> ids,
@Param("newToken") int newToken
);

@Query("select u.nickname from User u ")
List<String> findAllNicknames();
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public void updateUserGithubAccessToken(User loginUser) {
userJpaRepository.save(loginUser);
}


@Override
public List<String> getUserNicknames() {
return userJpaRepository.findAllNicknames();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.ezcode.codetest.application.usermanagement.user.service.UserService;
import org.ezcode.codetest.domain.user.model.entity.AuthUser;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -46,7 +47,7 @@ public ResponseEntity<UserInfoResponse> getUserInfo(@AuthenticationPrincipal Aut
}

@Operation(summary = "내 정보 수정", description = "닉네임, 블로그, 깃허브, 소개 등 개인 정보를 추가하거나 수정합니다.")
@PutMapping("/users")
@PutMapping(value = "/users", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<UserInfoResponse> modifyUserInfo(
@AuthenticationPrincipal AuthUser authUser,
@Valid @RequestPart("request") ModifyUserInfoRequest request,
Expand Down Expand Up @@ -90,6 +91,7 @@ public ResponseEntity<UserReviewTokenResponse> getReviewToken(
return ResponseEntity.status(HttpStatus.OK).body(userService.getReviewToken(authUser));
}

@Operation(summary = "회원의 푼 문제 수 조회", description = "날짜, 날짜마다 푼 문제 번호 리스트, 푼 문제 개수")
@GetMapping("/users/daily-solved")
public ResponseEntity<UserDailySolvedHistoryResponse> getUserDailySolvedHistory(
@AuthenticationPrincipal AuthUser authUser
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.ezcode.codetest.presentation.usermanagement;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.ezcode.codetest.application.usermanagement.auth.dto.request.FindPasswordRequest;
import org.ezcode.codetest.application.usermanagement.user.dto.request.ResetPasswordRequest;
import org.ezcode.codetest.application.usermanagement.auth.dto.request.SendEmailRequest;
Expand Down
10 changes: 9 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,12 @@ spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=600000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.validation-timeout=5000
spring.datasource.hikari.validation-timeout=5000

# ========================
# App Redirects (Email Verify)
# ========================
# 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
# 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
Comment on lines +164 to +170
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

리다이렉트 속성 추가는 적절. 메일 링크용 백엔드 베이스 URL 속성도 함께 정의 권장

메일 링크 호스트를 신뢰 가능한 설정에서 주입받을 수 있도록 백엔드 퍼블릭 베이스 URL 속성을 추가하세요.

 # ========================
 # App Redirects (Email Verify)
 # ========================
+# 백엔드 퍼블릭 베이스 URL (메일 인증 링크 생성 시 사용)
+app.backend.public-base-url=${APP_BACKEND_PUBLIC_BASE_URL:https://api.your-backend.example.com}
+
 # 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
 app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
 # 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
 app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# ========================
# App Redirects (Email Verify)
# ========================
# 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
# 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
# ========================
# App Redirects (Email Verify)
# ========================
# 백엔드 퍼블릭 베이스 URL (메일 인증 링크 생성 시 사용)
app.backend.public-base-url=${APP_BACKEND_PUBLIC_BASE_URL:https://api.your-backend.example.com}
# 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
# 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
🤖 Prompt for AI Agents
In src/main/resources/application.properties around lines 164 to 170, you
currently define frontend redirect URLs for email verification but lack a
configurable backend/public base URL for constructing mail links; add a new
property (e.g. app.public.base-url with an environment-backed default like
${APP_PUBLIC_BASE_URL:https://your-backend.example.com}) adjacent to the
redirect entries so the mailer can build absolute links using that value instead
of hardcoding hostnames; ensure the property key is documented with a brief
comment and referenced by the email-generation code to compose verification
links.