-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] Member, Challenger 도메인 기초 엔티티 기초 설계 & OAuth 기초 생성 #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
chore; domain class 내부 USER->MEMBER 이름 변경 OpenApiConfig는 변경사항은 없으나 코드 가독성을 위해서 분리 처리 Swagger Tag는 Enum으로 생성하여 관리하도록 함 (초반에는 asciidocs와 병행해서 관리) CommonErrorCode에 주석 추가해서 설명 명시
dockerfile은 정말 임시용이라 사용하시면 안됩니다!! 백업용, cicd 구현 완료 후 적용 예정
- SwaggerTag에 있는 오타를 수정하였습니다. - OAuth2 로그인 성공 및 실패 핸들러 구현하였습니다. (변경 필요) - 공통 에러 코드에 Spring Security 관련 에러 코드를 추가하였습니다 - User->Member 도메인 변경에 따라서 어노테이션 명을 변경하였습니다. - OAuth 핸들러를 제외한 기타 설정 또한 적용하였습니다. - 기존 WootecoStyle이 적용되지 않은 코드들을 일괄 포맷팅 진행하였습니다.
API 단 및 타 도메인에서 필요한 use case들은 추후 수정 예정입니다.
이메일 인증 관련 및 OAuth 자체를 제외한 Member과 연동되는 부분은 구현체가 정상적으로 구성되지 않았습니다. 도메인 관련 협업을 위해 WIP로 제출합니다.
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthrough광범위한 OAuth2 인증 통합, Member 및 Challenger 도메인 구현, 보안 계층 재구성을 포함한 대규모 기능 추가입니다. 데이터베이스 스키마 정의, 엔티티/서비스/포트 계층 구현, OpenAPI 구성 개선, 그리고 회원 등록/OAuth 로그인 워크플로우를 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as OAuth2 사용자
participant Frontend as Frontend
participant Controller as OAuth2 Callback<br/>(SecurityConfig)
participant CustomOAuth2UserService as CustomOAuth2UserService
participant DefaultOAuth2UserService as DefaultOAuth2UserService
participant OAuth2Provider as OAuth2 Provider<br/>(Google/Kakao/etc)
participant OAuthLoginService as OAuthLoginService<br/>(ProcessOAuthLoginUseCase)
participant MemberPersistence as MemberPersistenceAdapter
participant DB as Database
participant SuccessHandler as OAuth2SuccessHandler
participant JwtProvider as JwtTokenProvider
User->>Frontend: OAuth2 로그인 클릭
Frontend->>Controller: /oauth2/authorization/{provider}로 리디렉션
Controller->>OAuth2Provider: OAuth2 권한 요청
OAuth2Provider-->>User: 로그인 동의 요청
User-->>OAuth2Provider: 동의
OAuth2Provider-->>Controller: Authorization Code 반환
Controller->>CustomOAuth2UserService: loadUser(OAuth2UserRequest)
CustomOAuth2UserService->>DefaultOAuth2UserService: loadUser(userRequest)
DefaultOAuth2UserService->>OAuth2Provider: 사용자 정보 조회
OAuth2Provider-->>DefaultOAuth2UserService: 사용자 정보 응답
DefaultOAuth2UserService-->>CustomOAuth2UserService: OAuth2User 반환
CustomOAuth2UserService->>CustomOAuth2UserService: OAuth2Attributes로 매핑
CustomOAuth2UserService->>OAuthLoginService: processOAuthLogin(ProcessOAuthLoginCommand)
OAuthLoginService->>MemberPersistence: findByProviderAndProviderUserId()
MemberPersistence->>DB: 기존 OAuth 연결 조회
alt 기존 사용자 존재
DB-->>MemberPersistence: MemberOAuth 반환
MemberPersistence-->>OAuthLoginService: Optional<MemberOAuth>
OAuthLoginService-->>CustomOAuth2UserService: memberId 반환
else 신규 사용자
DB-->>MemberPersistence: 없음
MemberPersistence-->>OAuthLoginService: Optional.empty()
OAuthLoginService->>MemberPersistence: save(Member) + saveOAuth(MemberOAuth)
MemberPersistence->>DB: Member/MemberOAuth 저장
DB-->>MemberPersistence: 저장 완료
MemberPersistence-->>OAuthLoginService: memberId 반환
end
OAuthLoginService-->>CustomOAuth2UserService: memberId
CustomOAuth2UserService->>CustomOAuth2UserService: MemberPrincipal(memberId, email, attributes, nameAttributeKey) 생성
CustomOAuth2UserService-->>SuccessHandler: MemberPrincipal 반환
SuccessHandler->>JwtProvider: createAccessToken(memberId, roles)
JwtProvider-->>SuccessHandler: accessToken
SuccessHandler->>JwtProvider: createRefreshToken(memberId)
JwtProvider-->>SuccessHandler: refreshToken
SuccessHandler->>Frontend: /oauth/callback?accessToken=...&refreshToken=... 리디렉션
Frontend-->>User: 로그인 완료 및 토큰 저장
sequenceDiagram
participant User as 클라이언트
participant Controller as MemberController
participant ManageMemberUseCase as ManageMemberUseCase
participant MemberCommandService as MemberCommandService
participant LoadMemberPort as LoadMemberPort
participant SaveMemberPort as SaveMemberPort
participant DB as Database
participant Member as Member Entity
User->>Controller: POST /v1/member/register/complete<br/>(CompleteRegisterMemberRequest)
Controller->>Controller: `@CurrentMember에서` memberId 추출
Controller->>ManageMemberUseCase: completeRegister(CompleteRegisterMemberCommand)
ManageMemberUseCase->>MemberCommandService: completeRegister(command)
MemberCommandService->>LoadMemberPort: findById(memberId)
LoadMemberPort->>DB: Member 조회
DB-->>LoadMemberPort: Member 반환
LoadMemberPort-->>MemberCommandService: Member
MemberCommandService->>Member: validateIfRegisterAvailable()
alt 이미 등록됨 (상태 != PENDING)
Member-->>MemberCommandService: MemberDomainException
MemberCommandService-->>Controller: 예외 발생
else 등록 가능 (PENDING 상태)
Member-->>MemberCommandService: 통과
MemberCommandService->>Member: updateProfile(nickname, schoolId, profileImageId)
MemberCommandService->>Member: activate()
MemberCommandService->>SaveMemberPort: save(member)
SaveMemberPort->>DB: Member 업데이트
DB-->>SaveMemberPort: 저장 완료
MemberCommandService->>SaveMemberPort: saveTermAgreements(agreements)
SaveMemberPort->>DB: MemberTermAgreement 일괄 저장
DB-->>SaveMemberPort: 저장 완료
SaveMemberPort-->>MemberCommandService: List<MemberTermAgreement>
MemberCommandService-->>ManageMemberUseCase: memberId
end
ManageMemberUseCase-->>Controller: memberId
Controller-->>User: ApiResponse(Long memberId)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (13)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 73
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/main/java/com/umc/product/global/exception/constant/Domain.java (1)
6-6: 주석의 오타를 수정해주세요."Buisioness"를 "Business"로 수정해야 합니다.
🔎 수정 제안
- * 의도에 의해 발생한 Buisioness Exception의 경우, 도메인을 구분하기 위해 사용됩니다. + * 의도에 의해 발생한 Business Exception의 경우, 도메인을 구분하기 위해 사용됩니다.src/main/java/com/umc/product/global/security/resolver/CurrentUserArgumentResolver.java (1)
14-15: 클래스명이 변경된 도메인 명명 규칙과 불일치합니다.클래스명이
CurrentUserArgumentResolver이지만, 실제로는MemberPrincipal과@CurrentMember애노테이션을 처리합니다. 도메인 명명이 USER에서 MEMBER로 변경되었으므로 클래스명도CurrentMemberArgumentResolver로 변경하는 것이 일관성 측면에서 바람직합니다.🔎 권장 수정안
-public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { +public class CurrentMemberArgumentResolver implements HandlerMethodArgumentResolver {src/main/java/com/umc/product/global/config/SecurityConfig.java (1)
155-167: CORS 설정 하드코딩 확인
allowedOriginPatterns가localhost로 하드코딩되어 있습니다. 프로덕션 환경에서는 외부 프로퍼티(application.yml)를 통해 설정하도록 개선하는 것이 좋습니다.🔎 프로퍼티 기반 CORS 설정 제안
+ @Value("${cors.allowed-origins}") + private List<String> allowedOrigins; @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOriginPatterns( - List.of("http://localhost:8080", "http://localhost:3000")); + configuration.setAllowedOriginPatterns(allowedOrigins); configuration.setAllowedMethods(List.of("*")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true);src/main/java/com/umc/product/temp/controller/TempController.java (1)
25-30: 테스트 토큰 발급 엔드포인트 보안 고려사항
getTestToken메서드가@Profile("local | dev")로 제한되어 있지만, 운영 환경에 실수로 해당 프로필이 활성화될 경우 보안 위험이 있습니다. 추가적인 안전장치를 고려해 주세요.또한,
userId에 대한 유효성 검증 없이 토큰이 발급됩니다. 존재하지 않는 사용자 ID로도 토큰이 생성될 수 있습니다.🔎 추가 보안 검증 제안
@Operation(summary = "테스트용 토큰 발급", description = "userId로 JWT 토큰을 발급합니다.") @Public @GetMapping("/token/at/{userId}") public ApiResponse<String> getTestToken(@PathVariable Long userId) { + if (userId == null || userId <= 0) { + throw new IllegalArgumentException("Invalid userId"); + } return ApiResponse.onSuccess(jwtTokenProvider.createAccessToken(userId, null)); }
🤖 Fix all issues with AI Agents
In @docker/app/dockerfile:
- Around line 56-57: Enable the Docker HEALTHCHECK and point it at the
management actuator port instead of 8080: update the commented HEALTHCHECK to
use the management port (use an env var like MANAGEMENT_PORT with default 9090)
and include sensible flags (--interval, --timeout, --start-period, --retries);
ensure the URL targets /actuator/health per application.yml; also ensure the
base image includes a HTTP client (install curl or use wget) so the HEALTHCHECK
CMD can run successfully.
In @docs/erd.dbml:
- Line 575: Rename the misspelled column identifier `submited_ip` to
`submitted_ip` in the ERD definition (replace `submited_ip varchar` with
`submitted_ip varchar`) and search for other schema references (migrations,
models, queries, or docs) that use `submited_ip` and update them to
`submitted_ip` to keep the schema consistent.
- Around line 287-294: Add a foreign key column to link workbook_mission to its
parent original_workbook: add an original_workbook_id column (type bigint) to
the workbook_mission table and create a FK reference to original_workbook.id
(e.g., original_workbook_id -> original_workbook.id) so each WorkbookMission
(workbook_mission) explicitly points to its OriginalWorkbook; ensure the
column's nullability matches domain rules and update any relevant indexes or
constraints accordingly.
In
@src/main/java/com/umc/product/challenger/application/port/in/command/ManageChallengerUseCase.java:
- Around line 7-19: The interface methods givePointsToChallenger,
assignRoleToChallenger, and revokeRoleFromChallenger lack necessary parameters;
change each method to accept a Command object (e.g.,
GivePointsToChallengerCommand, AssignRoleToChallengerCommand,
RevokeRoleFromChallengerCommand) carrying required fields (challengerId,
pointType, amount/description/relatedWorkbookId for points; challengerId,
roleType, organizationType, organizationId, leadingPart, gisuId for assign;
challengerId and roleId or roleType for revoke), and update the
ManageChallengerUseCase method signatures to void
givePointsToChallenger(GivePointsToChallengerCommand), void
assignRoleToChallenger(AssignRoleToChallengerCommand), and void
revokeRoleFromChallenger(RevokeRoleFromChallengerCommand).
In
@src/main/java/com/umc/product/challenger/application/port/in/query/ChallengerPublicInfo.java:
- Around line 7-13: The ChallengerPublicInfo query record is missing null-check
validation; add @NotNull to each record component (challengerId, memberId,
gisuId, part) in the ChallengerPublicInfo record header so the validation
framework enforces non-null values, and add the corresponding import (e.g.,
jakarta.validation.constraints.NotNull or javax.validation.constraints.NotNull)
at the top of the file; keep the @Builder annotation as-is and ensure the
annotations are applied directly to the record components.
- Around line 15-23: The from(Challenger challenger) factory method lacks
null-safety; add a defensive null check at the start of
ChallengerPublicInfo.from to throw a clear exception (e.g.,
IllegalArgumentException or NullPointerException with message) if challenger is
null, or alternatively annotate the parameter with @NonNull (importing the
appropriate annotation) to enforce non-null contract; ensure you reference the
ChallengerPublicInfo.from(Challenger) method and preserve existing builder usage
when returning the mapped object.
In
@src/main/java/com/umc/product/challenger/application/port/in/query/GetChallengerUseCase.java:
- Line 4: The TODO notes that challenger public/private info separation is
needed; implement this by adding a new use-case method
getChallengerPrivateInfo(Long challengerId) in the GetChallengerUseCase
interface (and its implementing service), or extend the existing
getChallenger(...) signature to accept a caller context (e.g., CurrentMember)
and perform permission-based filtering there, and introduce a separate DTO
(ChallengerPrivateInfo) to carry private fields; update the service layer to
return ChallengerPublicInfo for unauthenticated/unauthorized calls and
ChallengerPrivateInfo (or a combined wrapper) for authorized callers, and add
unit tests covering both public and private access paths.
In @src/main/java/com/umc/product/challenger/domain/Challenger.java:
- Around line 44-50: The field name challengerPoint in class Challenger should
be renamed to the plural challengerPoints to follow collection naming
conventions; update the field declaration (List<ChallengerPoint>
challengerPoints = new ArrayList<>()), any getters/setters, constructors, and
all usages across the codebase to reference challengerPoints instead of
challengerPoint, leaving the @OneToMany mapping (mappedBy = "challenger", fetch,
cascade, orphanRemoval) unchanged so JPA mapping stays intact.
- Around line 35-42: The enum fields in Challenger (the fields part and status)
are missing @Enumerated(EnumType.STRING) so they default to ORDINAL; update the
Challenger class by annotating the fields Challenger.part and Challenger.status
with @Enumerated(EnumType.STRING) to persist enum names instead of ordinals and
avoid future data-mapping issues.
In @src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java:
- Around line 32-33: Rename the DB column mapping for the ChallengerPoint.type
field to avoid confusion: change the @Column name from "point" to "point_type"
on the ChallengerPoint class so the field type maps to a logical column name;
update any related JPA queries, DTOs, repository code, and database
migration/schema (or @Column annotations in other entities) to use "point_type"
and run migrations to keep schema and code in sync.
- Around line 16-40: ChallengerPoint lacks a creation method and audit fields:
have ChallengerPoint extend BaseEntity (to inherit created/modified timestamps)
and add a domain creation API (either a @Builder or static factory like
of/create) that accepts Challenger challenger, PointType type, String
description, Long relatedWorkbookId and sets the fields (including linking
challenger). Ensure the constructor remains protected and the new
factory/builder enforces non-null challenger and type and returns a
fully-initialized ChallengerPoint instance.
- Around line 28-30: The @ManyToOne association on the
ChallengerPoint.challenger field is currently using the default EAGER fetch and
should be changed to LAZY to avoid N+1 and unnecessary queries; update the
@ManyToOne annotation on the challenger field to include fetch = FetchType.LAZY
(and add the import for javax.persistence.FetchType if missing) so the
relationship matches the parent's lazy-loading strategy.
- Around line 28-30: Challenger allows external code to mutate its
ChallengerPoint collection via the generated getter, so add domain methods on
the Challenger entity—e.g., addPoint(ChallengerPoint point) and
removePoint(ChallengerPoint point)—that encapsulate modifications to the
collection and enforce business rules; in these methods set/unset the
bi-directional association (call point.setChallenger(this) on add, and
point.setChallenger(null) on remove) and validate any constraints before
mutating the internal collection, and change the collection accessor to return
an unmodifiable view or omit a public setter to prevent direct external adds.
In @src/main/java/com/umc/product/challenger/domain/ChallengerRole.java:
- Around line 27-29: The @ManyToOne mapping for the challenger field in
ChallengerRole is missing an explicit fetch strategy (defaults to EAGER) which
can cause N+1 queries; update the @ManyToOne annotation on the Challenger
challenger field in ChallengerRole to specify fetch = FetchType.LAZY (and add
the import for javax.persistence.FetchType if needed) so the associated
Challenger is loaded lazily.
- Around line 22-49: ChallengerRole is missing a public domain constructor; add
a public constructor on ChallengerRole that accepts the required fields
(Challenger challenger, RoleType roleType, OrganizationType organizationType,
Long organizationId, ChallengerPart leadingPart, Long gisuId) and assigns them
to the corresponding fields, leaving id to be generated; ensure the constructor
validates non-nullable inputs (or uses Objects.requireNonNull) and do not
perform external side-effects—use domain methods on ChallengerRole for any
future state changes instead of mutating fields elsewhere.
In @src/main/java/com/umc/product/challenger/domain/enums/ChallengerPart.java:
- Around line 3-11: Enum naming is inconsistent for multi-word constants: update
ChallengerPart to use underscore-separated constants (change SPRINGBOOT ->
SPRING_BOOT and NODEJS -> NODE_JS) to follow typical enum naming conventions,
and optionally add brief Javadoc or inline comments for each constant (similar
to ChallengerStatus) to improve readability; modify the declarations in the
ChallengerPart enum accordingly and run tests/compile to ensure no usages break.
In @src/main/java/com/umc/product/challenger/domain/enums/ChallengerStatus.java:
- Line 7: Enum constant comment for WITHDRAWN in ChallengerStatus has a typo
"자진탈부"; update the inline comment to the correct word "자진탈퇴" by editing the
comment next to the WITHDRAWN enum value in ChallengerStatus.
In @src/main/java/com/umc/product/challenger/domain/enums/OrganizationType.java:
- Around line 3-7: OrganizationType enum currently only defines constants
(CENTRAL, CHAPTER, SCHOOL); add a description/displayName field plus a
constructor and a getter to store a user-facing string (e.g., Korean labels) and
update each constant to pass its description; adjust any code that prints the
enum to use the new getter or override toString in OrganizationType to return
the description so UI rendering shows the display name instead of the enum
identifier.
- Around line 3-7: Rename the enum OrganizationType to
ChallengerOrganizationType to follow the {Domain}{Type} naming convention and
clarify domain ownership; update all usages (e.g., the organizationType field in
ChallengerRole) to reference ChallengerOrganizationType and recompile; ensure
the @Enumerated(EnumType.STRING) mapping and any import statements, switch/case
usages, tests, and database column references (e.g., organization_type) are
updated accordingly to avoid compile/runtime errors.
In @src/main/java/com/umc/product/challenger/domain/enums/PointType.java:
- Around line 3-7: Rename the enum PointType to follow the {Domain}{Type}
pattern by renaming it to ChallengerPointType: change the enum declaration from
PointType to ChallengerPointType and update all usages/imports throughout the
codebase to refer to ChallengerPointType (including switch cases, variable
types, static imports, and unit tests); ensure the file name and any references
in serialization/mapping (e.g., JSON names, DB converters) are updated if they
rely on the old type name to avoid runtime errors.
In @src/main/java/com/umc/product/challenger/domain/enums/RoleType.java:
- Around line 3-23: RoleType currently only lists constants; add extensible
metadata to support future authority checks by converting RoleType into a
parameterized enum with fields for int level, String displayName, and a new
OrganizationType enum (CENTRAL, CHAPTER, SCHOOL, GENERAL), add a constructor and
getters (or annotate with Lombok @Getter/@AllArgsConstructor), initialize each
enum constant with appropriate level/displayName/organizationType (e.g.,
CENTRAL_PRESIDENT(1, "총괄", OrganizationType.CENTRAL), ... CHALLENGER(99, "챌린저",
OrganizationType.GENERAL)), and add a helper method
hasHigherAuthorityThan(RoleType other) that compares levels; create
OrganizationType enum referenced by RoleType.
- Around line 3-23: Rename the enum symbol RoleType to ChallengerRoleType (and
rename the file accordingly) to follow the {Domain}{Type} pattern; update the
enum declaration from "public enum RoleType" to "public enum ChallengerRoleType"
while keeping all existing constants unchanged, then search & update all
usages/imports/references (including switch statements,
serialization/deserialization, tests, and any mappings) to use
ChallengerRoleType so compilation and runtime behavior remain identical.
In
@src/main/java/com/umc/product/challenger/domain/exception/ChallengerErrorCode.java:
- Line 12: Update the CHALLENGER_NOT_FOUND enum entry in ChallengerErrorCode to
use domain-consistent Korean wording: replace the message "사용자를 찾을 수 없습니다." with
"챌린저를 찾을 수 없습니다." so the error text matches the Challenger domain (identify
CHALLENGER_NOT_FOUND inside the ChallengerErrorCode enum and modify its message
string).
In @src/main/java/com/umc/product/global/config/OpenApiConfig.java:
- Around line 64-69: In the tags() method replace the terminal collector call
using Collectors.toList() with the Stream.toList() convenience method: locate
the private List<Tag> tags() method that streams
Arrays.stream(SwaggerTag.values()),
sorted(Comparator.comparingInt(SwaggerTag::getOrder)), and mapped with
SwaggerTag::toTag, then call .toList() instead of .collect(Collectors.toList())
to simplify the code for Java 16+.
- Around line 24-25: Convert the instance fields accessToken and refreshToken
into class-level constants by declaring them as static final and renaming to
uppercase (ACCESS_TOKEN and REFRESH_TOKEN) to reflect constant semantics; update
all usages of accessToken (e.g., where referenced at the two places noted) to
ACCESS_TOKEN and refreshToken (the two-line usage) to REFRESH_TOKEN so the code
compiles and clearly signals these are immutable shared constants.
In @src/main/java/com/umc/product/global/config/SecurityConfig.java:
- Line 10: The CustomOAuth2UserService class is placed under
member.adapter.in.web.oauth but OAuth2AuthenticationSuccessHandler and
OAuth2AuthenticationFailureHandler live in global.security.oauth; to follow
project convention move CustomOAuth2UserService into the global.security.oauth
package (update its package declaration), update imports (e.g., in
SecurityConfig and any other references) to import
com.umc.product.global.security.oauth.CustomOAuth2UserService, adjust any
package-private access if needed, and run a build/tests to ensure there are no
unresolved references.
- Around line 89-97: Add end-to-end integration tests to verify the full OAuth2
login flow with sessionCreationPolicy(SessionCreationPolicy.STATELESS) and
oauth2Login configured (ensure customOAuth2UserService and oAuth2SuccessHandler
generate and return JWTs statelessly); update the
OAuth2AuthenticationFailureHandler implementation referenced by
oAuth2FailureHandler so it does not leak exception.getMessage() in
redirect/query params—replace detailed exception messages with a generic error
code/message before sending to the client.
In @src/main/java/com/umc/product/global/constant/SwaggerTag.java:
- Around line 56-67: Add a unit test that asserts the Constants inner class
values stay synchronized with the SwaggerTag enum: iterate over each value of
the SwaggerTag enum (or the enum that represents the same tags) and verify a
matching constant exists in SwaggerTag.Constants with the same name and expected
string value, failing the test if any constant is missing or differs; this
ensures any future change to the enum or Constants will be caught automatically
rather than relying on manual review.
- Line 23: Fix the typo in the comment within the SwaggerTag class: change
"Constatns" to "Constants" in the comment line that reads "// 추가하는 경우, 하단의
Constatns에도 반드시 동일하게 추가할 것" so it correctly reads "// 추가하는 경우, 하단의 Constants에도
반드시 동일하게 추가할 것".
- Around line 16-19: Enum constants in SwaggerTag (curriculum, schedule,
community, notice) use lowercase; rename them to UPPER_SNAKE_CASE (CURRICULUM,
SCHEDULE, COMMUNITY, NOTICE) to follow Java conventions and update any
corresponding names in the inner Constants class so they match the new enum
constant identifiers; ensure all usages/reference sites in the codebase are
updated to the new names to avoid compile errors.
In
@src/main/java/com/umc/product/global/exception/constant/CommonErrorCode.java:
- Around line 28-29: The new error codes SECURITY_NOT_GIVEN and
SECURITY_FORBIDDEN in the CommonErrorCode enum use a different naming convention
than the existing DOMAIN-CATEGORY-NUMBER pattern; either rename them to follow
the existing pattern (e.g., change SECURITY_NOT_GIVEN -> COMMON-SECURITY-0001
and SECURITY_FORBIDDEN -> COMMON-SECURITY-0002 within the CommonErrorCode enum)
or move them into a new enum SecurityErrorCode with the same HttpStatus/messages
and use SECURITY-0001/0002 there (update all references to use the new enum
constants). Ensure you update any usages/imports of SECURITY_NOT_GIVEN and
SECURITY_FORBIDDEN to the new names or enum.
In @src/main/java/com/umc/product/global/security/JwtAuthenticationFilter.java:
- Line 34: MemberPrincipal is being constructed with an empty email string; fix
by either adding email to the JWT and extracting it, or loading it from the DB.
Preferred fix: update JwtTokenProvider.createToken(...) to add email as a claim
and implement a getEmail(token) extractor, then in JwtAuthenticationFilter use
JwtTokenProvider.getEmail(token) to populate new MemberPrincipal(userId, email).
Alternate fix: inject your User repository into JwtAuthenticationFilter and load
the user by userId (e.g., userRepository.findById(userId).map(User::getEmail))
and pass that email into MemberPrincipal; ensure MemberPrincipal(email) is
non-null or document it if optional.
In @src/main/java/com/umc/product/global/security/MemberPrincipal.java:
- Around line 18-25: The constructor MemberPrincipal currently assigns the
incoming Map<String, Object> attributes directly, exposing internal state;
change it to defensively copy and/or wrap the map (e.g., new
HashMap<>(attributes) and optionally Collections.unmodifiableMap(...)) before
assigning to the attributes field so external mutations cannot alter the
principal's internal map; ensure null-handling (use emptyMap if null) and update
any other constructors or factory methods that set attributes similarly.
- Around line 43-49: In MemberPrincipal.getName(), avoid returning the literal
"null" when attributes contains no value for nameAttributeKey: check whether
attributes containsKey(nameAttributeKey) and that
attributes.get(nameAttributeKey) is non-null (or use Objects.toString(value,
null) check) and only then return its string; otherwise return
String.valueOf(memberId) as a safe fallback. Ensure you reference the attributes
map and nameAttributeKey and convert the attribute value to a string only after
the null check.
In @src/main/java/com/umc/product/global/security/oauth/OAuth2Attributes.java:
- Around line 60-73: The ofNaver method can NPE when response is null; add a
null-check for response (the Map obtained from
attributes.get(userNameAttributeName)) and handle it before calling
response.get(...). Modify ofNaver to validate that response != null (and
optionally that required keys like "id","name","email" exist), throw a clear
IllegalArgumentException or return a safe default OAuth2Attributes via
OAuth2Attributes.builder() if absent, and ensure userNameAttributeName is used
only after response is validated so subsequent calls to response.get("id"),
response.get("name"), response.get("email"), and response.get("nickname") are
safe.
- Around line 44-58: The ofKakao method can NPE when
attributes.get("kakao_account") or its "profile" is missing; update ofKakao to
defensively handle nulls by checking attributes contains "kakao_account" and
that kakaoAccount and profile are non-null and instances of Map before casting,
use empty maps or safe defaults for name, nickname, and email if fields are
absent, and derive providerId via safe lookup (e.g.,
String.valueOf(attributes.getOrDefault(userNameAttributeName, ""))). Ensure the
builder calls (provider, providerId, name, email, nickname, attributes,
nameAttributeKey) receive these validated or defaulted values to avoid runtime
NullPointerException.
In
@src/main/java/com/umc/product/global/security/oauth/OAuth2AuthenticationFailureHandler.java:
- Around line 48-52: In OAuth2AuthenticationFailureHandler, avoid exposing
exception.getMessage() to the frontend via the targetUrl; instead log the full
exception message/server details (use the caught Exception or
AuthenticationException) to your server logger inside the failure handler and
build targetUrl using a generic user-facing message such as "Authentication
failed" or "oauth_failed" (replace the .queryParam("message",
exception.getMessage()) with .queryParam("message", "Authentication failed") or
similar), keeping redirectUrl and targetUrl construction otherwise unchanged.
- Around line 15-18: 클래스 OAuth2AuthenticationFailureHandler에 불필요하게 붙어있는
@RequiredArgsConstructor 어노테이션을 제거하세요; 이 클래스에는 final 필드가 없고 @Value 필드들은 setter
주입으로 처리되므로 생성자 자동생성이 필요 없습니다. 해당 어노테이션을 삭제해도 동작에 영향이 없도록 확인하고, 클래스 선언부에서
@RequiredArgsConstructor만 제거하면 됩니다.
In
@src/main/java/com/umc/product/global/security/oauth/OAuth2AuthenticationSuccessHandler.java:
- Around line 32-33: In OAuth2AuthenticationSuccessHandler, avoid direct cast of
authentication.getPrincipal(); instead check that authentication.getPrincipal()
is an instance of MemberPrincipal before casting (reference MemberPrincipal and
the authentication.getPrincipal() call), and if not, handle defensively by
throwing a clear AuthenticationServiceException (or custom auth exception) or
logging and aborting the flow so a ClassCastException cannot propagate.
- Around line 47-48: The current log.info call in
OAuth2AuthenticationSuccessHandler logs targetUrl which contains accessToken and
refreshToken; remove or sanitize sensitive tokens before logging by either (1)
not logging targetUrl at all and replacing the line with a generic log like
"Redirecting to OAuth callback" or (2) strip/mask query parameters named
accessToken and refreshToken from targetUrl (e.g., build a sanitizedTargetUrl
without those params) and log that instead; keep the actual redirect using
getRedirectStrategy().sendRedirect(request, response, targetUrl).
In
@src/main/java/com/umc/product/global/security/resolver/CurrentUserArgumentResolver.java:
- Around line 39-43: Update the comment that incorrectly references
"UserPrincipal" to accurately state that a MemberPrincipal is returned; adjust
the comment near the authentication.getPrincipal() check in
CurrentUserArgumentResolver (the block that checks "if (principal instanceof
MemberPrincipal) { return principal; }") to mention MemberPrincipal instead of
UserPrincipal.
- Around line 19-22: Update the misleading comments in
CurrentUserArgumentResolver to match the actual annotations and types used:
replace references to @CurrentUser with @CurrentMember and replace UserPrincipal
with MemberPrincipal so the comments accurately describe the checks performed on
parameter.hasParameterAnnotation(CurrentMember.class) and
MemberPrincipal.class.isAssignableFrom(parameter.getParameterType()).
In
@src/main/java/com/umc/product/member/adapter/in/web/dto/request/CompleteRegisterMemberRequest.java:
- Around line 27-28: The agreedTerms field in CompleteRegisterMemberRequest is
only annotated with @NotNull so an empty list is allowed; update validation to
ensure required terms are agreed by either adding @NotEmpty to the agreedTerms
field to forbid empty lists or, preferably, create and apply a custom constraint
(e.g., @RequiredTerms with a RequiredTermsValidator) that inspects the
List<TermType> agreedTerms and fails when it does not contain the mandatory
TermType values (e.g., TermType.TERMS_OF_SERVICE and TermType.PRIVACY_POLICY);
annotate the agreedTerms field with the new constraint and implement the
validator logic to return a violation when any required term is missing.
In @src/main/java/com/umc/product/member/adapter/in/web/MemberController.java:
- Around line 49-54: The getUser method in MemberController currently returns
MemberResponse directly; change its signature to return
ApiResponse<MemberResponse> and wrap the result of
MemberResponse.from(memberInfo) with ApiResponse.success(...) (or the project's
standard ApiResponse factory), keeping the call to
getMemberUseCase.getById(memberId) and MemberResponse.from(memberInfo) intact so
the controller returns ApiResponse<MemberResponse> instead of raw
MemberResponse.
- Around line 33-46: The controller method completeRegister currently returns a
raw Long; update it to return the project's ApiResponse<T> wrapper (e.g.,
ApiResponse<Long>) and wrap the result before returning. Locate the
completeRegister method in MemberController and change its return type to
ApiResponse<Long>, call manageMemberUseCase.completeRegister(...) as before,
then return ApiResponse.success(userId) (or the equivalent factory method used
across the project) so responses conform to the ApiResponse<T> standard. Ensure
imports and any OpenAPI/Operation annotations remain intact and adjust tests if
they assert raw Long responses.
In
@src/main/java/com/umc/product/member/adapter/in/web/oauth/CustomOAuth2UserService.java:
- Around line 31-32: The debug logging in CustomOAuth2UserService currently
prints oAuth2User.getAttributes() which can expose sensitive data; remove or
replace this full-attributes log and either omit attribute logging entirely or
log only specific non-sensitive fields (e.g., registrationId, maybe a hashed or
masked userId/email) and mask values before logging; update the log calls in the
method that handles the OAuth2User (references: CustomOAuth2UserService, the
log.debug(...) lines using registrationId and oAuth2User.getAttributes()) so no
raw OAuth attribute map is written to logs.
- Around line 62-69: In CustomOAuth2UserService's catch block (catch(Exception
ex)) stop logging full exception messages and causes (avoid ex.getMessage() and
ex.getCause()) and instead log only non-sensitive details such as registrationId
and ex.getClass().getSimpleName(); then rethrow only
OAuth2AuthenticationException as-is and for any other exception wrap it into a
new OAuth2AuthenticationException (or a subclass) with a generic, non-sensitive
error message before throwing to avoid checked-exception issues and leaking
tokens/userdata.
- Line 29: Create a single reusable DefaultOAuth2UserService instance instead of
instantiating it per request: declare a private final field named delegate of
type OAuth2UserService<OAuth2UserRequest, OAuth2User> initialized with new
DefaultOAuth2UserService() in the CustomOAuth2UserService class, and remove the
per-method instantiation so methods (e.g., loadUser) use the field delegate
directly.
In
@src/main/java/com/umc/product/member/adapter/out/persistence/MemberJpaRepository.java:
- Around line 8-14: 명시적 인덱스가 없으므로 Member 엔티티에 @Table(indexes = {...})를 추가해
email과 nickname 컬럼에 인덱스(idx_member_email, idx_member_nickname)를 정의하여
findByEmail, findByNickname, existsByEmail, existsByNickname 조회 성능과 인덱스 관리를 명확히
하세요; 기존 unique 제약은 유지하되 Member 클래스에 @Index(name="idx_member_email",
columnList="email")와 @Index(name="idx_member_nickname", columnList="nickname")를
선언하면 됩니다.
In
@src/main/java/com/umc/product/member/adapter/out/persistence/MemberPersistenceAdapter.java:
- Around line 34-38: The method findByNickname currently contains a developer
test comment ("QueryDSL 사용 테스트 하고자 넣은 method"); either remove the test-only code
or formalize it: if this repository needs a production lookup, delete the
comment and keep the method as a documented API (update Javadoc and ensure
MemberPersistenceAdapter.findByNickname delegates to
memberQueryRepository.findByNickname properly); if it was only for QueryDSL
testing, remove the method entirely and track the work in an issue or move test
helpers into test code. Ensure the change references the findByNickname method
and the comment so reviewers can verify the cleanup.
- Around line 75-78: The implementation in MemberPersistenceAdapter uses
memberOAuthRepository.findByProviderAndProviderId(provider, providerUserId)
which is inconsistent with the Port method name
LoadMemberOAuthPort.findByProviderAndProviderUserId and the parameter name
providerUserId; update the repository API (method name and parameter) to match
the port — rename repository method to findByProviderAndProviderUserId and its
parameter to providerUserId (or alternatively rename the port method to match
the repository), then update all call sites (e.g., MemberPersistenceAdapter) to
use the unified name so method semantics and parameter names are consistent
across LoadMemberOAuthPort, MemberPersistenceAdapter, and memberOAuthRepository.
In
@src/main/java/com/umc/product/member/adapter/out/persistence/MemberQueryRepository.java:
- Around line 15-19: countAllMembers currently loads all Member entities into
memory via stream().count(), causing OOM risk; change it to perform a DB-level
count by using QueryDSL's count projection: use
queryFactory.select(QMember.member.count()).fetchOne() (or fetchFirst()/handle
possible null) to return the total as a Long, referencing countAllMembers(),
queryFactory and QMember.member.count().
In
@src/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.java:
- Around line 32-39: The method toTermAgreementEntities(Long memberId) accepts a
memberId parameter even though the CompleteRegisterMemberCommand record already
has a memberId field; remove the redundant method parameter and update
toTermAgreementEntities() to reference the record's memberId directly (use the
record's memberId accessor) when building MemberTermAgreement instances so there
is no risk of mismatched IDs.
In
@src/main/java/com/umc/product/member/application/port/in/command/ManageMemberUseCase.java:
- Around line 3-10: The ManageMemberUseCase interface violates SRP by containing
two use cases; split it into two interfaces named RegisterMemberUseCase (with
Long register(RegisterMemberCommand)) and CompleteRegisterMemberUseCase (with
Long completeRegister(CompleteRegisterMemberCommand)), remove the old
ManageMemberUseCase, update any classes that previously implemented
ManageMemberUseCase to implement the appropriate new interface(s) (e.g., service
implementations, controllers, adapters), and fix imports/constructor
injections/usages to refer to RegisterMemberUseCase or
CompleteRegisterMemberUseCase accordingly.
In
@src/main/java/com/umc/product/member/application/port/in/command/ProcessOAuthLoginCommand.java:
- Around line 6-12: Add javax.validation.constraints.NotNull annotations to the
components of the ProcessOAuthLoginCommand record: annotate provider,
providerId, email, name and nickname with @NotNull (and add the import) so the
record fields carry Bean Validation metadata consistent with project
conventions; keep any existing Objects.requireNonNull checks in the canonical
constructor if desired, but ensure the component declarations are annotated with
@NotNull.
- Around line 13-17: ProcessOAuthLoginCommand's canonical constructor doesn't
validate the name field causing potential NPEs in
registerOAuthMemberWithPendingState(); add validation for name in the
ProcessOAuthLoginCommand canonical constructor using
Objects.requireNonNull(name, "name must not be null") or alternatively document
and implement a clear fallback when extracting OAuth data (e.g., supply a
default or map to generateNickname()) so that
registerOAuthMemberWithPendingState() never receives a null name; update
references to ProcessOAuthLoginCommand, name,
registerOAuthMemberWithPendingState(), and generateNickname() accordingly.
In
@src/main/java/com/umc/product/member/application/port/in/command/RegisterMemberCommand.java:
- Around line 20-27: The compact constructor for RegisterMemberCommand currently
only does null checks; replace or augment these with Bean Validation annotations
on the corresponding fields/constructor parameters: use @NotBlank for name and
nickname to prevent blank/whitespace, @Email (and @NotBlank) for email to
enforce format, @NotNull for oauthProvider and providerId if needed, and
@NotEmpty (or @Size(min=1)) for agreedTerms to ensure at least one agreed term.
After adding annotations to the RegisterMemberCommand fields/parameters, you can
remove the requireNonNull calls from the compact constructor and ensure
validation is triggered by your framework (e.g., @Valid where this command is
used).
In
@src/main/java/com/umc/product/member/application/port/in/query/GetMemberUseCase.java:
- Around line 3-11: Split the current GetMemberUseCase interface into two
single-responsibility interfaces: keep getById(Long userId) and
getByEmail(String email) in GetMemberUseCase, and move existsById(Long userId)
and existsByEmail(String email) into a new CheckMemberExistenceUseCase
interface; then update all implementors and injection sites to implement or
depend on the appropriate interface(s) (e.g., classes that previously
implemented GetMemberUseCase must implement both interfaces if they provide both
behaviors, and constructors/beans/fields using existsById/existsByEmail should
reference CheckMemberExistenceUseCase while callers of getById/getByEmail keep
using GetMemberUseCase).
In
@src/main/java/com/umc/product/member/application/port/in/query/MemberInfo.java:
- Around line 15-25: MemberInfo.from currently passes Member.getSchoolId() and
Member.getProfileImageId() through without documenting nullability; decide on a
consistent null contract: either make the MemberInfo fields explicitly nullable
(annotate the schoolId and profileImageId fields/constructor parameters with
@Nullable or change their types to Optional<Long>) so callers know null is
allowed, or enforce non-null at conversion by adding null checks in
MemberInfo.from and supplying a default or throwing a clear exception; update
the MemberInfo class signature and the from(Member) method accordingly and
ensure updateProfile() usage remains consistent with the chosen approach.
In
@src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java:
- Line 44: In MemberCommandService (around the method where step comments are
written), fix the comment numbering so steps are sequential: either renumber the
existing "4. Member 생성 및 저장" to "3." if there is no missing step, or if a
logical step is missing, insert the missing "3." comment with a short
description of that intermediate action (e.g., validation, DTO->entity
conversion, or persistence preparation) before the current "4." comment so the
sequence reads 1, 2, 3, 4 consistently.
- Line 26: Rename the inconsistent field saveUserPort to saveMemberPort in
MemberCommandService: change the private final field declaration of type
SaveMemberPort from saveUserPort to saveMemberPort, and update all
references/usages inside the class (constructor parameter, assignments, and
calls) that currently use saveUserPort (e.g., occurrences around the constructor
and methods at the previously noted spots) so they reference saveMemberPort
instead to match the SaveMemberPort type and improve clarity.
In
@src/main/java/com/umc/product/member/application/service/command/OAuthLoginService.java:
- Line 32: The TODO comment in OAuthLoginService (class OAuthLoginService) about
"임시 회원가입 하는 회원" must be tracked as an issue: create a ticket documenting
required work for handling PENDING-state temporary registrations and OAuth
flows, and replace the inline TODO with the issue ID; the issue should include
(1) handling OAuth login for members in PENDING status, (2) behavior when an
existing email is used with a different OAuth provider (merge, link, reject),
and (3) policy for adding/changing OAuth links; then update OAuthLoginService
and related methods to reference the created issue ID instead of a bare TODO.
- Around line 25-26: The log in OAuthLoginService is recording user PII (email);
remove any logging of command.email() (and similar user-identifying fields) in
the OAuth login flow and replace with non-PII identifiers or masked/hashed
values; update the logging in the method(s) that use command (the log line shown
and the later log at line 37) to only emit provider and a non-sensitive id (or a
deterministic hash/masked form) and ensure no raw email or other personal data
is written to logs.
- Around line 61-65: The generateNickname method risks collisions by using email
prefix + timestamp%10000; replace its suffix with a UUID-based string and add a
uniqueness check loop: in generateNickname(String email) produce a suffix from
UUID.randomUUID().toString().substring(0,8), build nickname = prefix + "_" +
suffix, and loop while loadMemberPort.existsByNickname(nickname) up to a small
max attempts (e.g., 5); if attempts exhausted, throw
BusinessException(Domain.MEMBER, MemberErrorCode.NICKNAME_GENERATION_FAILED).
Ensure variable names are meaningful (prefix, suffix, nickname, attempts) and
keep this logic inside generateNickname.
- Around line 36-59: registerOAuthMemberWithPendingState is missing
email-duplication validation which allows multiple members with the same email;
inject and use LoadMemberPort to check for existing member by email before
creating a new Member, throw the same duplicate-email exception used by
MemberCommandService.register() (or reuse its validation logic) if a member
exists, and only proceed to call saveMemberPort.save(...) and
saveMemberPort.saveOAuth(...) when the email is unique to preserve data
integrity.
- Around line 24-34: In processOAuthLogin, don't return memberId directly from
loadMemberOAuthPort.findByProviderAndProviderUserId; instead load the Member
(via loadMemberPort or resolving Member from MemberOAuth), call
Member.validateActive() to block WITHDRAWN/INACTIVE users before returning the
id; in registerOAuthMemberWithPendingState call
loadMemberPort.existsByEmail(command.email()) before creating a Member and if
true throw new BusinessException(MemberErrorCode.EMAIL_ALREADY_EXISTS) so
email-duplication is handled explicitly (mirror the behavior in
MemberCommandService).
In @src/main/java/com/umc/product/member/domain/enums/TermType.java:
- Around line 3-7: The enum is named TermType but the project convention
requires {Domain}{Type}; rename the enum TermType to MemberTermType and move it
into the same package (member.domain.enums) by renaming the file to
MemberTermType.java, update all usages/imports/references to MemberTermType
across the codebase, and keep the enum constants (SERVICE, PRIVACY, MARKETING)
unchanged to preserve behavior.
In
@src/main/java/com/umc/product/member/domain/exception/MemberDomainException.java:
- Around line 11-12: The file MemberDomainException.java ends with an
unnecessary blank line; remove the trailing empty line at the end of the file so
the file ends immediately after the closing brace of the MemberDomainException
class (ensure the final token is the class's closing brace `}` with no extra
newline lines after it).
In @src/main/java/com/umc/product/member/domain/exception/MemberErrorCode.java:
- Around line 16-17: Update the enum entry for MEMBER_NOT_ACTIVE in
MemberErrorCode so its message clearly states the user is not active (distinct
from INVALID_MEMBER_STATUS); locate the MEMBER_NOT_ACTIVE constant in the
MemberErrorCode enum and replace its duplicate Korean message "올바르지 않은 사용자
상태입니다." with a clearer text like "사용자가 활성 상태가 아닙니다." (or equivalent) while
keeping the same HttpStatus and error code.
In @src/main/java/com/umc/product/temp/controller/TempController.java:
- Around line 17-21: The TempController class is missing the API versioned path
prefix; add a class-level @RequestMapping("/api/v1/temp") to TempController (the
class annotated with @RequiredArgsConstructor, @Profile("local | dev"),
@RestController("temp"), @Tag(name = Constants.TEST)) so the controller uses the
standard /api/v1/{domain} route; replace or supplement the current
@RestController("temp") value with the request mapping annotation to ensure
endpoints are served under /api/v1/temp.
In @src/main/resources/logback-spring.xml:
- Line 4: The log pattern truncates trace IDs via "%-4.4X{traceId}" which breaks
distributed tracing; update the pattern string used in logback-spring.xml (the
pattern line containing "%-4.4X{traceId}") to emit the full trace ID (e.g. use
"%X{traceId}" so the complete traceId is logged) and apply the same change to
the other affected pattern at the referenced location (line 20 equivalent) so
correlation across services is preserved; optionally, if you need a placeholder
when traceId is missing, adjust the pattern to include a default or conditional
marker in the same pattern expression.
...ain/java/com/umc/product/challenger/application/port/in/command/ManageChallengerUseCase.java
Show resolved
Hide resolved
src/main/java/com/umc/product/challenger/application/port/in/query/ChallengerPublicInfo.java
Show resolved
Hide resolved
src/main/java/com/umc/product/challenger/domain/Challenger.java
Outdated
Show resolved
Hide resolved
| public enum OrganizationType { | ||
| CENTRAL, | ||
| CHAPTER, | ||
| SCHOOL | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
UI 표시를 위한 description 필드 추가를 고려해보세요.
현재 Enum은 상수만 정의되어 있습니다. 만약 이 값들을 사용자에게 표시해야 한다면, 한글 설명이나 display name을 추가하는 것이 좋습니다.
🔎 description 필드 추가 예시
public enum OrganizationType {
- CENTRAL,
- CHAPTER,
- SCHOOL
+ CENTRAL("중앙"),
+ CHAPTER("챕터"),
+ SCHOOL("학교");
+
+ private final String description;
+
+ OrganizationType(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
}📝 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.
| public enum OrganizationType { | |
| CENTRAL, | |
| CHAPTER, | |
| SCHOOL | |
| } | |
| public enum OrganizationType { | |
| CENTRAL("중앙"), | |
| CHAPTER("챕터"), | |
| SCHOOL("학교"); | |
| private final String description; | |
| OrganizationType(String description) { | |
| this.description = description; | |
| } | |
| public String getDescription() { | |
| return description; | |
| } | |
| } |
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/enums/OrganizationType.java
around lines 3-7, OrganizationType enum currently only defines constants
(CENTRAL, CHAPTER, SCHOOL); add a description/displayName field plus a
constructor and a getter to store a user-facing string (e.g., Korean labels) and
update each constant to pass its description; adjust any code that prints the
enum to use the new getter or override toString in OrganizationType to return
the description so UI rendering shows the display name instead of the enum
identifier.
🧹 Nitpick | 🔵 Trivial
Enum 네이밍 컨벤션을 따르도록 개선을 권장합니다.
코딩 가이드라인에 따르면 Enum 클래스는 {Domain}{Type} 패턴을 따라야 합니다 (예: ChallengerStatus). 현재 OrganizationType은 ChallengerRole에서만 사용되는 Challenger 도메인 전용 Enum입니다.
더 명확한 도메인 소속을 나타내기 위해 ChallengerOrganizationType으로 변경하는 것을 고려해주세요.
Based on coding guidelines, Enum 네이밍 컨벤션 적용
🔎 네이밍 개선 제안
-public enum OrganizationType {
+public enum ChallengerOrganizationType {
CENTRAL,
CHAPTER,
SCHOOL
}그리고 ChallengerRole.java에서 해당 타입 참조도 함께 변경:
@Enumerated(EnumType.STRING)
@Column(name = "organization_type")
private ChallengerOrganizationType organizationType;📝 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.
| public enum OrganizationType { | |
| CENTRAL, | |
| CHAPTER, | |
| SCHOOL | |
| } | |
| public enum ChallengerOrganizationType { | |
| CENTRAL, | |
| CHAPTER, | |
| SCHOOL | |
| } |
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/enums/OrganizationType.java
around lines 3-7, Rename the enum OrganizationType to ChallengerOrganizationType
to follow the {Domain}{Type} naming convention and clarify domain ownership;
update all usages (e.g., the organizationType field in ChallengerRole) to
reference ChallengerOrganizationType and recompile; ensure the
@Enumerated(EnumType.STRING) mapping and any import statements, switch/case
usages, tests, and database column references (e.g., organization_type) are
updated accordingly to avoid compile/runtime errors.
src/main/java/com/umc/product/member/application/service/command/OAuthLoginService.java
Show resolved
Hide resolved
src/main/java/com/umc/product/member/application/service/command/OAuthLoginService.java
Show resolved
Hide resolved
src/main/java/com/umc/product/member/application/service/command/OAuthLoginService.java
Show resolved
Hide resolved
src/main/java/com/umc/product/member/domain/exception/MemberDomainException.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
이 PR은 Member와 Challenger 도메인의 기초 엔티티 설계 및 OAuth 인증 기능을 구현합니다. 헥사고날 아키텍처를 따르며, Domain-Application-Adapter 계층 구조를 준수하고 있습니다.
주요 변경 사항:
- Member 도메인: Entity, UseCase, Port, Service, Adapter 전체 레이어 구현
- Challenger 도메인: Entity 및 기본 구조 설계
- OAuth 2.0 로그인: Google, Kakao, Naver 지원 구현
Reviewed changes
Copilot reviewed 60 out of 69 changed files in this pull request and generated 27 comments.
Show a summary per file
| File | Description |
|---|---|
Member.java |
Member 도메인 엔티티 및 상태 관리 로직 (PENDING→ACTIVE) |
MemberOAuth.java |
OAuth 연동 정보 저장 엔티티 |
MemberTermAgreement.java |
약관 동의 정보 엔티티 |
Challenger.java |
Challenger 도메인 엔티티 (Part, Gisu, Status 포함) |
ChallengerRole.java |
챌린저 역할 정보 (조직별 역할 관리) |
ChallengerPoint.java |
챌린저 상벌점 엔티티 |
MemberQueryService.java |
Member 조회 UseCase 구현 (읽기 전용) |
MemberCommandService.java |
Member 등록 및 수정 UseCase 구현 |
OAuthLoginService.java |
OAuth 로그인 처리 서비스 (신규/기존 회원 처리) |
MemberPersistenceAdapter.java |
Member Port 구현 (JPA Repository 연결) |
MemberController.java |
Member REST API 엔드포인트 |
CustomOAuth2UserService.java |
OAuth2 사용자 정보 처리 어댑터 |
OAuth2AuthenticationSuccessHandler.java |
OAuth 인증 성공 시 JWT 토큰 발급 및 리다이렉트 |
OAuth2AuthenticationFailureHandler.java |
OAuth 인증 실패 시 에러 처리 |
MemberPrincipal.java |
OAuth2User 및 JWT 인증 Principal 통합 구현 |
SecurityConfig.java |
OAuth2 로그인 설정 추가 |
SwaggerTag.java |
API 문서화를 위한 태그 Enum 추가 |
erd.dbml |
전체 데이터베이스 스키마 정의 |
dockerfile |
Spring Boot 애플리케이션 컨테이너화 설정 |
...n/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.java
Show resolved
Hide resolved
src/main/java/com/umc/product/member/application/service/command/OAuthLoginService.java
Show resolved
Hide resolved
| throw new BusinessException(Domain.MEMBER, MemberErrorCode.EMAIL_ALREADY_EXISTS); | ||
| } | ||
|
|
||
| // 4. Member 생성 및 저장 |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: 주석에서 "User"라는 용어가 사용되고 있지만, 도메인 용어는 "Member"입니다. 일관성을 위해 주석을 "Member 생성 및 저장"로 변경해야 합니다.
src/main/java/com/umc/product/challenger/domain/ChallengerRole.java
Outdated
Show resolved
Hide resolved
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String code; | ||
| private final String message; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method overrides BaseCode.getHttpStatus; it is advisable to add an Override annotation.
| private final String message; | |
| private final String message; | |
| @Override | |
| public HttpStatus getHttpStatus() { | |
| return httpStatus; | |
| } |
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String code; | ||
| private final String message; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method overrides BaseCode.getMessage; it is advisable to add an Override annotation.
| ; | ||
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String code; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method overrides BaseCode.getCode; it is advisable to add an Override annotation.
| MEMBER_ALREADY_REGISTERED(HttpStatus.BAD_REQUEST, "MEMBER-0007", "이미 회원가입을 완료한 사용자입니다."), | ||
| ; | ||
|
|
||
| private final HttpStatus httpStatus; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method overrides BaseCode.getHttpStatus; it is advisable to add an Override annotation.
docker/app/dockerfile
Outdated
| # JVM options (single line to avoid parsing issues) | ||
| ENV JAVA_OPTS="-XX:+UseContainerSupport \ | ||
| -XX:MaxRAMPercentage=75.0 \ | ||
| -XX:+UseG1GC \ | ||
| -XX:+ExitOnOutOfMemoryError \ | ||
| -XX:+HeapDumpOnOutOfMemoryError \ | ||
| -XX:HeapDumpPath=/tmp/heapdump.hprof \ | ||
| -Duser.timezone=Asia/Seoul \ | ||
| -Djava.security.egd=file:/dev/./urandom" | ||
|
|
||
| # Use exec form with sh -c so JAVA_OPTS is expanded and signals forwarded | ||
| ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] | ||
|
|
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ENTRYPOINT uses sh -c with exec java $JAVA_OPTS -jar app.jar, which allows command injection if any external or untrusted configuration can influence JAVA_OPTS, because shell metacharacters like command substitution ($(), backticks) will be evaluated before java is executed. This can give an attacker arbitrary command execution inside the container at startup. To mitigate this, use an exec-form entrypoint that invokes java directly and passes JAVA_OPTS as separate arguments without going through a shell, or otherwise ensure JAVA_OPTS cannot be set from untrusted input.
| # JVM options (single line to avoid parsing issues) | |
| ENV JAVA_OPTS="-XX:+UseContainerSupport \ | |
| -XX:MaxRAMPercentage=75.0 \ | |
| -XX:+UseG1GC \ | |
| -XX:+ExitOnOutOfMemoryError \ | |
| -XX:+HeapDumpOnOutOfMemoryError \ | |
| -XX:HeapDumpPath=/tmp/heapdump.hprof \ | |
| -Duser.timezone=Asia/Seoul \ | |
| -Djava.security.egd=file:/dev/./urandom" | |
| # Use exec form with sh -c so JAVA_OPTS is expanded and signals forwarded | |
| ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] | |
| # JVM options are passed directly via exec-form ENTRYPOINT to avoid shell parsing issues | |
| ENTRYPOINT ["java", \ | |
| "-XX:+UseContainerSupport", \ | |
| "-XX:MaxRAMPercentage=75.0", \ | |
| "-XX:+UseG1GC", \ | |
| "-XX:+ExitOnOutOfMemoryError", \ | |
| "-XX:+HeapDumpOnOutOfMemoryError", \ | |
| "-XX:HeapDumpPath=/tmp/heapdump.hprof", \ | |
| "-Duser.timezone=Asia/Seoul", \ | |
| "-Djava.security.egd=file:/dev/./urandom", \ | |
| "-jar", \ | |
| "app.jar"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 64
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/main/java/com/umc/product/global/exception/GlobalExceptionHandler.java (2)
101-130: handleHttpMessageNotReadable에서 프로덕션 환경 정보 노출 및 NPE 위험.두 가지 문제가 있습니다:
정보 노출: 128번 줄의
e.getMostSpecificCause().getMessage()는 프로덕션 환경에서 상세한 스택 트레이스 정보를 노출합니다. 반면handleUnhandledException(140-142줄)은 프로덕션 환경을 확인하고 메시지를 숨깁니다. 일관성이 부족합니다.NPE 위험:
getMostSpecificCause()가 null을 반환할 수 있고,getMessage()도 null을 반환할 수 있습니다. 이는NullPointerException을 발생시킬 수 있습니다.🔧 제안하는 수정
protected ResponseEntity<Object> handleHttpMessageNotReadable( HttpMessageNotReadableException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) { log.error("JSON 파싱 에러: {}", e.getMessage()); String errorMessage = e.getMessage(); String simplifiedMessage = "잘못된 요청 형식입니다"; if (errorMessage != null) { if (errorMessage.contains("Cannot deserialize")) { simplifiedMessage = "요청 데이터 타입이 올바르지 않습니다"; } else if (errorMessage.contains("Required request body is missing")) { simplifiedMessage = "요청 본문이 필요합니다"; } else if (errorMessage.contains("JSON parse error")) { simplifiedMessage = "JSON 형식이 올바르지 않습니다"; } } + String detailMessage = isProductionProfile() + ? simplifiedMessage + : simplifiedMessage + " - " + Optional.ofNullable(e.getMostSpecificCause()) + .flatMap(cause -> Optional.ofNullable(cause.getMessage())) + .orElse("상세 정보 없음"); return handleExceptionInternalFalse( e, CommonErrorCode.BAD_REQUEST, headers, HttpStatus.BAD_REQUEST, request, - simplifiedMessage + " - " + e.getMostSpecificCause().getMessage() + detailMessage ); }
44-48: SecurityException 핸들러에서 null 메시지 처리.45번 줄의
e.getMessage()가 null을 반환할 수 있습니다. 로깅과 응답 모두에서 null 체크가 필요합니다.🔧 제안하는 수정
@ExceptionHandler({AuthorizationDeniedException.class, AccessDeniedException.class}) public ResponseEntity<Object> handleAccessDenied(Exception e, WebRequest request) { - log.warn("[ACCESS DENIED] {}", e.getMessage()); + log.warn("[ACCESS DENIED] {}", Optional.ofNullable(e.getMessage()).orElse("접근 거부")); - return buildErrorResponse(e, CommonErrorCode.FORBIDDEN, request, e.getMessage()); + return buildErrorResponse(e, CommonErrorCode.FORBIDDEN, request, + Optional.ofNullable(e.getMessage()).orElse("권한이 없습니다")); }src/main/java/com/umc/product/global/security/CustomAuthorizationManager.java (2)
51-53: 모든 예외를 무시하는 것은 보안 위험입니다.
handlerMapping.getHandler(request)호출 시 발생한 모든 예외를 조용히 무시하고 있습니다. 핸들러를 찾지 못한 404 케이스도 있겠지만, 예상치 못한 런타임 예외나 보안 관련 예외까지 무시될 수 있습니다. 최소한 로깅을 추가하거나 특정 예외만 처리해야 합니다.🔍 제안된 개선
} catch (Exception e) { - // 핸들러를 찾지 못한 경우 + // 핸들러 매핑 실패 시 기본적으로 인증 필수로 처리 + logger.debug("Failed to resolve handler for request", e); }
56-60: 하드코딩된 "anonymousUser" 문자열은 유지보수성이 떨어집니다.Spring Security의 익명 사용자를 식별하기 위해 문자열 비교를 사용하는 것은 취약합니다.
AnonymousAuthenticationToken클래스를 사용하거나 Spring Security의 표준 방식을 따르는 것이 좋습니다.✏️ 제안된 개선
- boolean isAuthenticated = auth != null && auth.isAuthenticated() - && !"anonymousUser".equals(auth.getPrincipal()); + boolean isAuthenticated = auth != null && auth.isAuthenticated() + && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken);src/main/java/com/umc/product/global/config/P6SpyConfig.java (1)
19-47: P6SpyConfig의 민감 정보 노출 위험성 해결 필요P6Spy가 모든 프로필에서 무조건 활성화되어 있어, SQL 쿼리에 포함된 비밀번호, 토큰, 사용자 정보 등 민감 정보가 평문으로 로깅됩니다.
P6SpyConfig는 @Profile 또는 @ConditionalOnProperty 없이 @configuration으로 정의되어 있어, 운영 환경에서도 자동으로 활성화됩니다. 또한 application-prod.yml 같은 프로필별 설정 파일이 존재하지 않아, 설정 레벨에서의 제어도 불가능합니다.
해결 방안:
- P6SpyConfig에
@Profile({"!prod", "!production"})추가 또는@ConditionalOnProperty적용- 운영 환경에서는 P6Spy 비활성화
- 개발 환경에서 사용할 경우, 민감 정보 마스킹 로직 추가 검토
src/main/java/com/umc/product/temp/controller/TempController.java (1)
25-30: 테스트 토큰 발급 엔드포인트의 입력 검증 누락개발/테스트 환경 전용이지만, userId에 대한 검증이 전혀 없습니다. 음수 또는 존재하지 않는 ID로 토큰이 발급될 수 있어 테스트 시나리오에서 혼란을 줄 수 있습니다.
♻️ 개선 제안
@Operation(summary = "테스트용 토큰 발급", description = "userId로 JWT 토큰을 발급합니다.") @Public @GetMapping("/token/at/{userId}") public ApiResponse<String> getTestToken(@PathVariable Long userId) { + if (userId <= 0) { + return ApiResponse.onFailure("INVALID_USER_ID", "유효하지 않은 사용자 ID입니다.", null); + } return ApiResponse.onSuccess(jwtTokenProvider.createAccessToken(userId, null)); }src/main/java/com/umc/product/global/security/resolver/CurrentUserArgumentResolver.java (1)
15-15: 클래스명 일관성을 고려해주세요.클래스명이
CurrentUserArgumentResolver로 유지되고 있으나, 내부 구현은CurrentMember와MemberPrincipal로 변경되었습니다. 클래스명을CurrentMemberArgumentResolver로 변경하여 일관성을 높이는 것을 권장합니다.♻️ 리팩토링 제안
-public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { +public class CurrentMemberArgumentResolver implements HandlerMethodArgumentResolver {참고: 이 변경 시 Spring 설정 파일(WebMvcConfigurer 구현 등)에서 빈 등록 부분도 함께 수정이 필요합니다.
🤖 Fix all issues with AI agents
In @docs/erd.dbml:
- Around line 41-42: The column name provider_member_id is ambiguous; rename it
to match the domain and the JPA entity by using a clearer identifier (e.g.,
provider_user_id or oauth_user_id) and ensure it exactly matches the field name
used in the MemberOAuth JPA entity; update the DBML definition for the provider
column (currently "provider_member_id") to the chosen name and, if present,
update any references to OAuthProvider and MemberOAuth mappings to keep schema
and entity names consistent.
- Around line 1-660: Update the DBML to match the JPA entities: add NAVER to the
OAuthProvider enum in DBML to match OAuthProvider.java; make
challenger_role.leadingPart nullable or remove the [null] in DBML if the JPA
field ChallengerRole.leadingPart is annotated @Column(nullable = false) (or
alternatively adjust the entity to allow null) so both definitions agree; and
rename the member_oauth column provider_member_id in the DBML to provider_id to
match MemberOAuth.java (or update the entity annotation to use
provider_member_id) so the DBML column name and @Column(name = ...) are
consistent.
In
@src/main/java/com/umc/product/challenger/application/port/in/command/ManageChallengerUseCase.java:
- Line 9: Remove the implementation/domain-detail comment "// 역할이라고 함은, 운영진 중
일부를 의미" from the ManageChallengerUseCase interface; interfaces should only
define the contract. If the clarification is needed, add it to the
ChallengerRole entity class Javadoc or to the implementing Service's
documentation (e.g., the class that implements ManageChallengerUseCase) or
project docs/wiki instead.
- Around line 3-20: The ManageChallengerUseCase interface bundles three distinct
use cases; split it into three single-responsibility interfaces named
GivePointsToChallengerUseCase, AssignRoleToChallengerUseCase, and
RevokeRoleFromChallengerUseCase, each exposing only the corresponding method
(e.g., givePointsToChallenger ->
GivePointsToChallengerUseCase.givePointsToChallenger, assignRoleToChallenger ->
AssignRoleToChallengerUseCase.assignRoleToChallenger, revokeRoleFromChallenger
-> RevokeRoleFromChallengerUseCase.revokeRoleFromChallenger); create separate
files for these interfaces, update any implementing classes to implement the
correct new interface(s), and adjust dependency injection or wiring to reference
the new interface types.
In
@src/main/java/com/umc/product/challenger/application/port/in/query/ChallengerPublicInfo.java:
- Around line 7-13: Add javax/validation NotNull constraints to the
ChallengerPublicInfo record components so none of the query fields can be null:
annotate challengerId, memberId, gisuId and part with @NotNull on the record
components in the ChallengerPublicInfo declaration (ensure you import the
correct NotNull, e.g., jakarta.validation.constraints.NotNull or
javax.validation.constraints.NotNull consistent with the project).
In
@src/main/java/com/umc/product/challenger/application/port/in/query/GetChallengerUseCase.java:
- Line 4: The TODO comment about distinguishing public/private challenger
visibility should be removed from the GetChallengerUseCase interface and either
recorded as a tracked issue in the project tracker or addressed by adding
explicit API/UseCase support: create a new method (e.g., getPublicChallengers /
getPrivateChallengers or add a visibility parameter to getChallenger) or design
permission-based filtering in the service layer; update GetChallengerUseCase to
reflect the chosen approach and reference the new issue/PR ID in code comments
instead of leaving a TODO.
- Line 9: The method getChallengerPublicInfo in the GetChallengerUseCase
interface is missing validation for its challengerId parameter; add
jakarta.validation.constraints.NotNull to the imports and annotate the method
parameter with @NotNull on getChallengerPublicInfo(Long challengerId) to prevent
nulls and enable bean validation.
In @src/main/java/com/umc/product/challenger/domain/Challenger.java:
- Around line 35-42: The Challenger entity's enum fields are missing
@Enumerated(EnumType.STRING), causing JPA to persist ordinals; update the
Challenger class to annotate the part and status fields with
@Enumerated(EnumType.STRING) (i.e., add @Enumerated(EnumType.STRING) above the
private ChallengerPart part and above the private ChallengerStatus status) so
enums are stored as names rather than ordinals.
In @src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java:
- Around line 32-33: The ChallengerPoint entity's enum field 'type' (PointType)
is missing the @Enumerated(EnumType.STRING) annotation, which causes JPA to
persist the ordinal by default; add @Enumerated(EnumType.STRING) to the 'type'
field declaration in the ChallengerPoint class so the enum is stored as its
name/string to prevent data inconsistency if enum order changes.
- Around line 28-30: The ChallengerPoint entity's @ManyToOne mapping for the
challenger field currently relies on the default EAGER fetch; change it to use
LAZY loading by updating the @ManyToOne annotation on the challenger field in
ChallengerPoint to explicitly set fetch = FetchType.LAZY so the challenger
association is loaded lazily and helps avoid N+1 issues.
- Around line 19-40: ChallengerPoint lacks a way to construct instances; add a
constructor or builder/factory so callers can create entities—either annotate
the class with Lombok's @Builder and add an all-args (or required-args)
constructor, or implement a public static factory method (e.g.,
ChallengerPoint.of(Challenger challenger, PointType type, String description,
Long relatedWorkbookId)) and a matching constructor to set fields (id remains
managed by JPA); ensure the constructor/factory assigns challenger, type,
description, and relatedWorkbookId and keeps the no-args protected constructor.
In @src/main/java/com/umc/product/challenger/domain/ChallengerRole.java:
- Around line 40-43: The @Column on ChallengerRole.leadingPart contradicts the
comment: change the column to allow nulls so non-leaders can have no leading
part; update the annotation on the field ChallengerRole.leadingPart (type
ChallengerPart) to set nullable = true or remove the nullable attribute entirely
so the database column permits null values consistent with the comment.
- Around line 27-29: The @ManyToOne on the ChallengerRole.challenger field is
missing explicit lazy loading; change the annotation to specify fetch =
FetchType.LAZY on the @ManyToOne of the ChallengerRole class and ensure
java.persistence.FetchType is imported; this forces lazy loading of the
challenger association to avoid eager fetch/N+1 issues when loading
ChallengerRole entities.
- Around line 31-32: The ChallengerRole enum field roleType is missing the
@Enumerated(EnumType.STRING) annotation; update the ChallengerRole class to
annotate the private RoleType roleType field with @Enumerated(EnumType.STRING)
so the enum is persisted as its name (string) rather than ordinal, preventing
data integrity issues when enum order changes.
In @src/main/java/com/umc/product/challenger/domain/enums/ChallengerPart.java:
- Around line 3-11: Rename the multi-word enum constants in ChallengerPart from
SPRINGBOOT to SPRING_BOOT and NODEJS to NODE_JS to follow enum naming
conventions, and add short Korean comments for each enum constant similar to
ChallengerStatus to improve readability; update any usages of
ChallengerPart.SPRINGBOOT and ChallengerPart.NODEJS across the codebase to the
new names (ChallengerPart.SPRING_BOOT, ChallengerPart.NODE_JS) to avoid
breakage.
In @src/main/java/com/umc/product/challenger/domain/enums/ChallengerStatus.java:
- Line 7: ChallengerStatus enum has a typo in the comment for the WITHDRAWN
constant ("자진탈부"); update that inline comment to the correct Korean word "자진탈퇴"
so the enum constant WITHDRAWN's comment reads "자진탈퇴".
In @src/main/java/com/umc/product/challenger/domain/enums/PointType.java:
- Around line 3-7: Rename the enum PointType to follow the `{Domain}{Type}`
pattern by renaming the type to ChallengerPointType (and the file to
ChallengerPointType.java), then update all usages/imports and type declarations
(e.g., references in ChallengerPoint entity, repositories, DTOs, mappers, and
tests) from PointType to ChallengerPointType so compilation and imports remain
consistent.
In
@src/main/java/com/umc/product/challenger/domain/exception/ChallengerErrorCode.java:
- Around line 8-22: ChallengerErrorCode relies on Lombok-generated getters but
does not explicitly implement BaseCode methods with @Override; add explicit
implementations of the interface methods (e.g., getHttpStatus(), getCode(),
getMessage()) inside the ChallengerErrorCode enum and annotate each with
@Override so the enum clearly implements BaseCode's contract (keep the existing
fields httpStatus, code, message and return them from these methods).
In @src/main/java/com/umc/product/global/config/OpenApiConfig.java:
- Around line 24-25: In OpenApiConfig, make the token name fields true constants
by changing the instance fields accessToken and refreshToken to static final
constants (e.g., private static final String ACCESS_TOKEN and private static
final String REFRESH_TOKEN) so they are single shared immutable values across
instances; update any usages of accessToken/refreshToken to reference the new
static names.
In @src/main/java/com/umc/product/global/config/SecurityConfig.java:
- Around line 101-102: The OAuth2 request matcher is too broad: in
SecurityConfig replace the generic .requestMatchers("/login/**",
"/oauth2/**").permitAll() with more specific OAuth2 endpoints so only the
intended authorization and callback paths are public; update the call that uses
requestMatchers to .requestMatchers("/oauth2/authorization/**",
"/login/oauth2/code/**").permitAll() (locate this in the SecurityConfig
configuration where requestMatchers is invoked).
In @src/main/java/com/umc/product/global/security/JwtAuthenticationFilter.java:
- Line 34: MemberPrincipal is being constructed with an empty email which breaks
consumers; change the auth flow to supply a real email: add an "email" claim
when creating tokens in JwtTokenProvider.createToken (and update
createAccessToken/createRefreshToken signatures if needed), implement
JwtTokenProvider.getEmail(token) to extract that claim, and in
JwtAuthenticationFilter replace MemberPrincipal memberPrincipal = new
MemberPrincipal(userId, ""); with MemberPrincipal memberPrincipal = new
MemberPrincipal(userId, jwtTokenProvider.getEmail(token)); alternatively (if you
prefer DB lookup) fetch Member by userId and pass its email into
MemberPrincipal, or make MemberPrincipal.email nullable and document it—choose
one approach and update the referenced methods/classes accordingly.
In @src/main/java/com/umc/product/global/security/MemberPrincipal.java:
- Around line 43-49: getName() can return null when attributes is non-empty but
lacks nameAttributeKey; change it to defensively check
attributes.containsKey(nameAttributeKey) and that
attributes.get(nameAttributeKey) is non-null, and if either check fails return
String.valueOf(memberId) (or an empty/fallback string), otherwise return
String.valueOf(attributes.get(nameAttributeKey)); update the getName() method to
perform these null/contains checks so callers never receive null.
- Around line 18-30: Add null validation for required fields in the
MemberPrincipal constructors: ensure memberId and email are non-null (e.g., via
Objects.requireNonNull or explicit checks that throw
IllegalArgumentException/NullPointerException) so getName() and other methods
cannot NPE; apply the checks in the primary constructor (the one with signature
MemberPrincipal(Long memberId, String email, Map<String, Object> attributes,
String nameAttributeKey)) so the JWT constructor MemberPrincipal(Long memberId,
String email) inherits the validation, and consider also validating attributes
and nameAttributeKey if they must be non-null (or default them safely).
- Around line 38-41: MemberPrincipal.getAuthorities() currently returns
Collections.emptyList(), causing roles to be omitted from JWTs; update
getAuthorities() to return the member's roles mapped to GrantedAuthority (e.g.,
map a stored roles/roleStrings field to new SimpleGrantedAuthority(role)) and
ensure MemberPrincipal has that roles field populated (constructor/build
method). Then update OAuth2AuthenticationSuccessHandler (the code that builds
the JWT) to pull authorities from the authenticated principal via
((MemberPrincipal)authentication.getPrincipal()).getAuthorities() and include
them in the JWT claims so role information is embedded in the token.
In @src/main/java/com/umc/product/global/security/oauth/OAuth2Attributes.java:
- Around line 60-73: In ofNaver, guard against
attributes.get(userNameAttributeName) returning null: check the retrieved
response map before casting and either throw a clear IllegalArgumentException
(or custom runtime exception) mentioning userNameAttributeName and attributes
when null, or substitute an empty Map and provide safe defaults for
providerId/name/email/nickname; update the cast to a safe unchecked cast only
after the null check so nameAttributeKey, attributes, and builder field accesses
cannot cause an NPE.
- Around line 44-58: The ofKakao method lacks null/type checks for nested maps
and required fields which can cause NPEs or ClassCastException; update ofKakao
to safely extract "kakao_account" and "profile" by verifying
attributes.get("kakao_account") and kakaoAccount.get("profile") are non-null and
instances of Map before casting, provide sensible defaults or throw a clear
OAuthProcessingException when required fields (providerId, email or nickname)
are missing, and use those validated values when building via
OAuth2Attributes.builder() (ensure providerId, name, email, nickname are
validated/normalized and attributes/nameAttributeKey remain set).
- Around line 31-42: ofGoogle currently extracts values from the attributes map
without null/type checks which can cause NPEs or ClassCastException and miss
missing required fields; update ofGoogle to validate and safely extract each
field (providerId, name, email, nickname) by checking
attributes.containsKey(...) and verifying instance types before casting, provide
sensible defaults or throw a clear IllegalArgumentException when required fields
like email are missing, and wrap any unsafe casts in safe helpers; apply the
same pattern to ofKakao and ofNaver and ensure the builder(...) call only
receives validated values and the attributes map is preserved.
In
@src/main/java/com/umc/product/global/security/oauth/OAuth2AuthenticationFailureHandler.java:
- Around line 29-45: In OAuth2AuthenticationFailureHandler (e.g., in
onAuthenticationFailure where the current log.error(...) calls occur), reduce
production log verbosity by logging only a concise message at WARN/ERROR and
moving detailed outputs (request URI, exception class/message, OAuth2Error
fields, cause, full stack trace) to DEBUG level and guard them with a
conditional (e.g., if (log.isDebugEnabled()) or similar); keep a short
warn/error like "OAuth2 authentication failed for request: {}" with minimal
context and use log.debug(...) for the rest referencing the same exception
variable and the OAuth2AuthenticationException/oauthEx checks.
- Around line 48-52: The current construction of targetUrl in
OAuth2AuthenticationFailureHandler uses exception.getMessage() directly in the
query string, which can leak internal details; replace that with a generalized
error code or safe message (e.g., "oauth_failed" or a mapped code) and log the
full exception server-side instead; update the UriComponentsBuilder call that
builds targetUrl to use the safe message instead of exception.getMessage(), and
ensure the original exception is recorded via logger.error/trace inside the
class for debugging.
In
@src/main/java/com/umc/product/global/security/oauth/OAuth2AuthenticationSuccessHandler.java:
- Line 47: Summary: Logging the full redirect URL in
OAuth2AuthenticationSuccessHandler (log.info("Redirecting to: {}", targetUrl))
exposes accessToken/refreshToken. Update the logging to avoid printing sensitive
query params by sanitizing or masking tokens before logging: locate
OAuth2AuthenticationSuccessHandler and the place where targetUrl is
constructed/returned, parse the URL (e.g., via java.net.URI) and either remove
the query string or redact access_token/refresh_token parameters, then log only
the sanitized URL or non-sensitive components (host/path) instead of the raw
targetUrl; ensure the actual redirect still uses the original targetUrl but logs
never contain plaintext tokens.
- Around line 32-33: The code casts authentication.getPrincipal() directly to
MemberPrincipal in OAuth2AuthenticationSuccessHandler which can throw
ClassCastException; update the handling by checking with "instanceof
MemberPrincipal" before casting, e.g., if the principal is a MemberPrincipal
then extract memberId from memberPrincipal.getMemberId(), otherwise handle the
unexpected type (log an error with the actual principal class and either throw
an AuthenticationException or fail the authentication flow gracefully) so the
handler never performs an unchecked cast.
- Around line 42-45: 현재 OAuth2AuthenticationSuccessHandler에서 redirectUrl에
accessToken/refreshToken을 쿼리 파라미터로 붙이는 UriComponentsBuilder(targetUrl) 부분은 토큰 노출
취약점이 있으므로 변경하세요: 대신 성공 처리 시 HttpOnly·Secure·SameSite 쿠키로 accessToken과
refreshToken을 설정하도록 OAuth2AuthenticationSuccessHandler의 핸들러 로직을 수정(쿠키 이름, 만료시간,
path 설정 포함)하고 redirectUrl에는 토큰을 포함하지 말 것; 만약 쿠키가 불가능한 클라이언트라면 targetUrl로 리다이렉트하는
대신 자동 제출되는 POST 폼(서버가 HTML 응답으로 반환)으로 토큰을 안전하게 전달하는 대안 구현을 고려하세요.
In
@src/main/java/com/umc/product/global/security/resolver/CurrentUserArgumentResolver.java:
- Around line 19-22: The inline comments in CurrentUserArgumentResolver.java
reference @CurrentUser and UserPrincipal but the code uses CurrentMember and
MemberPrincipal; update the comments to match the implementation by replacing
mentions of "@CurrentUser" with "@CurrentMember" and "UserPrincipal" with
"MemberPrincipal" (or otherwise reword the comment lines around hasAnnotation
and hasUserType to accurately describe
parameter.hasParameterAnnotation(CurrentMember.class) and
MemberPrincipal.class.isAssignableFrom(...)).
- Line 39: Update the misleading comment in CurrentUserArgumentResolver: replace
the mention of "UserPrincipal" with the correct return type "MemberPrincipal"
(where the resolver actually returns a MemberPrincipal instance), ensuring any
comment near the resolveArgument/resolveCurrentUser logic accurately describes
that a MemberPrincipal object is returned.
In
@src/main/java/com/umc/product/member/adapter/in/web/dto/request/CompleteRegisterMemberRequest.java:
- Around line 24-25: The request DTO field schoolId is required but missing
validation; add the javax.validation @NotNull annotation to the schoolId field
in CompleteRegisterMemberRequest so it matches the non-null check performed in
CompleteRegisterMemberCommand (keep profileImageId as optional since
Member.updateProfile handles nulls). Ensure the annotation is placed on the Long
schoolId declaration in the CompleteRegisterMemberRequest class.
In @src/main/java/com/umc/product/member/adapter/in/web/MemberController.java:
- Line 23: The REST API base path on the MemberController is using the wrong
naming convention; update the @RequestMapping value on the MemberController
class from "/v1/member" to the canonical format "/api/v1/members" so it follows
the "/api/v1/{domain}" pattern and uses the plural resource name; locate the
@RequestMapping annotation on the MemberController class and replace the string
accordingly, then run relevant integration/route tests to ensure no other code
references the old path.
- Around line 41-46: The controller method completeRegister currently returns a
raw Long; update it to return ApiResponse<Long> by wrapping the result of
manageMemberUseCase.completeRegister(request.toCommand(currentMember.getMemberId()))
in the standard ApiResponse success payload (e.g., ApiResponse.success(userId)
or new ApiResponse<>(true, userId, null) depending on your ApiResponse
constructor); change the method signature to public ApiResponse<Long>
completeRegister(...) and adjust any imports/serialization accordingly so the
API follows the standard success/data/error shape.
- Around line 49-54: The GET endpoint in MemberController#getUser currently
returns any member's info without authorization and returns MemberResponse
directly; update it to enforce that only the requesting user or a user with
admin role can access the requested memberId (e.g., obtain current user id from
SecurityContext/Principal or use @PreAuthorize to allow access when
authentication.getName() equals memberId or hasRole('ADMIN')), call
getMemberUseCase.getById(memberId) only after the check, and wrap the result
using ApiResponse.of(MemberResponse.from(memberInfo)) so the method returns
ApiResponse<MemberResponse> instead of MemberResponse.
In
@src/main/java/com/umc/product/member/adapter/in/web/oauth/CustomOAuth2UserService.java:
- Around line 62-69: Replace the noisy catch block in CustomOAuth2UserService
where you currently log ex, ex.getMessage(), and ex.getCause(); instead log a
single non-sensitive message with the provider (e.g., "OAuth2 login failed for
provider={}") and then throw a custom unchecked exception (e.g.,
OAuth2AuthenticationException or a project-specific wrapper) that wraps the
original exception as its cause and exposes a safe error code/message for upper
handlers; remove logging of exception messages/causes to avoid sensitive data
leakage and prevent duplicate logs at higher layers so the top-level handler can
perform consistent logging/metrics.
- Around line 31-32: The DEBUG logging of the full OAuth attributes in
CustomOAuth2UserService (where you call log.debug("OAuth2User attributes: {}",
oAuth2User.getAttributes())) risks leaking sensitive data; change this to either
remove the full-attributes log or redact sensitive keys (e.g., access_token,
refresh_token, id_token, email, phone, ssn) before logging, and instead log only
safe, non-sensitive markers such as registrationId, user name/id, or a masked
attribute map; implement the redaction in the same method (e.g., loadUser or
where oAuth2User is handled) by filtering the attribute map and logging the
sanitized map or a simple confirmation message.
- Around line 47-49: The debug log in CustomOAuth2UserService currently prints
PII (attributes.getEmail()) which risks exposing user emails; update the log in
the method that calls log.debug(...) to remove the raw email or replace it with
a non-PII form (e.g., omit email entirely or mask it like user@*** or show a
hash/partial only) while keeping provider, providerId and name as needed; ensure
references to attributes.getEmail() are eliminated from debug output and use a
maskedEmail variable if masking is chosen.
In
@src/main/java/com/umc/product/member/adapter/out/persistence/MemberPersistenceAdapter.java:
- Around line 34-38: The method MemberPersistenceAdapter.findByNickname contains
a leftover test comment "QueryDSL 사용 테스트 하고자 넣은 method"—remove this test-only
comment from the production code; if the method is intended for production keep
the method but delete or replace the comment with a concise javadoc explaining
its purpose (e.g., "Find Member by nickname"), otherwise delete the method if it
is strictly test-only.
In
@src/main/java/com/umc/product/member/adapter/out/persistence/MemberQueryRepository.java:
- Around line 15-19: countAllMembers currently calls
queryFactory.selectFrom(QMember.member).stream().count(), which loads all Member
entities into memory; change it to perform a DB-level count by using QueryDSL
count projection instead (e.g., use
queryFactory.select(QMember.member.count()).from(QMember.member).fetchOne() or
the framework's fetchCount equivalent) inside
MemberQueryRepository::countAllMembers so the database returns the numeric count
without materializing entities.
In
@src/main/java/com/umc/product/member/application/port/in/command/ProcessOAuthLoginCommand.java:
- Around line 6-12: Add Jakarta Bean Validation annotations to the
ProcessOAuthLoginCommand record: import jakarta.validation.constraints.NotNull
and annotate the provider, providerId, email, and name components with @NotNull
(leave nickname unannotated if optional) so the record enforces non-null inputs;
update the record declaration in ProcessOAuthLoginCommand accordingly.
- Around line 13-17: ProcessOAuthLoginCommand's compact constructor currently
validates provider, providerId, and email but misses the required name check;
add Objects.requireNonNull(name, "name must not be null") to the compact
constructor of ProcessOAuthLoginCommand (or alternatively implement defaulting
logic where OAuth data is mapped before constructing ProcessOAuthLoginCommand so
that name is never null, e.g., call generateNickname() or another default when
extracting name). Ensure the chosen fix references the ProcessOAuthLoginCommand
compact constructor and any mapping/creation code that constructs that command.
In
@src/main/java/com/umc/product/member/application/port/in/command/RegisterMemberCommand.java:
- Around line 12-27: The RegisterMemberCommand record currently stores the
mutable agreedTerms list as-is, allowing external modification; make a
defensive, unmodifiable copy in the compact constructor by replacing the stored
reference with an immutable copy (e.g., use List.copyOf(agreedTerms) or
Collections.unmodifiableList(new ArrayList<>(agreedTerms))) after the null
checks so the record’s agreedTerms field is immutable and cannot be modified
from outside.
In
@src/main/java/com/umc/product/member/application/port/in/query/MemberInfo.java:
- Around line 11-12: MemberInfo currently declares nullable fields schoolId and
profileImageId without explicit nullability; update the DTO to make nullability
explicit by either annotating the fields with @Nullable (e.g., annotate Long
schoolId and Long profileImageId with @Nullable) or change their types to
Optional<Long> (e.g., Optional<Long> schoolId, Optional<Long> profileImageId)
and adjust constructors/getters accordingly; apply the same change for the
duplicate occurrences (the fields at lines 21-22) so callers and implementors
see nullability at the type level.
In
@src/main/java/com/umc/product/member/application/port/out/LoadMemberOAuthPort.java:
- Around line 7-11: The interface LoadMemberOAuthPort uses findByUserId(Long
userId) which is semantically inconsistent with the domain and adapter
(MemberOAuthRepository.findByMemberId); rename the method to findByMemberId(Long
memberId) and update its parameter name accordingly, then update all
implementing adapters/repositories and call sites to implement/invoke
LoadMemberOAuthPort.findByMemberId while keeping the return type
Optional<MemberOAuth> and preserving method semantics.
In
@src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java:
- Line 56: 주석의 용어가 일관되지 않습니다; MemberCommandService 클래스 내 주석 "// 7. User 상태를
ACTIVE로 변경"을 찾아 "Member 상태를 ACTIVE로 변경"으로 변경해 주세요, 관련 메서드나 로직명(예:
MemberCommandService, activateMember 혹은 해당 상태 변경을 수행하는 메서드 이름)을 건드릴 필요는 없고 주석
텍스트만 일관된 용어인 "Member"로 수정하면 됩니다.
- Around line 56-59: The register() method calls savedMember.activate() but does
not persist that state change explicitly, causing inconsistency with
completeRegister(); after savedMember.activate() call add an explicit save via
saveUserPort.save(savedMember) (matching the pattern used in completeRegister())
so the activation is persisted and the method behavior is consistent.
In
@src/main/java/com/umc/product/member/application/service/command/OAuthLoginService.java:
- Around line 36-46: registerOAuthMemberWithPendingState is missing
email-duplication checks and can violate DB uniqueness; inject or use
LoadMemberPort and before creating a new Member call
loadMemberPort.existsByEmail(command.email()) (or
loadMemberPort.findByEmail(command.email()) to get existing Member) and if an
existing member is found, update/attach the OAuth provider details to that
Member instead of creating a new one, otherwise proceed to create and save as
currently done; ensure you use the same semantics as
MemberCommandService.register() and still call saveMemberPort.save(...) only for
new or updated Member entities.
- Around line 24-34: The code returns memberId immediately when
loadMemberOAuthPort.findByProviderAndProviderUserId(...) finds a MemberOAuth,
but it omits member state validation; instead, after mapping to MemberOAuth use
LoadMemberPort to load the full Member (via its id), call
member.validateActive() (or equivalent state check) and only then return the
memberId, throwing or handling inactive/withdrawn cases; update
processOAuthLogin to inject/use LoadMemberPort, replace the direct
MemberOAuth::getMemberId mapping with a flow that loads the Member, invokes
validateActive(), and returns the id, falling back to
registerOAuthMemberWithPendingState(...) for new users.
In @src/main/java/com/umc/product/member/domain/exception/MemberErrorCode.java:
- Around line 16-17: The two enum entries INVALID_MEMBER_STATUS and
MEMBER_NOT_ACTIVE in MemberErrorCode currently share the same message; update
MEMBER_NOT_ACTIVE’s message to a distinct, clearer text (e.g., "활성화되지 않은
사용자입니다.") so the error codes represent different situations; locate the
MemberErrorCode enum and change the message string for the MEMBER_NOT_ACTIVE
constant while keeping its HttpStatus and code unchanged.
In @src/main/java/com/umc/product/member/domain/Member.java:
- Line 96: Add domain methods in Member: implement deactivate() and dormant() to
change the member state field (e.g., status or enum) and update relevant
timestamps; validate current state inside deactivate() to prevent deactivating
an already deactivated member and inside dormant() to prevent marking an already
dormant or deactivated member (throw IllegalStateException or a domain-specific
exception), and ensure any related invariants (e.g., clear session/token fields
or set isActive=false) are enforced; also add unit tests for Member.deactivate()
and Member.dormant() to cover normal and invalid transitions, or if you prefer
not to implement now, open a new issue referencing Member, deactivate(), and
dormant() with the required validation and test tasks.
- Around line 49-57: The Member builder currently allows null/blank/invalid
inputs; add validation inside the Member construction path (e.g., in the private
Member(...) constructor invoked by the Lombok @Builder or in a single static
factory called by the builder) to enforce: non-null and non-blank for name,
nickname and email (trim inputs), length bounds for name and nickname (define
and use clear min/max constants), and a proper email format check (use a simple
regex or a shared EmailValidator utility); on failure throw a clear runtime
exception such as IllegalArgumentException with descriptive messages so invalid
objects cannot be created.
- Around line 67-79: The updateProfile method currently only checks for nulls so
blank or whitespace-only nicknames can overwrite valid values; modify
Member.updateProfile to treat blank/whitespace-only nickname as invalid (e.g.,
if nickname is not null and nickname.trim().isEmpty() then throw an appropriate
exception or skip assignment) so only non-blank nicknames update this.nickname;
keep existing null handling for schoolId and profileImageId and reuse
validateActive() at the start; if you prefer using a utility, use a single
non-null-and-not-blank check (e.g., StringUtils.hasText or similar) before
assigning nickname.
In @src/main/java/com/umc/product/member/domain/MemberOAuth.java:
- Around line 38-39: The providerId field in MemberOAuth lacks a DB length
constraint; update the field annotation to include a reasonable max length
(e.g., set @Column(name = "provider_id", nullable = false, length = 255)) and
add a matching validation annotation (e.g., @Size(max = 255)) on the providerId
field so the schema enforces the limit and the entity validates inputs before
persistence.
- Around line 41-46: The private builder constructor MemberOAuth(Long memberId,
OAuthProvider provider, String providerId) lacks application-level null checks
for required fields; add explicit validations at the start of that constructor
(or a private validate method called from it) to check memberId, provider, and
providerId are not null (and providerId not empty) and throw clear exceptions
(e.g., IllegalArgumentException or NullPointerException) with descriptive
messages like "memberId must not be null", "provider must not be null",
"providerId must not be null or empty" to fail fast and surface meaningful
errors before reaching DB constraints.
In @src/main/java/com/umc/product/member/domain/MemberTermAgreement.java:
- Around line 36-37: MemberTermAgreement currently defines a separate Instant
field agreedAt which may duplicate BaseEntity's createdAt; review whether
agreedAt is necessary and either remove it and reuse createdAt everywhere the
agreement timestamp is required, or retain agreedAt and add a clear
Javadoc/field comment explaining its distinct business meaning (e.g., differs
from entity creation time) and ensure mapping/initialization is consistent (set
agreedAt explicitly when the user agrees). Reference:
MemberTermAgreement.agreedAt and BaseEntity.createdAt.
In @src/main/java/com/umc/product/member/domain/OAuthProvider.java:
- Around line 3-8: Add JavaDoc to the OAuthProvider enum and its constants to
explain the enum's purpose and each provider, and implement a static parsing
utility method OAuthProvider.fromString(String provider) that safely converts a
case-insensitive provider name to the corresponding enum constant (handle
null/blank input and unknown values by throwing an IllegalArgumentException with
a clear message) so callers can centralize error handling when mapping external
strings to OAuthProvider.
| public interface ManageChallengerUseCase { | ||
| /** | ||
| * 챌린저 상벌점 부여 | ||
| */ | ||
| void givePointsToChallenger(); | ||
|
|
||
| // 역할이라고 함은, 운영진 중 일부를 의미 | ||
|
|
||
| /** | ||
| * 챌린저에게 역할을 부여 | ||
| */ | ||
| void assignRoleToChallenger(); | ||
|
|
||
| /** | ||
| * 챌린저 역할 회수 | ||
| */ | ||
| void revokeRoleFromChallenger(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UseCase 인터페이스를 단일 책임 원칙에 따라 분리해주세요.
이전 리뷰에서 지적된 것처럼, ManageChallengerUseCase는 3개의 서로 다른 유스케이스를 포함하고 있습니다. 헥사고날 아키텍처와 프로젝트 컨벤션에 따라 각 UseCase 인터페이스는 하나의 유스케이스만 담당해야 하며, {Action}{Domain}UseCase 네이밍 패턴을 따라야 합니다.
다음과 같이 분리가 필요합니다:
GivePointsToChallengerUseCaseAssignRoleToChallengerUseCaseRevokeRoleFromChallengerUseCase
Based on learnings, UseCase 인터페이스는 단일 책임 원칙을 따라야 합니다.
🤖 Prompt for AI Agents
In
@src/main/java/com/umc/product/challenger/application/port/in/command/ManageChallengerUseCase.java
around lines 3 - 20, The ManageChallengerUseCase interface bundles three
distinct use cases; split it into three single-responsibility interfaces named
GivePointsToChallengerUseCase, AssignRoleToChallengerUseCase, and
RevokeRoleFromChallengerUseCase, each exposing only the corresponding method
(e.g., givePointsToChallenger ->
GivePointsToChallengerUseCase.givePointsToChallenger, assignRoleToChallenger ->
AssignRoleToChallengerUseCase.assignRoleToChallenger, revokeRoleFromChallenger
-> RevokeRoleFromChallengerUseCase.revokeRoleFromChallenger); create separate
files for these interfaces, update any implementing classes to implement the
correct new interface(s), and adjust dependency injection or wiring to reference
the new interface types.
| */ | ||
| void givePointsToChallenger(); | ||
|
|
||
| // 역할이라고 함은, 운영진 중 일부를 의미 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
인터페이스 내 구현 세부사항 주석을 제거하는 것을 권장합니다.
"역할이라고 함은, 운영진 중 일부를 의미"라는 주석은 도메인 지식이나 구현 세부사항에 해당하며, 인터페이스 레벨에서는 불필요합니다. 이러한 설명은 다음 위치가 더 적합합니다:
- 도메인 엔티티(ChallengerRole)의 클래스 레벨 Javadoc
- 프로젝트 문서나 위키
- 구현체(Service)의 주석
인터페이스는 계약(contract)만 정의하고, 세부 구현이나 도메인 개념 설명은 분리하는 것이 바람직합니다.
🤖 Prompt for AI Agents
In
@src/main/java/com/umc/product/challenger/application/port/in/command/ManageChallengerUseCase.java
at line 9, Remove the implementation/domain-detail comment "// 역할이라고 함은, 운영진 중
일부를 의미" from the ManageChallengerUseCase interface; interfaces should only
define the contract. If the clarification is needed, add it to the
ChallengerRole entity class Javadoc or to the implementing Service's
documentation (e.g., the class that implements ManageChallengerUseCase) or
project docs/wiki instead.
| @Builder | ||
| public record ChallengerPublicInfo( | ||
| Long challengerId, | ||
| Long memberId, | ||
| Long gisuId, | ||
| ChallengerPart part | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Query 레코드에 필드 검증 어노테이션을 추가해주세요.
Query 레코드는 불변성과 함께 @NotNull 검증을 포함해야 합니다. 현재 모든 필드에 검증이 누락되어 있어 null 값이 전달될 수 있습니다.
제안 수정
+import jakarta.validation.constraints.NotNull;
+
@Builder
public record ChallengerPublicInfo(
+ @NotNull
Long challengerId,
+ @NotNull
Long memberId,
+ @NotNull
Long gisuId,
+ @NotNull
ChallengerPart part
) {🤖 Prompt for AI Agents
In
@src/main/java/com/umc/product/challenger/application/port/in/query/ChallengerPublicInfo.java
around lines 7 - 13, Add javax/validation NotNull constraints to the
ChallengerPublicInfo record components so none of the query fields can be null:
annotate challengerId, memberId, gisuId and part with @NotNull on the record
components in the ChallengerPublicInfo declaration (ensure you import the
correct NotNull, e.g., jakarta.validation.constraints.NotNull or
javax.validation.constraints.NotNull consistent with the project).
| package com.umc.product.challenger.application.port.in.query; | ||
|
|
||
| public interface GetChallengerUseCase { | ||
| // TODO: 챌린저에 대해서 public/private 정보 구분 필요 시 method 추가해서 진행하여야 함 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
TODO 주석은 향후 이슈로 관리하는 것을 권장합니다.
public/private 정보 구분에 대한 TODO가 남아있습니다. 코드베이스에 TODO를 남기는 것보다 별도 이슈로 등록하여 추적하는 것이 더 효과적입니다.
별도 이슈를 생성하거나, 구현 방안(별도 UseCase 메서드 추가, 권한 기반 필터링 등)에 대한 설계 제안이 필요하시면 도움을 드릴 수 있습니다.
🤖 Prompt for AI Agents
In
@src/main/java/com/umc/product/challenger/application/port/in/query/GetChallengerUseCase.java
at line 4, The TODO comment about distinguishing public/private challenger
visibility should be removed from the GetChallengerUseCase interface and either
recorded as a tracked issue in the project tracker or addressed by adding
explicit API/UseCase support: create a new method (e.g., getPublicChallengers /
getPrivateChallengers or add a visibility parameter to getChallenger) or design
permission-based filtering in the service layer; update GetChallengerUseCase to
reflect the chosen approach and reference the new issue/PR ID in code comments
instead of leaving a TODO.
| /** | ||
| * 챌린저 ID로 공개 가능한 정보 조회 | ||
| */ | ||
| ChallengerPublicInfo getChallengerPublicInfo(Long challengerId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력 파라미터 검증을 추가해주세요.
challengerId 파라미터에 @NotNull 검증 어노테이션이 누락되어 있습니다. null 값이 전달될 경우 NullPointerException이 발생할 수 있습니다.
🔒 권장 수정안
파일 상단에 import 추가:
import jakarta.validation.constraints.NotNull;메서드 시그니처 수정:
- ChallengerPublicInfo getChallengerPublicInfo(Long challengerId);
+ ChallengerPublicInfo getChallengerPublicInfo(@NotNull Long challengerId);🤖 Prompt for AI Agents
In
@src/main/java/com/umc/product/challenger/application/port/in/query/GetChallengerUseCase.java
at line 9, The method getChallengerPublicInfo in the GetChallengerUseCase
interface is missing validation for its challengerId parameter; add
jakarta.validation.constraints.NotNull to the imports and annotate the method
parameter with @NotNull on getChallengerPublicInfo(Long challengerId) to prevent
nulls and enable bean validation.
| public enum TermType { | ||
| SERVICE, // 서비스이용약관 | ||
| PRIVACY, // 개인정보처리방침 | ||
| MARKETING // 마케팅정보수신동의 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Enum 네이밍 컨벤션을 따라주세요.
프로젝트의 Enum 네이밍 컨벤션에 따르면 {Domain}{Type} 패턴을 따라야 합니다. 현재 TermType은 MemberTermType으로 변경하여 일관성을 유지해야 합니다.
Based on learnings, Enum 파일명은 {Domain}{Type} 패턴을 따라야 합니다.
♻️ 수정 제안
파일명을 MemberTermType.java로 변경하고 enum 이름도 함께 변경:
-public enum TermType {
+public enum MemberTermType {
SERVICE, // 서비스이용약관
PRIVACY, // 개인정보처리방침
MARKETING // 마케팅정보수신동의
}📝 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.
| public enum TermType { | |
| SERVICE, // 서비스이용약관 | |
| PRIVACY, // 개인정보처리방침 | |
| MARKETING // 마케팅정보수신동의 | |
| } | |
| public enum MemberTermType { | |
| SERVICE, // 서비스이용약관 | |
| PRIVACY, // 개인정보처리방침 | |
| MARKETING // 마케팅정보수신동의 | |
| } |
| INVALID_MEMBER_STATUS(HttpStatus.BAD_REQUEST, "MEMBER-0005", "올바르지 않은 사용자 상태입니다."), | ||
| MEMBER_NOT_ACTIVE(HttpStatus.BAD_REQUEST, "MEMBER-0006", "올바르지 않은 사용자 상태입니다."), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 메시지가 중복되어 있습니다.
INVALID_MEMBER_STATUS와 MEMBER_NOT_ACTIVE가 동일한 메시지("올바르지 않은 사용자 상태입니다.")를 사용하고 있습니다. 두 에러 코드는 서로 다른 상황을 나타내므로, 명확히 구분되는 메시지가 필요합니다.
제안하는 수정안:
INVALID_MEMBER_STATUS(HttpStatus.BAD_REQUEST, "MEMBER-0005", "올바르지 않은 사용자 상태입니다."),
MEMBER_NOT_ACTIVE(HttpStatus.BAD_REQUEST, "MEMBER-0006", "활성화되지 않은 사용자입니다."),🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/exception/MemberErrorCode.java
around lines 16 - 17, The two enum entries INVALID_MEMBER_STATUS and
MEMBER_NOT_ACTIVE in MemberErrorCode currently share the same message; update
MEMBER_NOT_ACTIVE’s message to a distinct, clearer text (e.g., "활성화되지 않은
사용자입니다.") so the error codes represent different situations; locate the
MemberErrorCode enum and change the message string for the MEMBER_NOT_ACTIVE
constant while keeping its HttpStatus and code unchanged.
| @Column(name = "member_id", nullable = false) | ||
| private Long memberId; // ID 참조만 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find Member entity and examine its structure and deletion methods
find . -type f -name "Member.java" | head -5Repository: UMC-PRODUCT/umc-product-server
Length of output: 131
🏁 Script executed:
# Search for Member entity and deletion-related methods
rg -n "class Member" --type java -A 30 | head -100Repository: UMC-PRODUCT/umc-product-server
Length of output: 9475
🏁 Script executed:
# Check MemberTermAgreement entity fully
rg -n "class MemberTermAgreement" --type java -A 50Repository: UMC-PRODUCT/umc-product-server
Length of output: 2412
🏁 Script executed:
# Search for deletion/cascade handling in services
rg -n -B 2 -A 5 "memberId.*delete|cascade|orphan|referential" --type javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 1006
🏁 Script executed:
# Check for soft delete implementation
rg -n "softDelete|DeletedAt|IsDeleted|deleted.*boolean" --type javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Check MemberStatus enum to see if it includes deletion/inactive states
rg -n "enum MemberStatus" --type java -A 10Repository: UMC-PRODUCT/umc-product-server
Length of output: 636
🏁 Script executed:
# Check Member entity methods for deletion or status change
rg -n "class Member" --type java -A 100 | grep -E "delete|Delete|status|Status|deactivate"Repository: UMC-PRODUCT/umc-product-server
Length of output: 1113
🏁 Script executed:
# Search for services that handle Member deletion and related cascade logic
rg -n "deleteMember|deleteById|deactivate|withdraw" --type java -B 2 -A 5Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Check repository methods for Member
rg -n "interface.*MemberRepository|class.*MemberRepository" --type java -A 30Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Look for cascading delete logic in service layer for MemberTermAgreement
rg -n "class.*MemberTermAgreement.*Service|MemberTermAgreement.*Repository" --type java -A 20Repository: UMC-PRODUCT/umc-product-server
Length of output: 2977
🏁 Script executed:
# Check for @PreRemove or lifecycle methods in Member entity
rg -n "@PreRemove|@PostRemove|@PreDelete|@PostDelete" --type java -B 2 -A 5Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Check Member entity fully for any lifecycle or deletion methods
cat src/main/java/com/umc/product/member/domain/Member.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 2925
🏁 Script executed:
# Search for withdrawal or deletion service methods and cleanup logic
rg -n "withdraw|delete|deactivate" --type java -B 3 -A 8 | grep -E "service|Service|command|Command"Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
참조 무결성 처리 로직이 필요합니다.
memberId를 FK 없이 참조하는 설계는 의도적이나, 현재 구현이 불완전합니다. Member 삭제 시 MemberTermAgreement와 MemberOAuth 레코드가 고아 상태가 되며, 응용 계층에서 이를 처리하는 로직이 없습니다.
필수 개선사항:
-
Member 삭제 시 관련 레코드 처리: MemberTermAgreement와 MemberOAuth를 함께 삭제하거나, 소프트 삭제(status = WITHDRAWN) 기반으로 통합해야 합니다. 현재 Member 엔티티의 TODO 주석("탈퇴 및 휴면 처리에 대한 도메인 로직은 추후 추가")에서 확인되듯이 이는 미구현 상태입니다.
-
삭제 전략 선택:
- 물리 삭제 선택 시 → 서비스 계층에서 Member 삭제 시 MemberTermAgreement/MemberOAuth도 함께 삭제하는 트랜잭션 로직 구현
- 소프트 삭제 선택 시 → MemberStatus.WITHDRAWN 상태 변경으로 통합하고, 관련 엔티티에도 삭제 마크 필요
-
설계 문서화: 현재 FK를 사용하지 않는 이유(도메인 간 느슨한 결합)가 주석으로 있으나, 대신 보장되는 참조 무결성 처리 방식을 명확히 문서화해야 합니다.
| @Column(name = "agreed_at", nullable = false) | ||
| private Instant agreedAt; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
agreedAt 필드의 필요성을 재검토해주세요.
MemberTermAgreement가 BaseEntity를 상속받고 있어 createdAt 필드를 가지고 있을 가능성이 높습니다. agreedAt 필드가 동의 시점을 저장하는 목적이라면, BaseEntity의 createdAt과 의미가 중복될 수 있습니다.
다음을 고려해주세요:
- 약관 동의 시점과 엔티티 생성 시점이 항상 동일하다면
createdAt재사용 검토 - 별도의 비즈니스 의미가 있다면 현재 구조 유지 및 문서화
Also applies to: 43-43
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/MemberTermAgreement.java around
lines 36 - 37, MemberTermAgreement currently defines a separate Instant field
agreedAt which may duplicate BaseEntity's createdAt; review whether agreedAt is
necessary and either remove it and reuse createdAt everywhere the agreement
timestamp is required, or retain agreedAt and add a clear Javadoc/field comment
explaining its distinct business meaning (e.g., differs from entity creation
time) and ensure mapping/initialization is consistent (set agreedAt explicitly
when the user agrees). Reference: MemberTermAgreement.agreedAt and
BaseEntity.createdAt.
| public enum OAuthProvider { | ||
| GOOGLE, | ||
| APPLE, | ||
| KAKAO, | ||
| NAVER | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
JavaDoc과 유틸리티 메서드 추가를 권장합니다.
현재 enum이 단순하게 정의되어 있지만, 다음 개선사항을 고려해주세요:
- JavaDoc 추가: enum의 목적과 각 상수의 의미를 문서화
- 파싱 메서드 추가: 문자열을 OAuthProvider로 변환하는
fromString(String provider)메서드 구현을 통해 예외 처리를 중앙화
♻️ 개선 제안
+/**
+ * OAuth 2.0 제공자(Provider)를 나타내는 열거형
+ * 지원되는 소셜 로그인 플랫폼을 정의합니다.
+ */
public enum OAuthProvider {
+ /** Google OAuth 2.0 */
GOOGLE,
+ /** Apple Sign In */
APPLE,
+ /** Kakao OAuth 2.0 */
KAKAO,
+ /** Naver OAuth 2.0 */
NAVER;
+
+ /**
+ * 문자열을 OAuthProvider로 변환
+ * @param provider 제공자 이름 (대소문자 무시)
+ * @return 해당하는 OAuthProvider
+ * @throws IllegalArgumentException 지원하지 않는 제공자인 경우
+ */
+ public static OAuthProvider fromString(String provider) {
+ try {
+ return OAuthProvider.valueOf(provider.toUpperCase());
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new IllegalArgumentException("Unsupported OAuth provider: " + provider);
+ }
+ }
}🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/OAuthProvider.java around lines
3 - 8, Add JavaDoc to the OAuthProvider enum and its constants to explain the
enum's purpose and each provider, and implement a static parsing utility method
OAuthProvider.fromString(String provider) that safely converts a
case-insensitive provider name to the corresponding enum constant (handle
null/blank input and unknown values by throwing an IllegalArgumentException with
a clear message) so callers can centralize error handling when mapping external
strings to OAuthProvider.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
| // Use DBML to define your database structure | ||
| // Docs: https://dbml.dbdiagram.io/docs | ||
|
|
||
| // created_at, updated_at은 모든 테이블에 있는 것을 가정합니다. | ||
| // base_entity로 제작 | ||
|
|
||
|
|
||
| // === 사용자 및 회원가입 관련 === | ||
|
|
||
| enum MemberStatus { | ||
| PENDING // OAuth 로그인 시도 했던 사람 | ||
| ACTIVE // 유효한 회원 | ||
| INACTIVE // 휴면 계정 | ||
| WITHDRAWN // 탈퇴 계정 | ||
| } | ||
|
|
||
| Table member { | ||
| id bigint [pk, increment] | ||
| name varchar | ||
| nickname varchar | ||
| email varchar | ||
| school_id varchar [ref: > school.id] | ||
| profile_image_id bigint | ||
| status MemberStatus | ||
|
|
||
| note: '회원가입 시 단계를 구분하기 위한 단계' | ||
|
|
||
| // 사용자의 학교는 챌린저 활동 당시에 결정되도록 함. | ||
| // 지원서에 지원하는 학교를 명시하도록 함. | ||
| } | ||
|
|
||
| enum OAuthProvider { | ||
| APPLE | ||
| KAKAO | ||
| } | ||
|
|
||
| Table member_oauth { | ||
| id bigint [pk, increment] | ||
| member_id bigint [ref: > member.id] | ||
| provider OAuthProvider [note: 'OAuth Provider'] | ||
| provider_member_id varchar [note: 'OAuth Provider의 ID'] | ||
| } | ||
|
|
||
| // === 약관 관련 === | ||
|
|
||
| // Table terms { | ||
| // id bigint [pk, increment] | ||
| // content varchar | ||
| // is_required bool | ||
| // } | ||
|
|
||
| // Terms는 따로 문서로 관리하도록 하고, 사용자가 어떤 약관을 동의하였는지 종류만 enum으로 표기하도록 함 | ||
|
|
||
| enum TermType { | ||
| SERVICE_POLICY | ||
| PRIVACY_POLICY | ||
| MARKETING_POLICY | ||
| } | ||
|
|
||
| Table member_term_agreement { | ||
| id bigint [pk, increment] | ||
| member_id bigint [ref: > member.id] | ||
| term_type TermType | ||
| agreed_at datetime [default: `now()`] | ||
| } | ||
|
|
||
| Table school { | ||
| id bigint [pk, increment] | ||
| name varchar | ||
| logo_image_id bigint | ||
| email_domain varchar [note: 'cau.ac.kr 처럼 학교별 이메일 '] | ||
| } | ||
|
|
||
| Table gisu { | ||
| id bigint [pk, increment, note: '기수 번호 (1, 2, 3...)'] | ||
| is_active bool [note: '현재 활동 기수 여부'] | ||
| start_at datetime | ||
| ends_at datetime | ||
| } | ||
|
|
||
| Table central_organization { | ||
| id bigint [pk, increment] | ||
| gisu_id bigint [ref: > gisu.id] | ||
| name varchar [default: '9기 중앙운영사무국'] | ||
|
|
||
| Note: '중앙 조직은 단 하나만 존재' | ||
| } | ||
|
|
||
| Table chapter { | ||
| id bigint [pk, increment] | ||
| name varchar | ||
| gisu_id bigint [ref: > gisu.id] | ||
| } | ||
|
|
||
| // 지부 내 학교 | ||
| Table chapter_school { | ||
| id bigint [pk, increment] | ||
| chapter_id bigint [ref: > chapter.id] | ||
| school_id bigint [ref: > school.id] | ||
| } | ||
|
|
||
| // === 웹 팀 지원서 관리는 이쪽에 === | ||
|
|
||
|
|
||
| // === 실제 활동 챌린저 관련 === | ||
|
|
||
| enum ChallengerStatus { | ||
| ACTIVE // 활성 | ||
| GRADUATED // 수료 | ||
| EXPELLED // 제명 (3OUT 포함) | ||
| WITHDRAWN // 자진탈부 | ||
| } | ||
|
|
||
| enum ChallengerPart { | ||
| PLAN | ||
| DESIGN | ||
| WEB | ||
| IOS | ||
| ANDROID | ||
| SPRINGBOOT | ||
| NODEJS | ||
| } | ||
|
|
||
| Table challenger { | ||
| id bigint [pk, increment] | ||
|
|
||
| member_id bigint [ref: > member.id] | ||
| part ChallengerPart | ||
| gisu_id bigint [ref: > gisu.id] | ||
| status ChallengerStatus | ||
| } | ||
|
|
||
| /** | ||
| e.g. | ||
|
|
||
| 중앙대학교 회장: role_group = 중앙대학교 role = (scope:학교, name:회장) | ||
| 중앙운영사무국 총괄: role_group = 중앙운영사무국 role = 중앙운영사무국 총괄 | ||
|
|
||
| **/ | ||
|
|
||
| enum OrganizationType { | ||
| CENTRAL | ||
| CHAPTER | ||
| SCHOOL | ||
| } | ||
|
|
||
| Table challenger_role { | ||
| id bigint [pk, increment] | ||
| challenger_id bigint [ref: > challenger.id] | ||
| role_type RoleType [not null] | ||
|
|
||
| // 조직 정보 (계층 구조로 명확화) | ||
| organization_type OrganizationType [not null, note: '조직 단위'] | ||
| organization_id bigint [not null, note: '지부, 학교 ID'] | ||
|
|
||
| // 파트장인 경우 | ||
| leadingPart ChallengerPart [null, note: '파트장인 경우만'] | ||
|
|
||
| gisu_id bigint [ref: > gisu.id] | ||
| } | ||
|
|
||
| enum PointType { | ||
| BEST_WORKBOOK | ||
| WARNING | ||
| OUT | ||
| } | ||
|
|
||
| Table challenger_point { | ||
| id bigint [pk, increment] | ||
| challenger_id bigint [ref: > challenger.id] | ||
| type PointType [note: '상벌점 종류 구분'] | ||
| description varchar [note: '사유'] | ||
| related_workbook_id bigint [ref: > challenger_workbook.id, note: '사유가 되는 워크북 ID, 기입하지 않을 수도 있어서 nullable'] | ||
| } | ||
|
|
||
| // === ROLE === | ||
|
|
||
| // 역할을 일일히 만들어야 하나? | ||
| // 교내: 회장, 부회장, 파트장, 기타운영진 | ||
| // 지부: 지부장 | ||
| // 중앙운영사무국: 총괄, 부총괄, 교육국, 교육국원, | ||
|
|
||
| enum RoleType { | ||
| // 중앙 | ||
| CENTRAL_PRESIDENT // 총괄 | ||
| CENTRAL_VICE_PRESIDENT // 부총괄 | ||
| CENTRAL_DIRECTOR // 국장 | ||
| CENTRAL_MANAGER // 국원 | ||
| CENTRAL_PART_LEADER // 중앙 파트장 | ||
|
|
||
| // 지부 | ||
| CHAPTER_LEADER // 지부장 | ||
| CHAPTER_STAFF // 지부 운영진 | ||
|
|
||
| // 학교 | ||
| SCHOOL_PRESIDENT // 회장 | ||
| SCHOOL_VICE_PRESIDENT // 부회장 | ||
| SCHOOL_PART_LEADER // 파트장 | ||
| SCHOOL_STAFF // 기타 운영진 | ||
|
|
||
| // 일반 | ||
| CHALLENGER // 챌린저 | ||
| } | ||
|
|
||
|
|
||
| // === Home === | ||
|
|
||
|
|
||
|
|
||
| // === 공지 === | ||
|
|
||
| Table notice { | ||
| id bigint [pk, increment] | ||
|
|
||
| title varchar | ||
| content varchar | ||
| author_challenger_id bigint [ref: > challenger.id, note: '작성자 챌린저 ID, 사용자 ID로 구분?'] | ||
| vote_id bigint [ref: > question.id, note: '투표가 없는 경우 null'] | ||
| link varchar [note: '링크'] | ||
|
|
||
| // 공지의 카테고리 (누가 작성할 수 있는지) | ||
| scope OrganizationType [note: '어떤 scope에서 작성된 공지인지'] | ||
| organization_id bigint [not null, note: 'scope에 따라 중앙/지부/학교 ID'] | ||
|
|
||
| // 여러 카테고리의 사람에게 날리는 공지라면? | ||
| // e.g. Spring&Node 대상 공지 (개발파트 공지) | ||
| // e.g.2 Pla&Design 대상 공지 | ||
|
|
||
| // 누가 이 공지를 볼 수 있는지 | ||
| target_roles array [note: 'RoleType의 Array, null이면 전체 역할 대상 (회장단 or 지부장 공지용)'] | ||
| target_parts array [note: 'ChallengerPart의 Array, null이면 전체 파트 대상'] | ||
| target_gisu_id bigint [ref: > gisu.id, note: 'null아면 전체 기수 대상'] | ||
|
|
||
| // 알람 발송 대상이였는지도 저장해야 하나, OR 그냥 바로 발송하는 방식? | ||
| should_notify bool [default: true, note: '알림 전송 여부'] | ||
| notified_at datetime [null, note: '알림 발송 시각'] | ||
| } | ||
|
|
||
| Table notice_read { | ||
| id bigint [pk, increment] | ||
| notice_id bigint [ref: > notice.id] | ||
| challenger_id bigint [ref: > challenger.id] | ||
| } | ||
|
|
||
| Table notice_image { | ||
| id bigint [pk, increment] | ||
| notice_id bigint [ref: > notice.id] | ||
| image_id bigint [ref: > files.id] | ||
| } | ||
| // === INFRA: S3 === | ||
|
|
||
| Table files { | ||
| id bigint [pk, increment] | ||
| original_file_name varchar | ||
| stored_file_name varchar | ||
| s3_key varchar | ||
| content_type varchar | ||
| file_size varchar | ||
| status bool [note: '업로드 여부'] | ||
| } | ||
|
|
||
| // ========================================== | ||
| // ================ 예은 작업본 시작 ================ | ||
| // ========================================== | ||
|
|
||
| // === 활동 부분 === | ||
|
|
||
| // 파트별 커리큘럼 부모 | ||
| Table curriculum { | ||
| id bigint [pk, increment] | ||
| gisu_id bigint [ref: > gisu.id, note: '어떤 기수의 워크북인지'] | ||
| part ChallengerPart [note: '어떤 파트의 워크북인지'] | ||
| } | ||
|
|
||
| // 파트별 커리큘럼 - 주차 | ||
| // 작성할 수 있는 role을 한정 ? - 비즈니스로 할지, role을 엮을지 | ||
| Table original_workbook { | ||
| id bigint [pk, increment] | ||
| curriculum_id bigint [ref: > curriculum.id, note: '해당 워크북이 소속된 커리큘럼'] | ||
| title varchar [note: '주차별 워크북 제목'] | ||
| description varchar [note: '워크북 설명'] | ||
| workbook_url varchar [note: 'Notion으로 워크북 관리 시 URL'] | ||
| order_no int [note: '워크북 순서'] | ||
| } | ||
|
|
||
| Table workbook_mission { | ||
| id bigint [pk, increment] | ||
| title varchar [note: '미션 이름'] | ||
| mission_type MissionType [note: '미션의 형태'] | ||
| content varchar [note: '미션 내용'] | ||
|
|
||
| Note: '각 주차별 워크북에 있는 미션' | ||
| } | ||
|
|
||
| enum WorkbookStatus { | ||
| PASS | ||
| FAIL | ||
| PENDING | ||
| } | ||
|
|
||
| enum MissionType { | ||
| LINK [note: '링크 형식'] | ||
| MEMO [note: '메모 형식'] | ||
| PLAIN [note: '완료만 체크하는 형식'] | ||
| } | ||
|
|
||
| // 주차별 챌린저 워크북 | ||
| Table challenger_workbook { | ||
| id bigint [pk, increment] | ||
| challenger_id bigint [ref: > challenger.id] | ||
| original_workbook_id bigint [ref: > original_workbook.id, note: '원본 워크북'] | ||
| status WorkbookStatus | ||
| schedule_id bigint [ref: > schedule.id] | ||
| is_best bool [note: '베스트 워크북 여부'] | ||
| } | ||
|
|
||
| Table challenger_mission { | ||
| id bigint [pk, increment] | ||
| mission_id bigint [ref: > workbook_mission.id] | ||
| challenger_workbook_id bigint [ref: > challenger_workbook.id] | ||
| submission varchar [note: '제출물'] | ||
| } | ||
|
|
||
| Table study_group { | ||
| id bigint [pk, increment] | ||
| name varchar [note: '스터디명'] | ||
| gisu_id bigint [ref: > gisu.id] | ||
| group_leader_id bigint [ref: > challenger.id, note: '스터디장'] | ||
|
|
||
| note: '각 스터디를 나타냄, 학교/지부에는 속하지 않고 기수에만 속함.' | ||
| } | ||
|
|
||
| Table study_group_member { | ||
| id bigint [pk, increment] | ||
| group_id bigint [ref: > study_group.id] | ||
| member_id bigint [ref: > challenger.id] | ||
|
|
||
| note: '각 스터디에 속한 인원을 표현' | ||
| } | ||
|
|
||
| // ============= 스케쥴 ================== | ||
|
|
||
| // 해당 type에 따라서 출석 여부를 결정할 수 있는 사람의 권한 범위가 딸려옵니다. | ||
| enum ScheduleType { | ||
| STUDY_SESSION | ||
| CENTRAL_EVENT | ||
| CHAPTER_EVENT | ||
| SCHOOL_EVENT | ||
| } | ||
|
|
||
| // 모든 것은 일정으로 관리됨 (스터디, 행사 등) | ||
| Table schedule { | ||
| id bigint [pk, increment] | ||
| name varchar | ||
| description varchar | ||
| type ScheduleType | ||
| author_challenger_id bigint [ref: > challenger.id, note: '일정을 생성한 챌린저 ID'] | ||
|
|
||
| starts_at datetime | ||
| ends_at datetime | ||
| location_name varchar // 장소는 텍스트 형식으로만 제공 | ||
| location point | ||
|
|
||
| late_threshold interval [note: '지각 허용 시간', default: '00:10:00'] | ||
| absent_threshold interval [note: '결석 처리 시간', default: '00:30:00'] | ||
| attendance_radius int [default: 50, note: '인정 반경 (미터 단위)'] | ||
|
|
||
|
|
||
| // 정기 스터디도 스케쥴을 생성해야 하네 | ||
| // Spring Scheduler를 통해서 주기마다 생성하도록 해야할 것 같은데 | ||
| } | ||
|
|
||
| enum AttendanceStatus { | ||
| PENDING // 대기중 | ||
| PRESENT // 출석 | ||
| PRESENT_PENDING | ||
| LATE // 지각 | ||
| LATE_PENDING // 지각으로 출석 승인 대기 | ||
| ABSENT // 결석, 결석은 Scheduler에서 schedule의 absent threshold 경과 발견 시 자동으로 결석처리 | ||
| EXCUSED // 인정결석 | ||
| EXCUSED_PENDING | ||
| } | ||
|
|
||
| // 특정 schedule의 출석은 누가 변경할 수 있는가? 에 대한 명시가 필요해 보입니다. | ||
| // => schedule type 별로 조직을 결정. | ||
|
|
||
| // 각 스케쥴에 대한 출석 정보, 곧 개인별 출석부에 해당함. (이번 스케쥴의 출석 여부를 나타냄) | ||
| // 각 스케쥴을 생성할 때 미리 대상자를 결정해야 "출석부" 를 생성하는 것 | ||
| Table schedule_attendance { | ||
| id bigint [pk, increment] | ||
|
|
||
| // 혹시 다른 기수 사람일 수도 있으니까 member로 연결 | ||
| member_id bigint [ref: > member.id] | ||
| schedule_id bigint [ref: > schedule.id] | ||
| location point | ||
| status AttendanceStatus | ||
| // 승인은 무조건 운영진(=챌린저)가 해야함 | ||
| confirmed_challenger_id bigint [ref: > challenger.id] | ||
| reason varchar [note: '출석 인정 사유 발생 시 입력'] | ||
| } | ||
|
|
||
|
|
||
| // ========================================== | ||
| // ================ 세니 작업본 ================ | ||
| // ========================================== | ||
|
|
||
| // 1. 커뮤니티 부분은 challenger_id가 아니라 member_id로 매핑해야 할 것 같은데 어떻게 생각하시나요? | ||
|
|
||
| enum CommunityPostCategory { | ||
| LIGHTNING [note: '번개'] | ||
| HOBBY [note: '취미'] | ||
| QUESTION [note: '질문'] | ||
| INFO [note: '정보'] | ||
| SUGGESTION [note: '건의'] | ||
| GENERAL [note: '자유 (general로 이름 변경)'] | ||
| } | ||
|
|
||
| enum CommunityPostRegion { | ||
| SEOUL [note: '서울'] | ||
| GYEONGGI_INCHEON [note: '경기/인천'] | ||
| DAEJEON_CHUNGCHEONG [note: '대전/충청'] | ||
| BUSAN_GYEONGNAM [note: '부산/경남'] | ||
| } | ||
|
|
||
| enum CommunityPostStatus { | ||
| OPEN [note: '모집 중'] | ||
| CLOSED [note: '마감'] | ||
| } | ||
|
|
||
| Table community_post { // SuperType | ||
| id bigint [pk, increment] | ||
| title varchar | ||
| content varchar | ||
| is_anonymous bool [note: '건의사항의 경우 익명 여부'] | ||
| status CommunityPostStatus [default: 'OPEN'] | ||
| category CommunityPostCategory [not null] | ||
| region CommunityPostRegion [not null] | ||
|
|
||
| // 관계 매핑 | ||
| author_member_id bigint [ref: > member.id] | ||
| } | ||
|
|
||
| // flash_gather라는 표현을 쓴다고 구글이 그러는데 lightening 쓸까요? ㅋㅋㅋ | ||
| // 별개로, 이건 번개모임의 정보를 나타내는 부분이니 해당 번개 자체를 나타내면 좋을 것 같아요. | ||
| Table flash_gather { // SubType | ||
| id bigint [pk, increment] | ||
| meeting_at datetime [note: '번개 날짜/시간'] | ||
| location varchar [note: '구체적인 장소'] | ||
| max_people int [note: '최대 인원'] | ||
| open_chat_link varchar [note: '오픈채팅방 링크'] | ||
|
|
||
| // 관계 매핑 | ||
| post_id bigint [not null, unique, ref: - community_post.id] | ||
| } | ||
|
|
||
| // community_like 정도로 축약해도 좋을 것 같습니다. | ||
| Table community_post_like { | ||
| id bigint [pk, increment] | ||
|
|
||
| // 관계 매핑 | ||
| member_id bigint [ref: > member.id] | ||
| community_post_id bigint [ref: > community_post.id] | ||
| } | ||
|
|
||
| Table comment { | ||
| id bigint [pk, increment] | ||
| content varchar | ||
|
|
||
| // 관계 매핑 | ||
| author_challenger_id bigint [ref: > challenger.id] | ||
| community_post_id bigint [ref: > community_post.id] | ||
| parent_comment_id bigint [null, ref: > comment.id, note: '자기 참조'] | ||
| } | ||
|
|
||
| Table community_post_image { | ||
| id bigint [pk, increment] | ||
| post_id bigint [ref: > community_post.id] | ||
| image_id bigint [ref: > files.id] | ||
| } | ||
|
|
||
| // ====== 폼 ====== | ||
|
|
||
| Table form { | ||
| id bigint [pk, increment] | ||
| created_member_id bigint [ref: > member.id] | ||
| title varchar | ||
| description varchar | ||
| is_active bool | ||
|
|
||
| Note: '폼 한 개 (질문을 담고 있는)' | ||
| } | ||
|
|
||
| enum QuestionType { | ||
| SHORT_TEXT | ||
| LONG_TEXT | ||
| RADIO | ||
| CHECKBOX | ||
| DROPDOWN | ||
| SCHEDULE | ||
| PORTFOLIO | ||
| } | ||
|
|
||
| enum FormResponseStatus { | ||
| DRAFT | ||
| SUBMITTED | ||
| } | ||
|
|
||
| enum RecruitmentPhase { | ||
| BEFORE_APPLY | ||
| APPLY_OPEN | ||
| DOC_REVIEWING | ||
| DOC_RESULT_PUBLISHED | ||
| INTERVIEW_WAITING | ||
| FINAL_REVIEWING | ||
| FINAL_RESULT_PUBLISHED | ||
| CLOSED | ||
| } | ||
|
|
||
| enum RecruitmentPartStatus { | ||
| OPEN | ||
| CLOSED | ||
| } | ||
|
|
||
| enum RecruitmentScheduleType { | ||
| APPLY_WINDOW | ||
| DOC_REVIEW_WINDOW | ||
| DOC_RESULT_AT | ||
| INTERVIEW_WINDOW | ||
| FINAL_REVIEW_WINDOW | ||
| FINAL_RESULT_AT | ||
| OT_AT | ||
| ACTIVITY_WINDOW | ||
| } | ||
|
|
||
| enum EvaluationStage { | ||
| DOCUMENT | ||
| FINAL | ||
| } | ||
|
|
||
| enum EvaluationDecision { | ||
| PASS | ||
| FAIL | ||
| HOLD | ||
| } | ||
|
|
||
| Table question { | ||
| id bigint [pk, increment] | ||
| form_id bigint [ref: > form.id] | ||
| // text or content? | ||
| question_text varchar [note: '질문 본문'] | ||
| type QuestionType | ||
| is_required bool | ||
| order_no int | ||
|
|
||
| Note: '폼에 있는 질문 1개' | ||
| } | ||
|
|
||
| Table question_option { | ||
| id bigint [pk, increment] | ||
| question_id bigint [ref: > question.id] | ||
| content varchar | ||
| order_no int | ||
|
|
||
| Note: '객관식 유형의 질문들(단일 선택, 복수 선택 무관)의 옵션들' | ||
| } | ||
|
|
||
| Table form_response { | ||
| id bigint [pk, increment] | ||
| form_id bigint [ref: > form.id] | ||
| respondent_member_id bigint [ref: > member.id, note: '응답한 사용자 id'] | ||
| status FormResponseStatus [not null, default: 'DRAFT'] | ||
| submitted_at timestamptz | ||
| application_no varchar | ||
| submited_ip varchar | ||
|
|
||
| Note: '사용자의 응답지' | ||
| } | ||
|
|
||
| Table single_answer { | ||
| id bigint [pk, increment] | ||
| response_id bigint [ref: > form_response.id] | ||
| question_id bigint [ref: > question.id] | ||
|
|
||
| answered_as_type QuestionType [not null, note: '응답 당시 질문 타입'] | ||
|
|
||
| /* [JSONB 구조 및 규칙] | ||
| 변경 시 어떻게 할 것인지가 처리 필요 | ||
|
|
||
| 1. 주관식 (SHORT_TEXT, LONG_TEXT) | ||
| -> { "text": "답변내용" } | ||
|
|
||
| 2. 객관식 단일 (RADIO, DROPDOWN) | ||
| -> { "selectedOptionId": 123 } | ||
|
|
||
| 3. 객관식 다중 (CHECKBOX) | ||
| -> { "selectedOptionIds": [123, 124, 125] } | ||
|
|
||
| * 주의: 질문 타입에 맞지 않는 키가 존재하거나 값이 있으면 안 됨. | ||
| */ | ||
| value jsonb [not null, note: '답변 내용 전체를 JSON으로 저장'] | ||
|
|
||
| Note: '개별 질문에 대한 답변' | ||
| } | ||
|
|
||
| Table recruitment { | ||
| id bigint [pk, increment] | ||
|
|
||
| school_id bigint [ref: > school.id, note: '모집 주체(학교 단위)'] | ||
| gisu_id bigint [ref: > gisu.id] | ||
|
|
||
| title varchar | ||
| description varchar | ||
| form_id bigint [ref: > form.id, note: '이 모집에서 사용하는 폼'] | ||
|
|
||
| is_active bool [default: true] | ||
| phase RecruitmentPhase [default: 'BEFORE_APPLY', note: '대시보드 진행 단계(저장하거나 계산 가능)'] | ||
|
|
||
| Note: '모집 자체. 예: 중앙대 10기 추가모집' | ||
| } | ||
|
|
||
| Table recruitment_part { | ||
| id bigint [pk, increment] | ||
| recruitment_id bigint [ref: > recruitment.id] | ||
|
|
||
| part ChallengerPart | ||
| status RecruitmentPartStatus [default: 'OPEN', note: '파트별 모집중/마감(추가모집은 OPEN으로 전환)'] | ||
|
|
||
| Note: '파트별 모집 상태 관리' | ||
| } | ||
|
|
||
| Table recruitment_schedule { | ||
| id bigint [pk, increment] | ||
| recruitment_id bigint [ref: > recruitment.id] | ||
|
|
||
| type RecruitmentScheduleType | ||
| starts_at timestamptz | ||
| ends_at timestamptz | ||
| note varchar [note: '표시용 문구(선택)'] | ||
|
|
||
| Note: '모집 일정(활동 schedule과 분리). 단계별 윈도우/발표일 관리' | ||
| } | ||
|
|
||
| Table evaluation { | ||
| id bigint [pk, increment] | ||
| response_id bigint [ref: > form_response.id] | ||
|
|
||
| stage EvaluationStage | ||
| evaluator_member_id bigint [ref: > member.id, note: '평가자(운영진). challenger로 제한하면 challenger_id로 바꿔도 됨'] | ||
|
|
||
| score int | ||
| decision EvaluationDecision [default: 'HOLD'] | ||
| memo varchar | ||
|
|
||
| indexes { | ||
| (response_id, stage) [unique, name: 'uq_evaluation_response_stage'] | ||
| } | ||
|
|
||
| Note: '지원서 평가(서류/최종). 단일평가 가정' | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.java" | grep -i entity | head -20Repository: UMC-PRODUCT/umc-product-server
Length of output: 128
🏁 Script executed:
fd -e java -type f | grep -E "(Member|Challenger|OAuth)" | grep -v test | head -20Repository: UMC-PRODUCT/umc-product-server
Length of output: 244
🏁 Script executed:
find . -type f -name "*.java" -path "*/entity/*" -o -name "*.java" -path "*/entities/*" | head -30Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
find . -type f -name "*.java" | xargs grep -l "@Entity" | grep -v testRepository: UMC-PRODUCT/umc-product-server
Length of output: 527
🏁 Script executed:
find . -type f -name "*.java" | xargs grep -l "class Member\|class MemberOAuth\|class Challenger" | grep -v testRepository: UMC-PRODUCT/umc-product-server
Length of output: 1156
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/member/domain/Member.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 3604
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/member/domain/MemberOAuth.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 1847
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/challenger/domain/Challenger.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 2563
🏁 Script executed:
find . -type f -name "*.java" | xargs grep -l "enum MemberStatus\|enum OAuthProvider\|enum ChallengerStatus\|enum ChallengerPart" | grep -v testRepository: UMC-PRODUCT/umc-product-server
Length of output: 362
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/member/domain/enums/MemberStatus.java 2>/dev/null || find . -type f -name "MemberStatus.java" | head -1 | xargs cat -nRepository: UMC-PRODUCT/umc-product-server
Length of output: 318
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/challenger/domain/enums/ChallengerStatus.java 2>/dev/null || find . -type f -name "ChallengerStatus.java" | head -1 | xargs cat -nRepository: UMC-PRODUCT/umc-product-server
Length of output: 303
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/member/domain/OAuthProvider.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 243
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/challenger/domain/enums/ChallengerPart.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 311
🏁 Script executed:
find . -type f -name "ChallengerRole.java" -o -name "ChallengerPoint.java" | head -2 | xargs cat -nRepository: UMC-PRODUCT/umc-product-server
Length of output: 3211
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/challenger/domain/enums/PointType.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 238
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/challenger/domain/enums/RoleType.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 706
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/challenger/domain/enums/OrganizationType.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 242
DBML의 OAuthProvider 열거형과 JPA 엔티티의 불일치를 수정하세요.
DBML 스키마가 실제 구현된 JPA 엔티티와 몇 가지 불일치가 발견되었습니다:
-
OAuthProvider 열거형: DBML에는 GOOGLE, APPLE, KAKAO만 정의되어 있지만, 실제 Java 코드(
OAuthProvider.java)에는 NAVER가 추가로 존재합니다. DBML을 업데이트하여 NAVER를 추가하세요. -
ChallengerRole.leadingPart 필드: DBML에서는
[null]로 표시되어 nullable이지만, 실제 Java 엔티티(ChallengerRole.java)에서는@Column(nullable = false)로 정의되어 있습니다. DBML을 수정하거나 엔티티 정의를 재검토하여 일관성을 맞추세요. -
MemberOAuth 컬럼명: DBML에서는
provider_member_id로 표시되어 있으나, Java 엔티티(MemberOAuth.java)에서는@Column(name = "provider_id")로 정의되어 있습니다. 실제 데이터베이스 컬럼명과 DBML을 일치시키세요.
기타 주요 열거형(MemberStatus, ChallengerStatus, ChallengerPart, RoleType, PointType 등)과 핵심 엔티티 필드들은 DBML과 일치합니다.
🤖 Prompt for AI Agents
In @docs/erd.dbml around lines 1 - 660, Update the DBML to match the JPA
entities: add NAVER to the OAuthProvider enum in DBML to match
OAuthProvider.java; make challenger_role.leadingPart nullable or remove the
[null] in DBML if the JPA field ChallengerRole.leadingPart is annotated
@Column(nullable = false) (or alternatively adjust the entity to allow null) so
both definitions agree; and rename the member_oauth column provider_member_id in
the DBML to provider_id to match MemberOAuth.java (or update the entity
annotation to use provider_member_id) so the DBML column name and @Column(name =
...) are consistent.
| provider OAuthProvider [note: 'OAuth Provider'] | ||
| provider_member_id varchar [note: 'OAuth Provider의 ID'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
OAuth Provider ID 필드명을 검토하세요.
Line 42에서 provider_member_id로 명명되어 있는데, "member"라는 용어가 중의적일 수 있습니다. OAuth Provider의 사용자 ID를 의미한다면 provider_user_id 또는 oauth_user_id가 더 명확할 수 있습니다.
실제 JPA 엔티티(MemberOAuth)의 필드명과 일치하는지 확인하세요.
🤖 Prompt for AI Agents
In @docs/erd.dbml around lines 41 - 42, The column name provider_member_id is
ambiguous; rename it to match the domain and the JPA entity by using a clearer
identifier (e.g., provider_user_id or oauth_user_id) and ensure it exactly
matches the field name used in the MemberOAuth JPA entity; update the DBML
definition for the provider column (currently "provider_member_id") to the
chosen name and, if present, update any references to OAuthProvider and
MemberOAuth mappings to keep schema and entity names consistent.
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Table(name = "challenger_point") | ||
| public class ChallengerPoint { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne() | ||
| @JoinColumn(name = "challenger_id", nullable = false) | ||
| private Challenger challenger; | ||
|
|
||
| @Column(nullable = false, name = "point") | ||
| private PointType type; | ||
|
|
||
| @Column(length = 200) | ||
| private String description; | ||
|
|
||
| @Column(name = "related_workbook_id") | ||
| private Long relatedWorkbookId; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엔티티 생성을 위한 Builder 또는 생성자가 필요합니다.
현재 ChallengerPoint 엔티티에는 @NoArgsConstructor(access = PROTECTED)만 있고, 객체 생성을 위한 방법이 없습니다. @Builder 또는 정적 팩토리 메서드를 추가하여 엔티티 생성이 가능하도록 해야 합니다.
♻️ 제안하는 수정 사항
+import lombok.Builder;
+
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "challenger_point")
public class ChallengerPoint {
// ... fields ...
+
+ @Builder
+ public ChallengerPoint(Challenger challenger, PointType type, String description, Long relatedWorkbookId) {
+ this.challenger = challenger;
+ this.type = type;
+ this.description = description;
+ this.relatedWorkbookId = relatedWorkbookId;
+ }
}🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java around
lines 19 - 40, ChallengerPoint lacks a way to construct instances; add a
constructor or builder/factory so callers can create entities—either annotate
the class with Lombok's @Builder and add an all-args (or required-args)
constructor, or implement a public static factory method (e.g.,
ChallengerPoint.of(Challenger challenger, PointType type, String description,
Long relatedWorkbookId)) and a matching constructor to set fields (id remains
managed by JPA); ensure the constructor/factory assigns challenger, type,
description, and relatedWorkbookId and keeps the no-args protected constructor.
src/main/java/com/umc/product/global/config/SecurityConfig.java
Outdated
Show resolved
Hide resolved
| private static OAuth2Attributes ofGoogle(String userNameAttributeName, | ||
| Map<String, Object> attributes) { | ||
| return OAuth2Attributes.builder() | ||
| .provider(OAuthProvider.GOOGLE) | ||
| .providerId((String) attributes.get(userNameAttributeName)) | ||
| .name((String) attributes.get("name")) | ||
| .email((String) attributes.get("email")) | ||
| .nickname((String) attributes.get("name")) | ||
| .attributes(attributes) | ||
| .nameAttributeKey(userNameAttributeName) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
예외 처리 누락: 속성값 null 체크 및 타입 검증 필요
OAuth provider로부터 받은 attributes 맵에서 값을 추출할 때 null 체크와 타입 검증이 없습니다. 다음과 같은 문제가 발생할 수 있습니다:
attributes.get("name")등이 null을 반환할 경우 처리되지 않음- 예상하지 못한 타입이 반환될 경우
ClassCastException발생 가능 - 필수 필드(email 등)가 누락되어도 감지되지 않음
🔒 권장 수정안 - 안전한 속성 추출
private static OAuth2Attributes ofGoogle(String userNameAttributeName,
Map<String, Object> attributes) {
+ String email = (String) attributes.get("email");
+ if (email == null || email.isBlank()) {
+ throw new IllegalArgumentException("Google OAuth: email is required");
+ }
+
return OAuth2Attributes.builder()
.provider(OAuthProvider.GOOGLE)
.providerId((String) attributes.get(userNameAttributeName))
.name((String) attributes.get("name"))
- .email((String) attributes.get("email"))
+ .email(email)
.nickname((String) attributes.get("name"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}같은 방식으로 ofKakao(), ofNaver()에도 적용하세요.
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/global/security/oauth/OAuth2Attributes.java
around lines 31 - 42, ofGoogle currently extracts values from the attributes map
without null/type checks which can cause NPEs or ClassCastException and miss
missing required fields; update ofGoogle to validate and safely extract each
field (providerId, name, email, nickname) by checking
attributes.containsKey(...) and verifying instance types before casting, provide
sensible defaults or throw a clear IllegalArgumentException when required fields
like email are missing, and wrap any unsafe casts in safe helpers; apply the
same pattern to ofKakao and ofNaver and ensure the builder(...) call only
receives validated values and the attributes map is preserved.
| // 7. User 상태를 ACTIVE로 변경 | ||
| savedMember.activate(); | ||
|
|
||
| return savedMember.getId(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Member 엔티티 파일 찾기
find . -type f -name "Member.java" | grep -v targetRepository: UMC-PRODUCT/umc-product-server
Length of output: 131
🏁 Script executed:
# MemberCommandService 파일 확인
find . -type f -name "MemberCommandService.java" | grep -v targetRepository: UMC-PRODUCT/umc-product-server
Length of output: 166
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/member/domain/Member.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 3604
🏁 Script executed:
cat -n ./src/main/java/com/umc/product/member/application/service/command/MemberCommandService.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 4082
register() 메서드에서 activate() 후 명시적 저장 추가 필요
Line 57의 savedMember.activate() 호출 후 명시적 저장이 누락되어 있습니다. @transactional이 적용되어 있어 JPA 더티 체킹으로 작동하겠지만, 같은 클래스의 completeRegister() 메서드(line 76)에서는 상태 변경 후 saveUserPort.save(member)를 명시적으로 호출하고 있어 일관성이 부족합니다.
코드 명확성과 유지보수성을 위해 saveUserPort.save(savedMember)를 line 57 이후에 추가하기를 권장합니다.
completeRegister() 메서드의 패턴 참고
// completeRegister()에서는 명시적 저장
member.activate();
member.updateProfile(command.nickname(), command.schoolId(), command.profileImageId());
Member savedMember = saveUserPort.save(member); // 명시적 저장🤖 Prompt for AI Agents
In
@src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java
around lines 56 - 59, The register() method calls savedMember.activate() but
does not persist that state change explicitly, causing inconsistency with
completeRegister(); after savedMember.activate() call add an explicit save via
saveUserPort.save(savedMember) (matching the pattern used in completeRegister())
so the activation is persisted and the method behavior is consistent.
| member.validateIfRegisterAvailable(); | ||
|
|
||
| // 2. 이메일 중복 확인 및 이메일 인증이 완료되었는지 확인 | ||
| // TODO: 이메일 인증 로직 추후에 붙이도록 함 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
TODO 항목을 이슈로 추적하세요.
이메일 인증 로직이 누락되어 있으며 TODO로 표시되어 있습니다. 이는 보안 및 사용자 검증 측면에서 중요한 기능입니다.
이메일 인증 로직 구현을 위한 별도 이슈를 생성하시겠습니까? 구현 범위와 인증 방식(토큰 기반, OTP 등)을 제안해드릴 수 있습니다.
| @Builder | ||
| private Member(String name, String nickname, String email, Long schoolId, Long profileImageId) { | ||
| this.name = name; | ||
| this.nickname = nickname; | ||
| this.email = email; | ||
| this.schoolId = schoolId; | ||
| this.profileImageId = profileImageId; | ||
| this.status = MemberStatus.PENDING; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
입력 검증 누락: 빌더에서 필수 필드 및 유효성 검증 필요
Builder를 통해 Member 엔티티를 생성할 때 필수 필드(name, nickname, email)에 대한 검증이 없습니다. 다음과 같은 문제가 발생할 수 있습니다:
- null 값으로 생성 시도 시 DB 제약 조건 위반만으로 감지됨
- 빈 문자열이나 공백만 있는 문자열 허용
- 이메일 형식 검증 없음
- 닉네임/이름 길이 제한 없음
♻️ 권장 수정안 - 빌더에 검증 로직 추가
@Builder
private Member(String name, String nickname, String email, Long schoolId, Long profileImageId) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("이름은 필수입니다");
+ }
+ if (nickname == null || nickname.isBlank()) {
+ throw new IllegalArgumentException("닉네임은 필수입니다");
+ }
+ if (email == null || email.isBlank()) {
+ throw new IllegalArgumentException("이메일은 필수입니다");
+ }
+ // 이메일 형식 검증 (간단한 예시)
+ if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
+ throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다");
+ }
this.name = name;
this.nickname = nickname;
this.email = email;
this.schoolId = schoolId;
this.profileImageId = profileImageId;
this.status = MemberStatus.PENDING;
}코딩 가이드라인에 따라 입력값 검증을 통해 도메인 불변식을 보장해야 합니다.
📝 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.
| @Builder | |
| private Member(String name, String nickname, String email, Long schoolId, Long profileImageId) { | |
| this.name = name; | |
| this.nickname = nickname; | |
| this.email = email; | |
| this.schoolId = schoolId; | |
| this.profileImageId = profileImageId; | |
| this.status = MemberStatus.PENDING; | |
| } | |
| @Builder | |
| private Member(String name, String nickname, String email, Long schoolId, Long profileImageId) { | |
| if (name == null || name.isBlank()) { | |
| throw new IllegalArgumentException("이름은 필수입니다"); | |
| } | |
| if (nickname == null || nickname.isBlank()) { | |
| throw new IllegalArgumentException("닉네임은 필수입니다"); | |
| } | |
| if (email == null || email.isBlank()) { | |
| throw new IllegalArgumentException("이메일은 필수입니다"); | |
| } | |
| // 이메일 형식 검증 (간단한 예시) | |
| if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) { | |
| throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다"); | |
| } | |
| this.name = name; | |
| this.nickname = nickname; | |
| this.email = email; | |
| this.schoolId = schoolId; | |
| this.profileImageId = profileImageId; | |
| this.status = MemberStatus.PENDING; | |
| } |
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/Member.java around lines 49 -
57, The Member builder currently allows null/blank/invalid inputs; add
validation inside the Member construction path (e.g., in the private Member(...)
constructor invoked by the Lombok @Builder or in a single static factory called
by the builder) to enforce: non-null and non-blank for name, nickname and email
(trim inputs), length bounds for name and nickname (define and use clear min/max
constants), and a proper email format check (use a simple regex or a shared
EmailValidator utility); on failure throw a clear runtime exception such as
IllegalArgumentException with descriptive messages so invalid objects cannot be
created.
| } | ||
| } | ||
|
|
||
| // TODO: 탈퇴 및 휴면 처리에 대한 도메인 로직은 추후 추가 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
TODO 확인: 탈퇴 및 휴면 처리 로직
탈퇴 및 휴면 처리에 대한 도메인 로직이 아직 구현되지 않았습니다. 이 기능들이 필요한 시점에 다음과 같은 메서드들을 추가해야 합니다:
deactivate(): 회원 탈퇴 처리dormant(): 휴면 계정 전환- 관련 상태 검증 로직
이 기능들의 구현 코드를 생성하거나 새로운 이슈로 등록할까요?
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/Member.java at line 96, Add
domain methods in Member: implement deactivate() and dormant() to change the
member state field (e.g., status or enum) and update relevant timestamps;
validate current state inside deactivate() to prevent deactivating an already
deactivated member and inside dormant() to prevent marking an already dormant or
deactivated member (throw IllegalStateException or a domain-specific exception),
and ensure any related invariants (e.g., clear session/token fields or set
isActive=false) are enforced; also add unit tests for Member.deactivate() and
Member.dormant() to cover normal and invalid transitions, or if you prefer not
to implement now, open a new issue referencing Member, deactivate(), and
dormant() with the required validation and test tasks.
| @Column(name = "provider_id", nullable = false) | ||
| private String providerId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DB 제약 조건 누락: providerId 길이 제한 필요
providerId 컬럼에 길이 제한이 없어 DB 스키마가 불명확하고, 예상치 못한 긴 값이 입력될 경우 문제가 발생할 수 있습니다. OAuth provider마다 ID 형식이 다르지만, 일반적으로 합리적인 최대 길이를 설정하는 것이 좋습니다.
📝 권장 수정안
-@Column(name = "provider_id", nullable = false)
+@Column(name = "provider_id", nullable = false, length = 255)
private String providerId;또는 더 제한적인 길이가 필요한 경우:
-@Column(name = "provider_id", nullable = false)
+@Column(name = "provider_id", nullable = false, length = 100)
private String providerId;📝 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.
| @Column(name = "provider_id", nullable = false) | |
| private String providerId; | |
| @Column(name = "provider_id", nullable = false, length = 255) | |
| private String providerId; |
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/MemberOAuth.java around lines 38
- 39, The providerId field in MemberOAuth lacks a DB length constraint; update
the field annotation to include a reasonable max length (e.g., set @Column(name
= "provider_id", nullable = false, length = 255)) and add a matching validation
annotation (e.g., @Size(max = 255)) on the providerId field so the schema
enforces the limit and the entity validates inputs before persistence.
도메인 명 변경사항 잔재 적용
내부 구현을 Spring Security의 기본 제공 기능을 래핑하도록 변경
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 24
🤖 Fix all issues with AI agents
In @src/main/java/com/umc/product/challenger/domain/Challenger.java:
- Around line 63-67: Add explicit domain methods on the Challenger entity to
perform state transitions instead of using setters: implement methods like
graduate(), withdraw(), and expel() on the Challenger class that call the
existing validateChallengerStatus() and then set this.status to the appropriate
ChallengerStatus (e.g., ChallengerStatus.GRADUATED, ChallengerStatus.WITHDRAWN,
ChallengerStatus.EXPELLED); ensure these methods live alongside
validateChallengerStatus() and enforce the same precondition so only ACTIVE
challengers can transition.
- Around line 25-68: Add a bidirectional OneToMany for ChallengerRole to mirror
ChallengerPoint: declare a field List<ChallengerRole> challengerRoles = new
ArrayList<>(); annotated with @OneToMany(mappedBy = "challenger", fetch =
FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) so the
Challenger entity is consistent with ChallengerRole's @ManyToOne relationship
and participates in lifecycle/cascade/orphan handling; keep it initialized and
rely on the existing @Getter and no-args constructor.
In @src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java:
- Around line 30-32: The @ManyToOne on the ChallengerPoint entity’s challenger
field lacks an explicit fetch strategy; update the annotation on the
ChallengerPoint.challenger field to use FetchType.LAZY (e.g., @ManyToOne(fetch =
FetchType.LAZY)) to match ChallengerRole and prevent N+1 issues, and ensure the
FetchType import is present if needed.
- Around line 34-36: The entity ChallengerPoint defines a field named type
annotated with @Column(name = "point") causing a mismatch between the Java field
and DB column; update the mapping to be explicit and consistent by either
renaming the column to "point_type" in the @Column annotation or renaming the
field to match the existing column (e.g., change field type -> point), and
ensure the @Column(name = "...") value and the enum field PointType usage (and
any JPQL/SQL references) are updated accordingly so names remain consistent
across ChallengerPoint and database mappings.
- Around line 25-43: Add JPA-compatible constructors to ChallengerPoint: a
protected no-arg constructor for Hibernate and a public constructor that accepts
(Challenger challenger, PointType type, String description, Long
relatedWorkbookId) and sets the corresponding fields on this (id left null). In
the public constructor, also maintain the bidirectional relationship by adding
this to the challenger's collection (call challenger.addChallengerPoint(this) if
such helper exists, otherwise do challenger.getChallengerPoints().add(this)) to
keep Challenger.challengerPoints in sync.
In @src/main/java/com/umc/product/challenger/domain/ChallengerRole.java:
- Around line 29-31: The @ManyToOne on the challenger field in the
ChallengerRole entity is currently using the default EAGER fetch; change the
mapping on the ChallengerRole.challenger field to explicitly use FetchType.LAZY
(e.g., @ManyToOne(fetch = FetchType.LAZY)) to avoid unnecessary eager loading
and reduce N+1 risk, and ensure any code that needs the challenger uses fetch
joins or batch fetching in queries to load it when required.
- Around line 24-54: ChallengerRole lacks a JPA no-arg constructor plus a public
constructor and domain methods to create/manage instances; add a protected
no-arg constructor for JPA and a public constructor on ChallengerRole that
accepts the required fields (Challenger challenger, RoleType roleType,
OrganizationType organizationType, Long organizationId, Long gisuId, and
optional ChallengerPart leadingPart) and assigns/validates non-nullables, then
add domain methods such as changeRoleType(RoleType),
changeOrganization(OrganizationType, Long), changeLeadingPart(ChallengerPart)
and setGisuId(Long) to encapsulate state changes instead of exposing setters.
In @src/main/java/com/umc/product/global/config/SecurityConfig.java:
- Around line 81-87: The Javadoc for the main Security chain method in
SecurityConfig.java contains empty @param and @return tags; update the Javadoc
for that method (the main security chain method) by either adding meaningful
descriptions for the @param http (e.g., "the HttpSecurity to configure") and
@return (e.g., "the configured SecurityFilterChain") or remove the unused
@param/@return tags entirely so the comment is not empty.
- Around line 108-109: The OAuth2 endpoint patterns are hardcoded in
SecurityConfig; extract them into a constant (e.g., OAUTH2_PATHS) near the top
of the class (similar to SWAGGER_PATHS) and replace the inline strings used in
the .requestMatchers("/oauth2/authorization/**",
"/login/oauth2/code/**").permitAll() call with that constant; update any
imports/visibility as needed so SecurityConfig uses the new OAUTH2_PATHS
constant for clarity and easier maintenance.
- Around line 112-113: The filter chain currently calls
anyRequest().authenticated() in SecurityConfig which blocks endpoints that rely
on the @Public/@PreAuthorize("permitAll()") method-level annotation; change the
chain to either use anyRequest().permitAll() so all controller-level access is
enforced by method security, or explicitly permit endpoints annotated @Public by
adding specific antMatchers/RequestMatchers (e.g., "/health-check",
"/token/at/{userId}") with permitAll() in the same SecurityConfig filterChain
method; ensure method-level security remains enabled so @PreAuthorize/@Public
still protect others.
In @src/main/java/com/umc/product/global/constant/SwaggerTag.java:
- Line 23: Fix the typo in the class comment inside SwaggerTag by replacing
"Constatns" with "Constants" so the comment reads correctly (ensure the updated
sentence: "추가하는 경우, 하단의 Constants에도 반드시 동일하게 추가할 것"). Locate the comment in the
SwaggerTag class (e.g., near the top-level comment above the class declaration)
and update only the spelling.
- Around line 33-40: fromName 메서드에서 null 입력 시 35번 라인에서 NPE가 발생하므로
SwaggerTag.fromName의 시작부에 입력값 null 검증을 추가하고 null이면 IllegalArgumentException(또는
명확한 메시지를 담은 예외)으로 바꾸어 던지도록 수정하세요; 예를 들어 SwaggerTag.fromName에서 name이 null인지 확인하고
"Tag name must not be null" 같은 설명을 포함한 예외를 발생시키거나 Objects.requireNonNull을 사용해
방어코드를 추가하면 됩니다.
- Around line 56-67: Replace the hard-coded string fields in the nested
Constants class with references to the enum values so they stay in sync: update
each field in Constants (TEST, AUTH, MEMBER, CHALLENGER, ORGANIZATION,
CURRICULUM, SCHEDULE, COMMUNITY, NOTICE, ADMIN) to pull the corresponding value
from the SwaggerTag enum (e.g., use Tag.TEST.<appropriate accessor> or
Tag.TEST.name()/toString()/getLabel() depending on the enum API) instead of
repeating the literal strings, and remove or update the stale comment referenced
on line 23 accordingly.
In
@src/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.java:
- Around line 19-21: Update the outdated comment in
CurrentMemberArgumentResolver to reflect the actual annotation and principal
types: change references from "@CurrentUser" to "@CurrentMember" and from
"UserPrincipal" to "MemberPrincipal"; ensure the two comment lines that explain
checks (parameter.hasParameterAnnotation(CurrentMember.class) and parameter type
check for MemberPrincipal) match the current code and mention the exact symbols
CurrentMember and MemberPrincipal so the comments are accurate.
In
@src/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.java:
- Around line 13-22: Add Bean Validation annotations to the
CompleteRegisterMemberCommand record fields so controller-level @Valid can
automatically validate input: annotate memberId and schoolId with @NotNull, name
and nickname with @NotBlank, email with @NotBlank and @Email, and agreedTerms
with @NotEmpty (or @NotNull/@Size(min=1) as appropriate); import
javax.validation.constraints.* (or jakarta.validation.constraints.* depending on
project) and keep any existing requireNonNull checks if desired, and ensure
controllers accept the corresponding request DTO with @Valid so these
constraints are enforced.
- Line 21: CompleteRegisterMemberCommand의 agreedTerms가 비어있을 수 있으므로 빈 리스트/누락 동의
방지를 추가하세요; 예를 들어 DTO/command 필드에 Bean Validation 어노테이션(@NotEmpty 또는
@Size(min=1))을 붙이거나 Compact Constructor(또는 생성자)에서 agreedTerms가 null이거나 isEmpty인
경우 IllegalArgumentException을 던지도록 검증 로직을 추가하고, 필수 약관 검증이 필요하면 TermType 열거값을 검사해
최소 필수 항목이 포함되어 있는지도 확인하세요.
In
@src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java:
- Around line 45-60: After calling savedMember.activate() in
MemberCommandService.completeRegister(), explicitly persist the state change by
invoking saveMemberPort.save(savedMember) (same save method used earlier) so the
ACTIVE status is stored immediately and matches the explicit-save pattern used
elsewhere (e.g., completeRegister()). This makes intent clear and avoids relying
solely on JPA dirty-checking.
In @src/main/java/com/umc/product/member/domain/Member.java:
- Around line 67-79: The updateProfile method in Member currently updates
nickname, schoolId, and profileImageId but does not allow changing email;
annotate this intent by adding a Javadoc comment to Member.updateProfile that
explicitly states email is immutable (or rename the method to something clearer
like updateProfileExceptEmail/updateMutableProfile) and update any callers or
documentation to reflect the non-updatable email policy so the behavior is
unambiguous.
In @src/main/java/com/umc/product/member/domain/MemberOAuth.java:
- Around line 34-39: Extract the hardcoded column length literals in MemberOAuth
into named constants (e.g., PROVIDER_LENGTH and PROVIDER_ID_LENGTH) and replace
the numeric literals used in the @Column annotations for provider and providerId
with those constants; also update any validation logic in the MemberOAuth
builder/constructor that checks lengths to reuse the same constants so the
constraints remain consistent and maintainable.
- Around line 41-46: The builder constructor MemberOAuth(Long memberId,
OAuthProvider provider, String providerId) lacks input validation; update that
@Builder private MemberOAuth(...) to validate memberId, provider and providerId
are non-null, ensure providerId is not blank and its length <= 512, and throw a
suitable exception (e.g., IllegalArgumentException or a project-specific
InvalidMemberOAuthException) with clear messages when validations fail so the
entity cannot be constructed in an invalid state.
- Around line 31-32: MemberOAuth intentionally stores memberId as a Long to
avoid JPA coupling; ensure you add a DB index on the member_id column via Flyway
migration to avoid query performance issues when fetching OAuth entries by
memberId — create a migration that adds an index on member_id (referencing the
member_id column in the MemberOAuth table) and verify the migration is applied
in tests/CI.
In @src/main/resources/logback-spring.xml:
- Line 20: The console log pattern truncates traceId to 4 chars via the pattern
token "%-4.4X{traceId}", breaking distributed tracing; update the pattern in the
logback-spring.xml pattern element to preserve more or the full traceId (e.g.
use "%X{traceId}" to print the entire ID or "%-16.16X{traceId}" to keep a
16-char view) so console output matches the FILE appender and maintains
consistent, traceable IDs.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (22)
scripts/application_start-dev.shscripts/application_stop-dev.shscripts/application_stop-prod.shscripts/before_block_traffic.shscripts/before_install.shscripts/validate_service.shsrc/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.javasrc/main/java/com/umc/product/global/config/SecurityConfig.javasrc/main/java/com/umc/product/global/config/WebMvcConfig.javasrc/main/java/com/umc/product/global/constant/SwaggerTag.javasrc/main/java/com/umc/product/global/security/CustomAuthorizationManager.javasrc/main/java/com/umc/product/global/security/annotation/Public.javasrc/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.javasrc/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.javasrc/main/java/com/umc/product/member/application/service/command/MemberCommandService.javasrc/main/java/com/umc/product/member/domain/Member.javasrc/main/java/com/umc/product/member/domain/MemberOAuth.javasrc/main/resources/application.ymlsrc/main/resources/logback-spring.xmlsrc/test/java/com/umc/product/UmcProductApplicationTests.java
💤 Files with no reviewable changes (8)
- src/test/java/com/umc/product/UmcProductApplicationTests.java
- scripts/before_install.sh
- scripts/application_stop-prod.sh
- scripts/application_start-dev.sh
- scripts/before_block_traffic.sh
- scripts/application_stop-dev.sh
- scripts/validate_service.sh
- src/main/java/com/umc/product/global/security/CustomAuthorizationManager.java
🧰 Additional context used
📓 Path-based instructions (7)
src/**
⚙️ CodeRabbit configuration file
src/**: 다음 항목들을 꼼꼼하게 검토해줘.
- 예외 처리
- 예외가 적절히 처리되었는지 확인해줘. (try-catch, throws, ExceptionAdvice)
- 공통 예외 처리 모듈(예: GlobalHandler, ApiResponse 등)을 잘 활용했는지 확인.
- RuntimeException을 남발하지 않고, 의미 있는 커스텀 예외를 사용하는지 검토.
- 예외 메시지에 민감 정보(DB 정보, 사용자 정보 등)가 노출되지 않게 했는지 점검.
- 코드 품질 & 가독성
- 메소드/클래스가 단일 책임 원칙(SRP)에 맞게 구성되어 있는지.
- 중복 코드가 있는 경우, 유틸/공통 컴포넌트로 추출 가능한지.
- 의미 있는 변수명과 메소드명을 사용했는지.
- 매직 넘버, 하드코딩된 값이 존재하는지 점검.
- 성능 및 효율성
- 불필요한 DB 쿼리 호출, N+1 문제 가능성이 있는지 확인.
- Stream, loop, recursion 사용 시 시간복잡도/메모리 효율성을 고려했는지.
- 캐시 적용 가능성이 있거나, 과도한 연산이 반복되는 구간이 있는지.
- 트랜잭션 관리
- @transactional이 필요한 메소드에 누락되지 않았는지.
- 읽기 전용 트랜잭션(readOnly = true)을 적절히 사용했는지.
- DB 일관성, 롤백 정책이 올바른지 검토.
- 입력 검증 및 보안
- @Valid, Bean Validation 등을 통한 입력값 검증이 되어 있는지.
- 비밀번호, 토큰 등 민감한 정보가 로깅되지 않는지.
- 테스트
- 단위 테스트가 충분히 작성되었는지, 핵심 로직의 검증이 누락되지 않았는지.
- Mocking을 통한 독립 테스트 구조를 유지했는지.
- 경계값 테스트, 예외 케이스 테스트가 포함되어 있는지.
- 구조 및 설계
- Controller, Service, Repository 등 계층 구조가 올바르게 나뉘어 있는지.
- DTO, Entity, Domain 객체 간 변환 로직이 명확하고 중복되지 않는지.
- Config 클래스에서 Bean 등록이 과도하거나 순환 참조 위험이 없는지.
Files:
src/main/resources/application.ymlsrc/main/resources/logback-spring.xmlsrc/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.javasrc/main/java/com/umc/product/member/application/service/command/MemberCommandService.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.javasrc/main/java/com/umc/product/member/domain/Member.javasrc/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.javasrc/main/java/com/umc/product/global/constant/SwaggerTag.javasrc/main/java/com/umc/product/global/config/SecurityConfig.javasrc/main/java/com/umc/product/global/config/WebMvcConfig.javasrc/main/java/com/umc/product/global/security/annotation/Public.javasrc/main/java/com/umc/product/member/domain/MemberOAuth.java
**/*.java
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Package structure must follow hexagonal architecture with domain/, application/{port/in, port/out, service}/, and adapter/{in, out}/ directories
Files:
src/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.javasrc/main/java/com/umc/product/member/application/service/command/MemberCommandService.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.javasrc/main/java/com/umc/product/member/domain/Member.javasrc/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.javasrc/main/java/com/umc/product/global/constant/SwaggerTag.javasrc/main/java/com/umc/product/global/config/SecurityConfig.javasrc/main/java/com/umc/product/global/config/WebMvcConfig.javasrc/main/java/com/umc/product/global/security/annotation/Public.javasrc/main/java/com/umc/product/member/domain/MemberOAuth.java
**/domain/**/*.java
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
State changes in Entity classes must be performed through domain methods, not via setters
Files:
src/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.javasrc/main/java/com/umc/product/member/domain/Member.javasrc/main/java/com/umc/product/member/domain/MemberOAuth.java
**/*application/port/in/**/*{Command,Query}.java
📄 CodeRabbit inference engine (CLAUDE.md)
Command and Query records should be immutable with @NotNull validation and include conversion method toCommand() or toEntity()
Files:
src/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.java
**/*application/service/**/*Service.java
📄 CodeRabbit inference engine (CLAUDE.md)
**/*application/service/**/*Service.java: Service classes implementing usecases must be in application/service/{command|query}/ packages; command services use {Domain}CommandService, query services use {Domain}QueryService
Command services must use @transactional annotation; Query services must use @transactional(readOnly = true)
Files:
src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java
**/*global/security/**/*.java
📄 CodeRabbit inference engine (CLAUDE.md)
Security configuration must use JWT (io.jsonwebtoken 0.12.5) with JwtTokenProvider, JwtAuthenticationFilter in global/security/ package; use @public and @currentuser annotations for method security
Files:
src/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.javasrc/main/java/com/umc/product/global/security/annotation/Public.java
**/*global/config/**Config.java
📄 CodeRabbit inference engine (CLAUDE.md)
Spring configuration files must be in global/config/ package and use @configuration annotation
Files:
src/main/java/com/umc/product/global/config/SecurityConfig.javasrc/main/java/com/umc/product/global/config/WebMvcConfig.java
🧠 Learnings (20)
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to application*.{yml,yaml,properties} : JPA configuration must use OSIV=false and DDL mode=validate to prevent schema auto-creation and enforce Flyway-based migrations
Applied to files:
src/main/resources/application.yml
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*domain/*Entity.java : Use enum types for status and type fields with Enumerated(EnumType.STRING) annotation
Applied to files:
src/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*domain/*Entity.java : Entity field access should use NoArgsConstructor(access = AccessLevel.PROTECTED) and Getter, never use Setter
Applied to files:
src/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.java
📚 Learning: 2026-01-04T08:22:39.486Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-04T08:22:39.486Z
Learning: Applies to **/domain/**/*Entity.java : Entity classes must use NoArgsConstructor(access = AccessLevel.PROTECTED), not Setter, and reference only IDs of other domains instead of direct entity references
Applied to files:
src/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.java
📚 Learning: 2026-01-04T08:22:39.486Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-04T08:22:39.486Z
Learning: Applies to **/domain/**/*{Status,Type,State,Kind}.java : Enum classes should follow {Domain}{Type} naming pattern (e.g., ChallengerStatus.java)
Applied to files:
src/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*domain/*{Status,Type}.java : Enum naming convention: {Domain}{Status|Type} (e.g., ChallengerStatus, ScheduleType)
Applied to files:
src/main/java/com/umc/product/challenger/domain/Challenger.javasrc/main/java/com/umc/product/global/constant/SwaggerTag.java
📚 Learning: 2026-01-04T08:22:39.486Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-04T08:22:39.486Z
Learning: Applies to **/application/port/in/**Command.java : UseCase Command parameter objects should be defined as records, not plain classes
Applied to files:
src/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*application/port/in/**Command.java : Command DTO naming pattern: {Action}{Domain}Command; must be immutable records with validation
Applied to files:
src/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.javasrc/main/java/com/umc/product/member/application/service/command/MemberCommandService.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*application/port/in/**/*{Command,Query}.java : Command and Query records should be immutable with NotNull validation and include conversion method toCommand() or toEntity()
Applied to files:
src/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*adapter/in/web/dto/request/**Request.java : Request DTOs must include Valid, NotNull, and NotBlank validation annotations; include toCommand() conversion method to command record
Applied to files:
src/main/java/com/umc/product/member/application/port/in/command/CompleteRegisterMemberCommand.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*application/service/**/*Service.java : Service classes implementing usecases must be in application/service/{command|query}/ packages; command services use {Domain}CommandService, query services use {Domain}QueryService
Applied to files:
src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*application/service/**/*Service.java : Command services must use Transactional annotation; Query services must use Transactional(readOnly = true)
Applied to files:
src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Cross-domain Entity references are forbidden; only store ID fields (Column private Long {domain}Id), access other domains via UseCase interfaces
Applied to files:
src/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.java
📚 Learning: 2026-01-04T08:22:39.486Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-04T08:22:39.486Z
Learning: Avoid N+1 query problems by using Fetch Join in JPQL queries or loading related entities in separate batch queries
Applied to files:
src/main/java/com/umc/product/challenger/domain/ChallengerPoint.javasrc/main/java/com/umc/product/challenger/domain/ChallengerRole.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*global/security/**/*.java : Security configuration must use JWT (io.jsonwebtoken 0.12.5) with JwtTokenProvider, JwtAuthenticationFilter in global/security/ package; use Public and CurrentUser annotations for method security
Applied to files:
src/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.javasrc/main/java/com/umc/product/global/config/SecurityConfig.javasrc/main/java/com/umc/product/global/security/annotation/Public.java
📚 Learning: 2026-01-04T08:22:39.486Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-04T08:22:39.486Z
Learning: Applies to **/adapter/in/**Controller.java : Authentication information must be obtained from AuthenticationPrincipal SecurityUser parameter, not from Request body
Applied to files:
src/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*adapter/in/web/**Controller.java : All public API endpoints must include OpenAPI/Swagger documentation with Tag and Operation annotations
Applied to files:
src/main/java/com/umc/product/global/constant/SwaggerTag.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*Test.java : Test method naming must be descriptive in Korean or English with underscores (e.g., 챌린저_등록_성공, user_registration_fails_for_duplicate_email)
Applied to files:
src/main/java/com/umc/product/global/constant/SwaggerTag.java
📚 Learning: 2026-01-04T08:22:39.486Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-04T08:22:39.486Z
Learning: Applies to **/*Test.java : Test methods must use descriptive Korean names following {behavior}_{condition}_{result} pattern for clarity
Applied to files:
src/main/java/com/umc/product/global/constant/SwaggerTag.java
📚 Learning: 2026-01-04T08:23:08.581Z
Learnt from: CR
Repo: UMC-PRODUCT/umc-product-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-04T08:23:08.581Z
Learning: Applies to **/*adapter/in/web/**Controller.java : Controller methods must use RequestMapping with API version path (/api/v1/{domain}), include Operation and Tag for OpenAPI/Swagger documentation
Applied to files:
src/main/java/com/umc/product/global/config/SecurityConfig.java
🧬 Code graph analysis (5)
src/main/java/com/umc/product/challenger/domain/Challenger.java (3)
src/main/java/com/umc/product/challenger/domain/exception/ChallengerDomainException.java (1)
ChallengerDomainException(6-12)src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java (1)
Entity(21-43)src/main/java/com/umc/product/challenger/domain/ChallengerRole.java (1)
Entity(20-54)
src/main/java/com/umc/product/member/application/service/command/MemberCommandService.java (3)
src/main/java/com/umc/product/member/domain/exception/MemberDomainException.java (1)
MemberDomainException(6-12)src/main/java/com/umc/product/temp/controller/TempController.java (1)
RequiredArgsConstructor(17-44)src/main/java/com/umc/product/member/application/service/query/MemberQueryService.java (1)
Service(14-44)
src/main/java/com/umc/product/challenger/domain/ChallengerRole.java (4)
src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java (1)
Entity(21-43)src/main/java/com/umc/product/member/domain/Member.java (1)
Entity(20-97)src/main/java/com/umc/product/member/domain/MemberOAuth.java (1)
Entity(18-47)src/main/java/com/umc/product/challenger/domain/Challenger.java (1)
Entity(25-68)
src/main/java/com/umc/product/member/domain/Member.java (1)
src/main/java/com/umc/product/member/domain/exception/MemberDomainException.java (1)
MemberDomainException(6-12)
src/main/java/com/umc/product/global/constant/SwaggerTag.java (1)
src/main/java/com/umc/product/temp/controller/TempController.java (1)
RequiredArgsConstructor(17-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Agent
- GitHub Check: CodeQL analysis (java)
🔇 Additional comments (12)
src/main/resources/application.yml (2)
25-27: Docker Compose 설정의 위치를 확인해주세요.
spring.docker.compose.enabled설정이 기본application.yml에 위치하고 있어 모든 프로파일에 영향을 줍니다. 주석에는 "Local DB를 위한 Compose"라고 명시되어 있는데, 만약 로컬 개발 환경에만 적용하려는 의도라면application-local.yml로 이동하는 것이 더 명확합니다.현재 구조에서는 다른 프로파일(dev, prod 등)에서 Docker Compose를 사용하려면 각각 오버라이드해야 합니다. 의도한 동작이 맞는지 확인 부탁드립니다.
53-56: JPA 설정이 권장 사항을 잘 따르고 있습니다.
ddl-auto: validate와open-in-view: false설정이 올바르게 적용되어 있습니다. 이는 Flyway 기반 마이그레이션을 강제하고 스키마 자동 생성을 방지하는 모범 사례를 따릅니다.Based on learnings, JPA configuration must use OSIV=false and DDL mode=validate to enforce migration-based schema management.
src/main/resources/logback-spring.xml (1)
26-26: base.xml 주석 처리에 대한 의도 확인 필요
base.xml을 주석 처리한 것이 의도적인 변경인지 확인이 필요합니다:
base.xml은 Spring Boot의 기본 console/file appender를 포함하므로, 커스텀 appender(FILE, APP_CONSOLE)와 충돌을 피하기 위해 주석 처리한 것으로 보입니다defaults.xml(line 25)은 유지되어 있어 기본 속성들은 사용 가능합니다- 이 방식이 맞다면 문제없지만, 의도와 다르게 주석 처리된 것이라면 로그 설정이 예상과 다르게 동작할 수 있습니다
현재 설정이 의도한 대로 동작하는지 확인 부탁드립니다.
src/main/java/com/umc/product/global/constant/SwaggerTag.java (1)
12-19: Order 값 중복 확인 필요AUTH, CHALLENGER, ORGANIZATION, CURRICULUM, SCHEDULE, COMMUNITY, NOTICE가 모두 동일한
order=2값을 가지고 있습니다. Swagger UI에서 표시 순서를 제어하기 위한 필드인데, 동일한 값을 가지면 의도한 순서대로 표시되지 않을 수 있습니다. 의도적인 설계인지 확인해 주세요.src/main/java/com/umc/product/member/domain/Member.java (2)
49-57: LGTM! 도메인 객체 생성 시 기본 상태 설정이 명확합니다.Builder 패턴을 사용하여 Member 엔티티를 생성하고, 기본 상태를
PENDING으로 설정하는 로직이 적절합니다. 이는 회원가입 플로우에서 초기 상태를 명확하게 정의하고 있습니다.
59-65: LGTM! 상태 전환 검증 로직이 안전합니다.
activate()메서드가PENDING상태에서만ACTIVE로 전환할 수 있도록 검증하고 있어, 잘못된 상태 전환을 방지합니다. 도메인 예외를 통해 명확한 에러 메시지를 제공하는 점도 좋습니다.src/main/java/com/umc/product/global/config/SecurityConfig.java (2)
54-56: OAuth2 의존성 주입이 올바르게 구성되었습니다.OAuth2 인증 플로우에 필요한 세 가지 핵심 컴포넌트(UserService, SuccessHandler, FailureHandler)가 final 필드로 선언되어 생성자 주입을 통해 안전하게 관리되고 있습니다.
98-104: OAuth2 로그인 설정이 적절하게 구성되었습니다.userInfoEndpoint, successHandler, failureHandler가 모두 올바르게 연결되어 있으며, Spring Security의 OAuth2 클라이언트 플로우가 정상적으로 작동할 수 있는 구조입니다.
src/main/java/com/umc/product/global/security/annotation/Public.java (1)
7-31:@Public어노테이션의 메타 어노테이션 구현이 우수합니다.
@PreAuthorize("permitAll()")을 메타 어노테이션으로 사용하는 접근 방식은 Spring Security의 모범 사례를 따르고 있으며, 코드의 가독성과 유지보수성을 크게 향상시킵니다. Javadoc도 명확한 사용 예시와 함께 잘 작성되었습니다.단, SecurityConfig.java의
anyRequest().authenticated()설정과의 상호작용을 검증해야 합니다 (별도 코멘트 참조).src/main/java/com/umc/product/global/config/WebMvcConfig.java (1)
3-19: Argument Resolver 리팩토링이 올바르게 적용되었습니다.
CurrentUserArgumentResolver에서CurrentMemberArgumentResolver로의 이름 변경이 일관성 있게 적용되었으며, 의존성 주입 및 등록 로직도 정확합니다.src/main/java/com/umc/product/global/security/resolver/CurrentMemberArgumentResolver.java (1)
27-46: 인증 정보 처리 로직이 안전하게 구현되었습니다.
resolveArgument메서드가 null 인증, 익명 사용자, 타입 검증을 모두 적절하게 처리하고 있으며, 예외 상황에서 안전하게 null을 반환합니다.src/main/java/com/umc/product/member/domain/MemberOAuth.java (1)
18-25: 엔티티 설계가 잘 되어 있습니다.
- 유니크 제약조건
uk_member_oauth_provider_provider_id가(oauth_provider, provider_id)에 적절히 설정되어 중복 OAuth 계정 생성을 방지합니다.@NoArgsConstructor(access = AccessLevel.PROTECTED)와 setter 없이 빌더 패턴을 사용하여 불변성을 보장하는 설계가 좋습니다.- 헥사고날 아키텍처의 도메인 계층에 올바르게 위치해 있습니다.
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Table(name = "challenger") | ||
| public class Challenger extends BaseEntity { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(nullable = false, name = "member_id") | ||
| private Long memberId; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, name = "part") | ||
| private ChallengerPart part; | ||
|
|
||
| @Column(nullable = false, name = "gisu_id") | ||
| private Long gisuId; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, name = "status") | ||
| private ChallengerStatus status; | ||
|
|
||
| @OneToMany( | ||
| mappedBy = "challenger", | ||
| fetch = FetchType.LAZY, | ||
| cascade = CascadeType.ALL, | ||
| orphanRemoval = true | ||
| ) | ||
| private List<ChallengerPoint> challengerPoints = new ArrayList<>(); | ||
|
|
||
| public Challenger(Long memberId, ChallengerPart part, Long gisuId) { | ||
| this.memberId = memberId; | ||
| this.part = part; | ||
| this.gisuId = gisuId; | ||
| this.status = ChallengerStatus.ACTIVE; | ||
| } | ||
|
|
||
| public void validateChallengerStatus() { | ||
| if (this.status != ChallengerStatus.ACTIVE) { | ||
| throw new ChallengerDomainException(ChallengerErrorCode.CHALLENGER_NOT_ACTIVE); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
# Find ChallengerRole class definition and its relationship to Challenger
find . -name "ChallengerRole.java" -type fRepository: UMC-PRODUCT/umc-product-server
Length of output: 143
🏁 Script executed:
# Search for ChallengerRole class and its @ManyToOne annotation
rg -n "class ChallengerRole" -A 30 --type=javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 3106
🏁 Script executed:
# Check how ChallengerRole is used and referenced
rg -n "@ManyToOne.*Challenger|Challenger.*@OneToMany" -B 2 -A 2 --type=javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Verify ChallengerPart enum naming convention
rg -n "enum ChallengerPart" -A 5 --type=javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 611
🏁 Script executed:
# Check if Challenger has any other collections besides challengerPoints
rg -n "class Challenger" -A 50 --type=java | head -60Repository: UMC-PRODUCT/umc-product-server
Length of output: 5810
🏁 Script executed:
cat -n src/main/java/com/umc/product/challenger/domain/ChallengerRole.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 2069
🏁 Script executed:
# Check if there are other entities with @OneToMany bidirectional relationships
rg -n "@OneToMany.*mappedBy" --type=java -B 2 -A 2 | head -30Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
엔티티 설계가 정확히 구현되어 있습니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)와@Getter사용으로 setter 없는 패턴 준수memberId,gisuId를 ID 참조로 저장하여 도메인 간 직접 참조 방지@Enumerated(EnumType.STRING)사용- 도메인 로직
validateChallengerStatus()구현
ChallengerRole이 @ManyToOne으로 Challenger를 참조하고 있으나, Challenger에는 ChallengerRole에 대한 @OneToMany 관계가 없습니다. ChallengerPoint와 동일하게 양방향 관계를 설정하는 것이 일관성 있을 수 있습니다.
@OneToMany(mappedBy = "challenger", ...)
private List<ChallengerRole> challengerRoles = new ArrayList<>();🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/Challenger.java around lines
25 - 68, Add a bidirectional OneToMany for ChallengerRole to mirror
ChallengerPoint: declare a field List<ChallengerRole> challengerRoles = new
ArrayList<>(); annotated with @OneToMany(mappedBy = "challenger", fetch =
FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) so the
Challenger entity is consistent with ChallengerRole's @ManyToOne relationship
and participates in lifecycle/cascade/orphan handling; keep it initialized and
rely on the existing @Getter and no-args constructor.
| public void validateChallengerStatus() { | ||
| if (this.status != ChallengerStatus.ACTIVE) { | ||
| throw new ChallengerDomainException(ChallengerErrorCode.CHALLENGER_NOT_ACTIVE); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
상태 전환을 위한 도메인 메서드 추가를 고려하세요.
ChallengerStatus에 ACTIVE, GRADUATED, EXPELLED, WITHDRAWN 상태가 있지만, 상태 전환 메서드가 없습니다. 코딩 가이드라인에 따르면 Entity 클래스의 상태 변경은 setter가 아닌 도메인 메서드를 통해 이루어져야 합니다.
♻️ 상태 전환 메서드 예시
public void graduate() {
validateChallengerStatus();
this.status = ChallengerStatus.GRADUATED;
}
public void withdraw() {
validateChallengerStatus();
this.status = ChallengerStatus.WITHDRAWN;
}🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/Challenger.java around lines
63 - 67, Add explicit domain methods on the Challenger entity to perform state
transitions instead of using setters: implement methods like graduate(),
withdraw(), and expel() on the Challenger class that call the existing
validateChallengerStatus() and then set this.status to the appropriate
ChallengerStatus (e.g., ChallengerStatus.GRADUATED, ChallengerStatus.WITHDRAWN,
ChallengerStatus.EXPELLED); ensure these methods live alongside
validateChallengerStatus() and enforce the same precondition so only ACTIVE
challengers can transition.
| public class ChallengerPoint { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne() | ||
| @JoinColumn(name = "challenger_id", nullable = false) | ||
| private Challenger challenger; | ||
|
|
||
| @Column(nullable = false, name = "point") | ||
| @Enumerated(EnumType.STRING) | ||
| private PointType type; | ||
|
|
||
| @Column(length = 200) | ||
| private String description; | ||
|
|
||
| @Column(name = "related_workbook_id") | ||
| private Long relatedWorkbookId; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엔티티 생성을 위한 생성자가 누락되었습니다.
ChallengerPoint 엔티티에 인스턴스를 생성할 수 있는 public 생성자가 없습니다. Challenger.challengerPoints에 포인트를 추가하려면 생성자가 필요합니다.
🔧 생성자 추가 제안
@Column(name = "related_workbook_id")
private Long relatedWorkbookId;
+
+ public ChallengerPoint(Challenger challenger, PointType type,
+ String description, Long relatedWorkbookId) {
+ this.challenger = challenger;
+ this.type = type;
+ this.description = description;
+ this.relatedWorkbookId = relatedWorkbookId;
+ }
}🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java around
lines 25 - 43, Add JPA-compatible constructors to ChallengerPoint: a protected
no-arg constructor for Hibernate and a public constructor that accepts
(Challenger challenger, PointType type, String description, Long
relatedWorkbookId) and sets the corresponding fields on this (id left null). In
the public constructor, also maintain the bidirectional relationship by adding
this to the challenger's collection (call challenger.addChallengerPoint(this) if
such helper exists, otherwise do challenger.getChallengerPoints().add(this)) to
keep Challenger.challengerPoints in sync.
| @ManyToOne() | ||
| @JoinColumn(name = "challenger_id", nullable = false) | ||
| private Challenger challenger; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
@ManyToOne 관계에 FetchType.LAZY를 명시하세요.
ChallengerRole과 동일하게 N+1 문제 방지를 위해 FetchType.LAZY를 지정해야 합니다.
♻️ FetchType.LAZY 적용
- @ManyToOne()
+ @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "challenger_id", nullable = false)
private Challenger challenger;🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java around
lines 30 - 32, The @ManyToOne on the ChallengerPoint entity’s challenger field
lacks an explicit fetch strategy; update the annotation on the
ChallengerPoint.challenger field to use FetchType.LAZY (e.g., @ManyToOne(fetch =
FetchType.LAZY)) to match ChallengerRole and prevent N+1 issues, and ensure the
FetchType import is present if needed.
| @Column(nullable = false, name = "point") | ||
| @Enumerated(EnumType.STRING) | ||
| private PointType type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
컬럼명과 필드명 불일치를 확인하세요.
컬럼명이 "point"이지만 필드명은 type입니다. 이는 혼란을 야기할 수 있습니다. 컬럼명을 "point_type"으로 변경하거나 필드명과 일치시키는 것이 좋습니다.
♻️ 컬럼명 수정 제안
- @Column(nullable = false, name = "point")
+ @Column(nullable = false, name = "point_type")
@Enumerated(EnumType.STRING)
private PointType type;🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/challenger/domain/ChallengerPoint.java around
lines 34 - 36, The entity ChallengerPoint defines a field named type annotated
with @Column(name = "point") causing a mismatch between the Java field and DB
column; update the mapping to be explicit and consistent by either renaming the
column to "point_type" in the @Column annotation or renaming the field to match
the existing column (e.g., change field type -> point), and ensure the
@Column(name = "...") value and the enum field PointType usage (and any JPQL/SQL
references) are updated accordingly so names remain consistent across
ChallengerPoint and database mappings.
| // Domain Logic: 프로필 업데이트 | ||
| public void updateProfile(String nickname, Long schoolId, Long profileImageId) { | ||
| validateActive(); | ||
| if (nickname != null) { | ||
| this.nickname = nickname; | ||
| } | ||
| if (schoolId != null) { | ||
| this.schoolId = schoolId; | ||
| } | ||
| if (profileImageId != null) { | ||
| this.profileImageId = profileImageId; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
프로필 업데이트 메서드에서 이메일 업데이트가 불가능함을 명시하세요.
updateProfile() 메서드는 nickname, schoolId, profileImageId만 업데이트하며 email은 업데이트하지 않습니다. 이는 의도적인 설계로 보이지만(이메일은 변경 불가), 메서드 문서나 메서드명에서 이를 더 명확히 하면 좋겠습니다.
♻️ 제안하는 개선 방법
Javadoc 추가:
- // Domain Logic: 프로필 업데이트
+ /**
+ * Domain Logic: 프로필 업데이트
+ * <p>
+ * 닉네임, 학교, 프로필 이미지만 업데이트 가능합니다.
+ * 이메일은 불변이므로 변경할 수 없습니다.
+ * </p>
+ */
public void updateProfile(String nickname, Long schoolId, Long profileImageId) {또는 더 명확한 메서드명:
- public void updateProfile(String nickname, Long schoolId, Long profileImageId) {
+ public void updateProfileWithoutEmail(String nickname, Long schoolId, Long profileImageId) {🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/Member.java around lines 67 -
79, The updateProfile method in Member currently updates nickname, schoolId, and
profileImageId but does not allow changing email; annotate this intent by adding
a Javadoc comment to Member.updateProfile that explicitly states email is
immutable (or rename the method to something clearer like
updateProfileExceptEmail/updateMutableProfile) and update any callers or
documentation to reflect the non-updatable email policy so the behavior is
unambiguous.
| @Column(name = "member_id", nullable = false) | ||
| private Long memberId; // ID 참조만 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, find and examine the MemberOAuth.java file
find . -name "MemberOAuth.java" -type fRepository: UMC-PRODUCT/umc-product-server
Length of output: 136
🏁 Script executed:
# Check the actual content of MemberOAuth.java around lines 31-32
cat -n src/main/java/com/umc/product/member/domain/MemberOAuth.java | sed -n '25,40p'Repository: UMC-PRODUCT/umc-product-server
Length of output: 643
🏁 Script executed:
# Check MemberTermAgreement.java for the similar pattern
find . -name "MemberTermAgreement.java" -type fRepository: UMC-PRODUCT/umc-product-server
Length of output: 144
🏁 Script executed:
# Search for FK constraints and indexes in migration/schema files
find . -type f \( -name "*.sql" -o -name "*migration*" -o -name "*schema*" \) | head -20Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Search for all Long memberId fields in domain entities
rg -n 'private Long memberId' src/main/java/com/umc/product/member/domain/ -A2 -B2Repository: UMC-PRODUCT/umc-product-server
Length of output: 1002
🏁 Script executed:
# Check for @ManyToOne or @JoinColumn usage in member domain classes
rg -n '@ManyToOne|@JoinColumn' src/main/java/com/umc/product/member/domain/ -B3 -A3Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Check for @ManyToOne or @JoinColumn usage in member domain classes
rg -n '@ManyToOne|@JoinColumn' src/main/java/com/umc/product/member/ -B3 -A3Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Look for database schema files or SQL scripts
fd -e sql -e ddl -e yaml -e yml -e properties src/main/resources/Repository: UMC-PRODUCT/umc-product-server
Length of output: 455
🏁 Script executed:
# Check Member.java to understand the entity structure
cat -n src/main/java/com/umc/product/member/domain/Member.java | head -80Repository: UMC-PRODUCT/umc-product-server
Length of output: 3041
🏁 Script executed:
# Check if there are any foreign key constraints defined in the codebase
rg -i 'foreign key|constraint|references|fk_' src/main/Repository: UMC-PRODUCT/umc-product-server
Length of output: 1847
🏁 Script executed:
# Examine the domain package structure to understand architecture
find src/main/java/com/umc/product/member -type d | sortRepository: UMC-PRODUCT/umc-product-server
Length of output: 1287
🏁 Script executed:
# Check for any tests related to MemberOAuth to understand expected behavior
find . -path "*/test*" -name "*MemberOAuth*" -o -path "*/test*" -name "*Member*Test*" | grep -i oauthRepository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Search for database schema files more broadly
find src/main/resources -type f \( -name "*.sql" -o -name "*.ddl" \) 2>/dev/nullRepository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Check if there's a Flyway or Liquibase migration folder
find . -path "*/db/migration*" -o -path "*/liquibase*" -o -path "*/flyway*" 2>/dev/null | head -20Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Look for schema definition in application.yml or application.properties
fd . src/main/resources -e yml -e yaml -e properties | xargs grep -l "schema\|datasource\|jpa\|hibernate" 2>/dev/nullRepository: UMC-PRODUCT/umc-product-server
Length of output: 108
🏁 Script executed:
# Check MemberOAuth.java completely for any relationship configurations
cat -n src/main/java/com/umc/product/member/domain/MemberOAuth.javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 1861
🏁 Script executed:
# Check for any other entities that DO use @ManyToOne to compare
rg -n '@ManyToOne' src/main/java --type=java -B5 -A5Repository: UMC-PRODUCT/umc-product-server
Length of output: 2264
🏁 Script executed:
# Verify the coding guidelines match the hexagonal architecture
find . -name "README*" -o -name "ARCHITECTURE*" -o -name "CODING*" | head -10Repository: UMC-PRODUCT/umc-product-server
Length of output: 85
🏁 Script executed:
# Check application.yml for JPA/Hibernate configuration
cat src/main/resources/application.yml | grep -A 20 "jpa:\|hibernate:"Repository: UMC-PRODUCT/umc-product-server
Length of output: 980
🏁 Script executed:
# Check if there's any documentation about the ID reference pattern
rg -i "id.*reference|foreign key|hexagonal|domain isolation" src/main/java --type=java | head -20Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Look for Repository classes to see how queries are made with memberId
rg -n 'memberId' src/main/java/com/umc/product/member/adapter/out/persistence/ -B2 -A2Repository: UMC-PRODUCT/umc-product-server
Length of output: 594
🏁 Script executed:
# Find migration files
find . -type f -name "*.sql" -o -name "V*.sql" -o -name "*.ddl" 2>/dev/null | grep -E "migration|flyway|db"Repository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Check if there are any FK constraints defined elsewhere
rg -i 'alter table.*add.*constraint\|foreign key\|references' src/ test/ --type=sqlRepository: UMC-PRODUCT/umc-product-server
Length of output: 119
🏁 Script executed:
# Look for index definitions on member_id
rg -i 'create index\|member_id' . --type=sqlRepository: UMC-PRODUCT/umc-product-server
Length of output: 56
🏁 Script executed:
# Check all domain entities to see the pattern consistency
rg -n 'private Long.*Id.*// ID' src/main/java/com/umc/product --type=javaRepository: UMC-PRODUCT/umc-product-server
Length of output: 407
🏁 Script executed:
# Examine ChallengerRole to understand when @ManyToOne is used
cat -n src/main/java/com/umc/product/challenger/domain/ChallengerRole.java | head -50Repository: UMC-PRODUCT/umc-product-server
Length of output: 2012
🏁 Script executed:
# Check if there are any query methods that might suggest index usage patterns
rg -n 'findBy.*MemberId|Query.*memberId' src/main/java/com/umc/product/member/adapter/ -B2 -A2Repository: UMC-PRODUCT/umc-product-server
Length of output: 1211
🏁 Script executed:
# Verify the architectural documentation
cat README.md | head -100Repository: UMC-PRODUCT/umc-product-server
Length of output: 1256
도메인 간 느슨한 결합을 위한 의도된 설계
memberId를 Long 타입의 ID 참조만으로 유지하고 JPA 관계 매핑을 사용하지 않는 것은 헥사고날 아키텍처에서 도메인 간 독립성을 유지하기 위한 의도된 설계입니다. Member.java의 schoolId 필드에서도 동일한 패턴을 찾을 수 있으며, "organization 도메인 의존 방지"라는 주석으로 설계 의도가 명확히 드러나 있습니다.
다만 다음 항목을 확인해주세요:
- 인덱스 추가 검토:
member_id컬럼에 인덱스가 없으면 특정 회원의 OAuth 정보를 조회할 때 성능 문제가 발생할 수 있으므로, Flyway 마이그레이션에서 인덱스 추가를 고려해주세요.
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/MemberOAuth.java around lines 31
- 32, MemberOAuth intentionally stores memberId as a Long to avoid JPA coupling;
ensure you add a DB index on the member_id column via Flyway migration to avoid
query performance issues when fetching OAuth entries by memberId — create a
migration that adds an index on member_id (referencing the member_id column in
the MemberOAuth table) and verify the migration is applied in tests/CI.
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "oauth_provider", nullable = false, length = 20) | ||
| private OAuthProvider provider; | ||
|
|
||
| @Column(name = "provider_id", nullable = false, length = 512) | ||
| private String providerId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
매직 넘버를 상수로 추출하는 것을 고려해주세요.
컬럼 길이 값(20, 512)이 하드코딩되어 있습니다. 이 값들을 상수로 추출하면 유지보수성과 가독성이 향상됩니다.
♻️ 상수 추출 제안
+ private static final int OAUTH_PROVIDER_MAX_LENGTH = 20;
+ private static final int PROVIDER_ID_MAX_LENGTH = 512;
+
@Enumerated(EnumType.STRING)
- @Column(name = "oauth_provider", nullable = false, length = 20)
+ @Column(name = "oauth_provider", nullable = false, length = OAUTH_PROVIDER_MAX_LENGTH)
private OAuthProvider provider;
- @Column(name = "provider_id", nullable = false, length = 512)
+ @Column(name = "provider_id", nullable = false, length = PROVIDER_ID_MAX_LENGTH)
private String providerId;이렇게 하면 빌더 생성자의 검증 로직에서도 동일한 상수를 재사용할 수 있습니다.
📝 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.
| @Enumerated(EnumType.STRING) | |
| @Column(name = "oauth_provider", nullable = false, length = 20) | |
| private OAuthProvider provider; | |
| @Column(name = "provider_id", nullable = false, length = 512) | |
| private String providerId; | |
| private static final int OAUTH_PROVIDER_MAX_LENGTH = 20; | |
| private static final int PROVIDER_ID_MAX_LENGTH = 512; | |
| @Enumerated(EnumType.STRING) | |
| @Column(name = "oauth_provider", nullable = false, length = OAUTH_PROVIDER_MAX_LENGTH) | |
| private OAuthProvider provider; | |
| @Column(name = "provider_id", nullable = false, length = PROVIDER_ID_MAX_LENGTH) | |
| private String providerId; |
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/MemberOAuth.java around lines 34
- 39, Extract the hardcoded column length literals in MemberOAuth into named
constants (e.g., PROVIDER_LENGTH and PROVIDER_ID_LENGTH) and replace the numeric
literals used in the @Column annotations for provider and providerId with those
constants; also update any validation logic in the MemberOAuth
builder/constructor that checks lengths to reuse the same constants so the
constraints remain consistent and maintainable.
| @Builder | ||
| private MemberOAuth(Long memberId, OAuthProvider provider, String providerId) { | ||
| this.memberId = memberId; | ||
| this.provider = provider; | ||
| this.providerId = providerId; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
빌더 생성자에 입력 검증 로직이 누락되었습니다.
생성자에서 파라미터 검증이 전혀 이루어지지 않아 다음과 같은 문제가 발생할 수 있습니다:
- Null 값 허용:
memberId,provider,providerId가 모두nullable = false임에도 불구하고 null 체크가 없어 런타임에 데이터베이스 제약 조건 위반 예외가 발생합니다. - 길이 초과:
providerId의 최대 길이가 512자로 제한되어 있지만 검증하지 않아 예외가 발생할 수 있습니다. - 빈 문자열:
providerId가 빈 문자열인 경우 비즈니스 로직상 유효하지 않을 수 있습니다.
As per coding guidelines, 입력값 검증은 필수이며, 도메인 엔티티는 항상 유효한 상태를 유지해야 합니다.
🛡️ 입력 검증 로직 추가 제안
+ private static final int MAX_PROVIDER_ID_LENGTH = 512;
+
@Builder
private MemberOAuth(Long memberId, OAuthProvider provider, String providerId) {
+ validateMemberId(memberId);
+ validateProvider(provider);
+ validateProviderId(providerId);
+
this.memberId = memberId;
this.provider = provider;
this.providerId = providerId;
}
+
+ private void validateMemberId(Long memberId) {
+ if (memberId == null) {
+ throw new IllegalArgumentException("memberId는 필수입니다.");
+ }
+ }
+
+ private void validateProvider(OAuthProvider provider) {
+ if (provider == null) {
+ throw new IllegalArgumentException("OAuth provider는 필수입니다.");
+ }
+ }
+
+ private void validateProviderId(String providerId) {
+ if (providerId == null || providerId.isBlank()) {
+ throw new IllegalArgumentException("providerId는 필수이며 빈 값일 수 없습니다.");
+ }
+ if (providerId.length() > MAX_PROVIDER_ID_LENGTH) {
+ throw new IllegalArgumentException(
+ String.format("providerId는 %d자를 초과할 수 없습니다. (현재: %d자)",
+ MAX_PROVIDER_ID_LENGTH, providerId.length()));
+ }
+ }또는 프로젝트에 공통 예외 처리 구조가 있다면 커스텀 예외(InvalidMemberOAuthException 등)를 사용하는 것도 고려해주세요.
🤖 Prompt for AI Agents
In @src/main/java/com/umc/product/member/domain/MemberOAuth.java around lines 41
- 46, The builder constructor MemberOAuth(Long memberId, OAuthProvider provider,
String providerId) lacks input validation; update that @Builder private
MemberOAuth(...) to validate memberId, provider and providerId are non-null,
ensure providerId is not blank and its length <= 512, and throw a suitable
exception (e.g., IllegalArgumentException or a project-specific
InvalidMemberOAuthException) with clear messages when validations fail so the
entity cannot be constructed in an invalid state.
| <appender class="ch.qos.logback.core.ConsoleAppender" name="APP_CONSOLE"> | ||
| <encoder> | ||
| <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{traceId}] [%thread] %logger{40} : %msg%n | ||
| <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%-4.4X{traceId}] [%thread] %logger{40} : %msg%n |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
traceId 4자 제한으로 인한 추적 불가 문제
[%-4.4X{traceId}] 패턴은 traceId를 4자로 truncate하는데, 이는 분산 추적에 심각한 문제를 일으킬 수 있습니다:
- 일반적인 traceId(UUID, correlation ID 등)는 16-36자 이상으로 훨씬 깁니다
- 4자로 제한하면 서로 다른 요청이 동일한 truncated ID를 가질 확률이 매우 높아져 로그 추적이 사실상 불가능합니다
- FILE appender(line 4)는 전체 traceId를 유지하는데 콘솔만 제한하면 일관성이 떨어집니다
콘솔 가독성을 위해 제한하려면 최소 12-16자는 유지하거나, 전체 traceId를 표시하는 것을 권장합니다.
♻️ 권장 수정사항
- <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%-4.4X{traceId}] [%thread] %logger{40} : %msg%n
+ <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%X{traceId}] [%thread] %logger{40} : %msg%n또는 최소한 더 긴 길이 유지:
- <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%-4.4X{traceId}] [%thread] %logger{40} : %msg%n
+ <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%-16.16X{traceId}] [%thread] %logger{40} : %msg%n🤖 Prompt for AI Agents
In @src/main/resources/logback-spring.xml at line 20, The console log pattern
truncates traceId to 4 chars via the pattern token "%-4.4X{traceId}", breaking
distributed tracing; update the pattern in the logback-spring.xml pattern
element to preserve more or the full traceId (e.g. use "%X{traceId}" to print
the entire ID or "%-16.16X{traceId}" to keep a 16-char view) so console output
matches the FILE appender and maintains consistent, traceable IDs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 69 out of 78 changed files in this pull request and generated 12 comments.
| ### OAuth 로그인 후 회원가입 완료 처리 | ||
| - OAuth로 로그인한 사용자가 아직 회원가입이 완료되지 않은 상태(이름, 닉네임 등 사용자 정보를 설정하지 않은 상태)인 경우에 사용됩니다. | ||
| - 이름, 닉네임, 이메일, 학교명을 선택합니다. | ||
| - 작성 기준: 2026/01/06 하늘/박경운 |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: 문서화 누락 - 작성 날짜가 미래
작성 기준이 "2026/01/06"로 미래 날짜입니다. 현재는 2026년 1월 8일이므로 과거 날짜이지만, 실제 작성 날짜를 정확히 기록하는 것이 좋습니다.
| @ManyToOne() | ||
| @JoinColumn(name = "challenger_id", nullable = false) | ||
| private Challenger challenger; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: 아키텍처 규칙 위반 - Entity에서 다른 Entity 직접 참조
ChallengerPoint 엔티티가 Challenger 엔티티를 ManyToOne으로 직접 참조하고 있습니다. 헥사고날 아키텍처 가이드라인에 따르면 다른 도메인 Entity를 직접 참조하지 않고 ID만 저장해야 합니다.
권장 수정:
@ManyToOne관계를 제거하고challenger_id를 Long 타입 필드로 변경- ChallengerPoint가 Challenger의 자식 엔티티가 아닌 독립적인 도메인 엔티티로 관리
| MemberPrincipal memberPrincipal = (MemberPrincipal) authentication.getPrincipal(); | ||
| Long memberId = memberPrincipal.getMemberId(); | ||
|
|
||
| log.info("OAuth2 authentication success: memberId={}", memberId); | ||
|
|
||
| // JWT 토큰 생성 | ||
| String accessToken = jwtTokenProvider.createAccessToken(memberId, Collections.emptyList()); | ||
| String refreshToken = jwtTokenProvider.createRefreshToken(memberId); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: 보안 취약점 - 인증 정보 검증 누락
OAuth2 인증 성공 후 토큰 생성 시 memberId의 유효성을 검증하지 않고 있습니다. MemberPrincipal에서 null이거나 잘못된 memberId가 올 수 있는 경우를 대비해야 합니다.
권장 수정:
MemberPrincipal memberPrincipal = (MemberPrincipal) authentication.getPrincipal();
Long memberId = memberPrincipal.getMemberId();
if (memberId == null) {
log.error("OAuth2 authentication failed: memberId is null");
throw new IllegalStateException("Invalid member principal");
}| // 프론트엔드로 리다이렉트 (토큰 전달) | ||
| String targetUrl = UriComponentsBuilder.fromUriString(redirectUrl) | ||
| .queryParam("accessToken", accessToken) | ||
| .queryParam("refreshToken", refreshToken) | ||
| .build().toUriString(); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: 보안 취약점 - 토큰을 Query Parameter로 전달
Access Token과 Refresh Token을 URL의 Query Parameter로 프론트엔드에 전달하고 있습니다. 이는 브라우저 히스토리, 서버 로그, Referer 헤더 등에 토큰이 노출될 수 있는 보안 취약점입니다.
권장 수정:
- POST 요청의 Body나 안전한 Cookie(HttpOnly, Secure, SameSite)로 토큰 전달
- 또는 프론트엔드에서 Authorization Code Flow를 통해 안전하게 토큰 교환
| public Long completeRegister(@Valid @RequestBody CompleteRegisterMemberRequest request, | ||
| @CurrentMember MemberPrincipal currentMember) { | ||
|
|
||
| Long userId = manageMemberUseCase.completeRegister(request.toCommand(currentMember.getMemberId())); | ||
| return userId; | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: API 설계 - 불필요한 반환값
회원가입 완료 API가 userId를 반환하지만, 인증된 사용자는 이미 자신의 ID를 알고 있으므로 불필요합니다. ApiResponse로 감싸서 성공 메시지를 반환하는 것이 더 적절합니다.
권장 수정:
public ApiResponse<Void> completeRegister(@Valid @RequestBody CompleteRegisterMemberRequest request,
@CurrentMember MemberPrincipal currentMember) {
manageMemberUseCase.completeRegister(request.toCommand(currentMember.getMemberId()));
return ApiResponse.success();
}| public Long completeRegister(@Valid @RequestBody CompleteRegisterMemberRequest request, | ||
| @CurrentMember MemberPrincipal currentMember) { | ||
|
|
||
| Long userId = manageMemberUseCase.completeRegister(request.toCommand(currentMember.getMemberId())); | ||
| return userId; | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: API 설계 - Response 타입 일관성 부족
회원가입 완료 API는 Long을 직접 반환하지만, 사용자 조회 API는 MemberResponse를 반환합니다. ApiResponse로 감싸는 것이 일관성 있는 API 설계입니다.
권장 수정:
@PostMapping("/register/complete")
public ApiResponse<Long> completeRegister(...) {
Long userId = manageMemberUseCase.completeRegister(...);
return ApiResponse.success(userId);
}| @ManyToOne | ||
| @JoinColumn(name = "challenger_id", nullable = false) | ||
| private Challenger challenger; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: 아키텍처 규칙 위반 - Entity에서 다른 Entity 직접 참조
ChallengerRole 엔티티가 Challenger 엔티티를 ManyToOne으로 직접 참조하고 있습니다. 헥사고날 아키텍처 가이드라인에 따르면 다른 도메인 Entity를 직접 참조하지 않고 ID만 저장해야 합니다.
권장 수정:
@ManyToOne관계를 제거하고challenger_id를 Long 타입 필드로 변경- ChallengerRole이 Challenger의 자식 엔티티가 아닌 독립적인 도메인 엔티티로 관리
| @OneToMany( | ||
| mappedBy = "challenger", | ||
| fetch = FetchType.LAZY, | ||
| cascade = CascadeType.ALL, | ||
| orphanRemoval = true | ||
| ) | ||
| private List<ChallengerPoint> challengerPoints = new ArrayList<>(); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: 아키텍처 규칙 위반 - Entity에서 양방향 연관관계 사용
Challenger 엔티티가 ChallengerPoint와 OneToMany 양방향 연관관계를 맺고 있습니다. 헥사고날 아키텍처에서는 Entity 간 직접 참조를 피하고 ID 참조를 권장합니다.
권장 수정:
- OneToMany 관계를 제거
- ChallengerPoint는 challenger_id만 가지고 별도로 조회하도록 변경
- 필요 시 Repository에서 fetchJoin이나 별도 조회로 처리
| public Challenger(Long memberId, ChallengerPart part, Long gisuId) { | ||
| this.memberId = memberId; | ||
| this.part = part; | ||
| this.gisuId = gisuId; | ||
| this.status = ChallengerStatus.ACTIVE; | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: 도메인 로직 누락 - Builder 어노테이션 미사용
Challenger 엔티티에 public 생성자가 존재하지만, 다른 엔티티들과 일관성을 위해 @Builder 어노테이션과 private 생성자를 사용하는 것이 좋습니다.
권장 수정:
@Builder
private Challenger(Long memberId, ChallengerPart part, Long gisuId) {
this.memberId = memberId;
this.part = part;
this.gisuId = gisuId;
this.status = ChallengerStatus.ACTIVE;
}| @Operation(summary = "회원가입 완료", | ||
| description = """ | ||
| ### OAuth 로그인 후 회원가입 완료 처리 | ||
| - OAuth로 로그인한 사용자가 아직 회원가입이 완료되지 않은 상태(이름, 닉네임 등 사용자 정보를 설정하지 않은 상태)인 경우에 사용됩니다. | ||
| - 이름, 닉네임, 이메일, 학교명을 선택합니다. | ||
| - 작성 기준: 2026/01/06 하늘/박경운 | ||
| """) |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: 스펠링 오류 - URL 경로 오타
Swagger description에 "닉네임"이 반복되어 있고, "학교명을 선택합니다"가 어색합니다.
권장 수정:
description = """
### OAuth 로그인 후 회원가입 완료 처리
- OAuth로 로그인한 사용자가 아직 회원가입이 완료되지 않은 상태인 경우 사용됩니다.
- 이름, 닉네임, 이메일, 학교를 입력하여 회원가입을 완료합니다.
- 작성 기준: 2026/01/06 하늘/박경운
"""
Backend Test Results0 tests 0 ✅ 0s ⏱️ Results for commit 2f2a413. |
✅ 체크리스트
🔥 연관 이슈
🚀 작업 내용
Member도메인 엔티티 설계 및 Port, Service, Adapter 구현Challenger도메인 엔티티 설계💬 리뷰 중점사항
OAuth 관련해서
application-local.yml을 변경하였습니다. 해당 부분 Notion에서 참고해서 반영해주세요.📃 관련 문서
Hexagonal Architecture 관련 설계 컨벤션을 제작했습니다.
Summary by CodeRabbit
릴리스 노트
새로운 기능
Chores
✏️ Tip: You can customize this high-level summary in your review settings.