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
@@ -0,0 +1,62 @@
package com.example.solidconnection.admin.controller;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse;
import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition;
import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse;
import com.example.solidconnection.admin.dto.UserInfoDetailResponse;
import com.example.solidconnection.admin.dto.UserSearchCondition;
import com.example.solidconnection.admin.dto.UserSearchResponse;
import com.example.solidconnection.admin.service.AdminUserService;
import com.example.solidconnection.common.response.PageResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RequestMapping("/admin/users")
@RestController
public class AdminUserController {

private final AdminUserService adminUserService;

@GetMapping
public ResponseEntity<PageResponse<UserSearchResponse>> searchAllUsers(
@Valid @ModelAttribute UserSearchCondition searchCondition,
Pageable pageable
) {
Page<UserSearchResponse> page = adminUserService.searchAllUsers(searchCondition, pageable);
return ResponseEntity.ok(PageResponse.of(page));
}

@GetMapping("/{user-id}")
public ResponseEntity<UserInfoDetailResponse> getUserInfoDetail(
@PathVariable(name = "user-id") long userId
) {
UserInfoDetailResponse response = adminUserService.getUserInfoDetail(userId);
return ResponseEntity.ok(response);
}

@GetMapping("/restricted")
public ResponseEntity<PageResponse<RestrictedUserSearchResponse>> searchRestrictedUsers(
@Valid @ModelAttribute RestrictedUserSearchCondition searchCondition,
Pageable pageable
) {
Page<RestrictedUserSearchResponse> page = adminUserService.searchRestrictedUsers(searchCondition, pageable);
return ResponseEntity.ok(PageResponse.of(page));
}

@GetMapping("/restricted/{user-id}")
public ResponseEntity<RestrictedUserInfoDetailResponse> getRestrictedUserInfoDetail(
@PathVariable(name = "user-id") long userId
) {
RestrictedUserInfoDetailResponse response = adminUserService.getRestrictedUserInfoDetail(userId);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.solidconnection.admin.dto;

import java.time.ZonedDateTime;

public record BannedHistoryResponse(
ZonedDateTime createdAt
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.siteuser.domain.UserBanDuration;

public record BannedInfoResponse(
boolean isBanned,
UserBanDuration duration
) {
Comment on lines +5 to +8
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find project configuration files to determine Jackson version
fd -t f '(pom\.xml|build\.gradle|build\.gradle\.kts)' . | head -5

Repository: solid-connection/solid-connect-server

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Check Jackson version from Maven config
if [ -f "pom.xml" ]; then
  grep -A 3 "jackson" pom.xml | head -20
fi

Repository: solid-connection/solid-connect-server

Length of output: 63


🏁 Script executed:

#!/bin/bash
# Locate and examine BannedInfoResponse.java
find . -name "BannedInfoResponse.java" -type f

Repository: solid-connection/solid-connect-server

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Search for usages of BannedInfoResponse to understand the impact
rg "BannedInfoResponse" -t java -B 2 -A 2

Repository: solid-connection/solid-connect-server

Length of output: 3009


🏁 Script executed:

#!/bin/bash
# Check for similar record definitions and Jackson annotations in the codebase
rg "public record" -t java -A 3 | head -50

Repository: solid-connection/solid-connect-server

Length of output: 4166


🏁 Script executed:

#!/bin/bash
# Look for Jackson configuration or custom property naming patterns
rg "@JsonProperty|@JsonNaming|@JsonSerialize" -t java | head -20

Repository: solid-connection/solid-connect-server

Length of output: 2253


🏁 Script executed:

#!/bin/bash
# Check build.gradle for Jackson version
cat build.gradle | grep -A 2 -B 2 "jackson" | head -30

Repository: solid-connection/solid-connect-server

Length of output: 63


🏁 Script executed:

#!/bin/bash
# Read the actual BannedInfoResponse.java file
cat -n ./src/main/java/com/example/solidconnection/admin/dto/BannedInfoResponse.java

Repository: solid-connection/solid-connect-server

Length of output: 353


🏁 Script executed:

#!/bin/bash
# Check RestrictedUserSearchResponse to see the full context
cat -n ./src/main/java/com/example/solidconnection/admin/dto/RestrictedUserSearchResponse.java

Repository: solid-connection/solid-connect-server

Length of output: 535


🏁 Script executed:

#!/bin/bash
# Look for any test files related to BannedInfoResponse
find . -name "*Test*.java" -type f | xargs grep -l "BannedInfoResponse" 2>/dev/null

Repository: solid-connection/solid-connect-server

Length of output: 65


🏁 Script executed:

#!/bin/bash
# Check the SiteUserFilterRepositoryImpl to see how BannedInfoResponse is constructed
cat -n ./src/main/java/com/example/solidconnection/siteuser/repository/custom/SiteUserFilterRepositoryImpl.java | grep -A 10 "BANNED_INFO_RESPONSE_PROJECTION"

Repository: solid-connection/solid-connect-server

Length of output: 1253


🏁 Script executed:

#!/bin/bash
# More comprehensive search for Jackson dependencies
cat build.gradle

Repository: solid-connection/solid-connect-server

Length of output: 3198


🏁 Script executed:

#!/bin/bash
# Search for Spring Boot version which bundles Jackson
rg "org.springframework.boot|jackson-databind|jackson-core" build.gradle -A 1

Repository: solid-connection/solid-connect-server

Length of output: 823


🏁 Script executed:

#!/bin/bash
# Check if there are any serialization tests
rg "RestrictedUserSearchResponse|BannedInfoResponse" -t java --type-add "test:src/test/**" -B 2 -A 5 | head -50

Repository: solid-connection/solid-connect-server

Length of output: 6151


🌐 Web query:

Jackson 2.15 Java record boolean is prefix serialization property naming

💡 Result:

Jackson (including 2.15) derives JSON property names using JavaBean rules. For a boolean accessor named isXxx(), the logical property name becomes xxx (the is prefix is stripped), so a record component like boolean isActive typically serializes as "active": true, not "isActive": true. [1]

How to keep the is in the JSON name (recommended): annotate the record component

public record User(`@com.fasterxml.jackson.annotation.JsonProperty`("isActive")
                   boolean isActive) {}

@JsonProperty explicitly sets the external (JSON) name. [2][3]

Alternative approaches

  • Rename the component to active and (if you really need the JSON key to be isActive) expose it with @JsonProperty("isActive") on the component. [2][3]
  • If you need this globally, use a custom naming strategy (more invasive) that preserves is* names for booleans. [1]

Sources
[1] javathinking.com — explanation of Jackson stripping is for primitive boolean properties and options to prevent it
[2] Jackson Annotations wiki (FasterXML) — @JsonProperty purpose (define external/property name)
[3] @JsonProperty Javadoc — defines the logical/external property name used in JSON


🌐 Web query:

Spring Boot 3.1.5 Jackson version record support is prefix handling

💡 Result:

  • Spring Boot 3.1.5 ships with Jackson 2.15.3 (via dependency management / Jackson BOM). [1]

  • Records are supported by Jackson (databind) and Jackson’s record introspection also recognizes regular getters and is* getters. That means a record component like boolean isActive generates an accessor isActive() which Jackson can treat as an “is-getter” and apply standard bean name mangling (strip the is), producing JSON like {"active": true} instead of {"isActive": true}. [2][3][4]

How to keep the is prefix (common options):

  1. Rename the component to avoid the is prefix (e.g., active).
  2. Force the JSON name explicitly:
public record MyDto(`@com.fasterxml.jackson.annotation.JsonProperty`("isActive") boolean isActive) {}
  1. As a global behavior change, disable “is-getter” auto-detection (can have side effects for normal POJOs too): Jackson uses AUTO_DETECT_IS_GETTERS for this. [3]

Sources:
[1] Spring Boot 3.1.5 dependency versions (Jackson 2.15.3) (docs.spring.io)
[2] Jackson DefaultAccessorNamingStrategy.RecordNaming (records also recognize is-getters) (javadoc.io)
[3] Jackson MapperFeature.AUTO_DETECT_IS_GETTERS (fasterxml.github.io)
[4] Jackson default naming removes get/set/is prefixes (javadoc.io)

Citations:


🏁 Script executed:

#!/bin/bash
# Check if there are any API controller tests that validate JSON response
find . -name "*ControllerTest*.java" -o -name "*RestTest*.java" | xargs grep -l "BannedInfoResponse\|RestrictedUserSearchResponse" 2>/dev/null | head -5

Repository: solid-connection/solid-connect-server

Length of output: 63


🏁 Script executed:

#!/bin/bash
# Search for any controller endpoint returning RestrictedUserSearchResponse
rg "RestrictedUserSearchResponse" -t java -B 5 -A 5 | grep -E "@PostMapping|@GetMapping|@RequestMapping|public.*RestrictedUserSearchResponse" -A 2 -B 2 | head -40

Repository: solid-connection/solid-connect-server

Length of output: 3218


🏁 Script executed:

#!/bin/bash
# Check for any explicit Jackson configuration
rg "ObjectMapper|@Configuration|Jackson|PropertyNamingStrategy" -t java | head -20

Repository: solid-connection/solid-connect-server

Length of output: 2601


isBanned 컴포넌트가 Jackson 직렬화 시 "banned"으로 출력됩니다.

Spring Boot 3.1.5의 Jackson 2.15.3는 JavaBean 명명 규칙을 따르며, 불린 타입 접근자(getter)에서 is 접두사를 제거합니다. 따라서 isBanned() 메서드는 JSON에서 "banned"로 직렬화됩니다. 프론트엔드가 "isBanned"를 기대한다면 API 계약이 깨집니다.

다음 중 하나를 선택하여 수정하세요:

  1. 컴포넌트명 변경 (명시적 이름 변경)

    -boolean isBanned,
    +boolean banned,
  2. @JsonProperty 사용 (권장 - 기존 코드베이스 패턴과 일치)

    +import com.fasterxml.jackson.annotation.JsonProperty;
    
    public record BannedInfoResponse(
    -    boolean isBanned,
    +    `@JsonProperty`("isBanned") boolean isBanned,
         UserBanDuration duration
    ) {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/example/solidconnection/admin/dto/BannedInfoResponse.java`
around lines 5 - 8, The boolean record component is serialized as "banned"
because Jackson strips the "is" prefix; update the BannedInfoResponse record to
explicitly name the JSON property by annotating the boolean component with
`@JsonProperty`("isBanned") (i.e., add import
com.fasterxml.jackson.annotation.JsonProperty and annotate the component
declaration `@JsonProperty("isBanned") boolean isBanned`) so the generated JSON
key matches the expected "isBanned".


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.dto;

import java.time.ZonedDateTime;

public record MatchedInfoResponse(
String nickname,
ZonedDateTime matchedDate
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.dto;

import java.util.List;

public record MenteeInfoResponse(
UnivApplyInfoResponse univApplyInfos,
List<MatchedInfoResponse> mentorInfos
) {
Comment on lines +5 to +8
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine UnivApplyInfoResponse.java
fd "UnivApplyInfoResponse.java" --type f --exec cat -n {} \;

Repository: solid-connection/solid-connect-server

Length of output: 2255


필드명과 타입의 단수/복수 네이밍을 통일해 주세요.

univApplyInfos 필드는 복수형 이름을 사용하고 있지만, UnivApplyInfoResponse 타입은 단일 객체입니다. UnivApplyInfoResponse는 첫 번째, 두 번째, 세 번째 대학 지원 정보를 담는 세 개의 문자열 필드만 포함하고 있으므로, 일관성을 위해 다음과 같이 수정을 권장드립니다.

1. `univApplyInfos` → `univApplyInfo`로 단수형으로 변경
   - 타입이 단일 객체이므로 단수형 명명이 의도를 더 명확하게 전달합니다.
2. `mentorInfos`는 `List<MatchedInfoResponse>` 타입과 잘 맞으니 그대로 유지하셔도 됩니다. 👍
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/example/solidconnection/admin/dto/MenteeInfoResponse.java`
around lines 5 - 8, Rename the record component univApplyInfos to univApplyInfo
in the MenteeInfoResponse record to match its single-object type
UnivApplyInfoResponse (leave mentorInfos as-is); update all references/usages of
MenteeInfoResponse.univApplyInfos to MenteeInfoResponse.univApplyInfo and adjust
any constructors, mappers or serializers that reference the old name.


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import java.time.ZonedDateTime;

public record MentorApplicationHistoryInfoResponse(
MentorApplicationStatus mentorApplicationStatus,
String rejectedReason,
ZonedDateTime createdAt
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.dto;

import java.util.List;

public record MentorInfoResponse(
List<MatchedInfoResponse> menteeInfos,
List<MentorApplicationHistoryInfoResponse> mentorApplicationHistory
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.admin.dto;

import java.time.ZonedDateTime;

import com.example.solidconnection.report.domain.ReportType;

public record ReportedHistoryResponse(
ZonedDateTime reportedDate,
ReportType reportType
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.solidconnection.admin.dto;

import java.time.ZonedDateTime;

import com.example.solidconnection.report.domain.ReportType;
import com.example.solidconnection.report.domain.TargetType;

public record ReportedInfoResponse(
ZonedDateTime reportedDate,
TargetType targetType,
ReportType reportType
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.dto;

import java.util.List;

public record RestrictedUserInfoDetailResponse(
List<ReportedHistoryResponse> reportedHistoryResponses,
List<BannedHistoryResponse> bannedHistoryResponses
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.UserStatus;

public record RestrictedUserSearchCondition(
Role role,
UserStatus userStatus,
String keyword
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.UserStatus;

public record RestrictedUserSearchResponse(
String nickname,
Role role,
UserStatus userStatus,
ReportedInfoResponse reportedInfoResponse,
BannedInfoResponse bannedInfoResponse
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.solidconnection.admin.dto;

public record UnivApplyInfoResponse(
String firstChoiceUnivName,
String secondChoiceUnivName,
String thirdChoiceUnivName
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.admin.dto;

import java.util.List;

public record UserInfoDetailResponse(
MentorInfoResponse mentorInfoResponse, // 멘티일 경우 null
MenteeInfoResponse menteeInfoResponse, // 멘토일 경우 null
List<ReportedHistoryResponse> reportedHistoryResponses
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.siteuser.domain.Role;

public record UserSearchCondition(
Role role,
String keyword
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.solidconnection.admin.dto;

import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.UserStatus;

public record UserSearchResponse(
String nickname,
String email,
Role role,
UserStatus userStatus
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.example.solidconnection.admin.service;

import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse;
import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition;
import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse;
import com.example.solidconnection.admin.dto.UserInfoDetailResponse;
import com.example.solidconnection.admin.dto.UserSearchCondition;
import com.example.solidconnection.admin.dto.UserSearchResponse;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.siteuser.repository.custom.SiteUserFilterRepository;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class AdminUserService {

private final SiteUserRepository siteUserRepository;
private final SiteUserFilterRepository siteUserFilterRepository;
Copy link
Member

Choose a reason for hiding this comment

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

커스템 레포지토리를 구현하는 경우 JPA 레포지토리에 추가로 extends 하여 사용하고 있습니다(== JPA 레포지토리만 import하도록).

이런 느낌으로 작성해주시면 좋을 거 같습니다

Suggested change
private final SiteUserFilterRepository siteUserFilterRepository;
public interface SiteUserRepository extends JpaRepository<SiteUser, Long>, SiteUserFilterRepository


@Transactional(readOnly = true)
public Page<UserSearchResponse> searchAllUsers(UserSearchCondition searchCondition, Pageable pageable) {
return siteUserFilterRepository.searchAllUsers(searchCondition, pageable);
}

@Transactional(readOnly = true)
public Page<RestrictedUserSearchResponse> searchRestrictedUsers(RestrictedUserSearchCondition searchCondition, Pageable pageable) {
return siteUserFilterRepository.searchRestrictedUsers(searchCondition, pageable);
}

@Transactional(readOnly = true)
public UserInfoDetailResponse getUserInfoDetail(long userId) {
SiteUser siteUser = siteUserRepository.findById(userId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
return siteUserFilterRepository.getUserInfoDetailByUserId(siteUser.getId());
}
Copy link
Member

Choose a reason for hiding this comment

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

@Override
public UserInfoDetailResponse getUserInfoDetailByUserId(long userId) {
    SiteUser user = queryFactory
            .selectFrom(siteUser)
            .where(siteUser.id.eq(userId))
            .fetchOne();

    // ...
}

getUserInfoDetail 메서드에서 findById 메서드로 SiteUser 조회 한 번, 레포지토리의 getUserInfoDetailByUserId 메서드에서 SiteUser 총 두 번의 조회가 발생하고 있습니다. 인자로 siteUser 만 넘겨도 좋을 거 같아요 !


@Transactional(readOnly = true)
public RestrictedUserInfoDetailResponse getRestrictedUserInfoDetail(long userId) {
SiteUser siteUser = siteUserRepository.findById(userId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
return siteUserFilterRepository.getRestrictedUserInfoDetail(siteUser.getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.solidconnection.siteuser.repository.custom;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse;
import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition;
import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse;
import com.example.solidconnection.admin.dto.UserInfoDetailResponse;
import com.example.solidconnection.admin.dto.UserSearchCondition;
import com.example.solidconnection.admin.dto.UserSearchResponse;

public interface SiteUserFilterRepository {

Page<UserSearchResponse> searchAllUsers(UserSearchCondition searchCondition, Pageable pageable);

Page<RestrictedUserSearchResponse> searchRestrictedUsers(RestrictedUserSearchCondition searchCondition, Pageable pageable);

UserInfoDetailResponse getUserInfoDetailByUserId(long userId);

RestrictedUserInfoDetailResponse getRestrictedUserInfoDetail(long userId);
}
Loading
Loading