-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] #197: 신규 멤버 모집 관련 관리자 페이지 api 구현 #204
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
Changes from 4 commits
66a75a3
5991936
dd74a7e
dac2759
3da088d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package inha.gdgoc.domain.recruit.dto.request; | ||
|
|
||
| public record PaymentUpdateRequest( | ||
| boolean isPayed | ||
| ) { | ||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainPATCH DTO에서 primitive boolean 사용은 위험합니다: 누락 시 false로 바인딩되어 오작동 가능 PATCH 시 필드가 누락되면 primitive 권장 수정안: +import jakarta.validation.constraints.NotNull;
public record PaymentUpdateRequest(
- boolean isPayed
+ @NotNull Boolean isPayed
) {
}컨트롤러에서의 유효성 검사 적용도 확인 필요합니다: 🏁 Script executed: #!/bin/bash
# PaymentUpdateRequest가 @Valid로 검증되는지 확인
rg -nP -C3 '@PatchMapping\([^)]*payment[^)]*\)' src/main/java | sed -n '1,120p'
rg -n 'PaymentUpdateRequest' src/main/java | sed -n '1,120p'Length of output: 1487 PATCH DTO와 컨트롤러 유효성 검사 개선 필요 원격 PATCH 요청에서 primitive boolean을 그대로 쓰면, 클라이언트가 해당 필드를 아예 보내지 않았을 때(false로 디시리얼라이즈) 의도치 않은 “미납” 상태로 변경될 위험이 있습니다. 또한 컨트롤러 메서드에 수정해야 할 지점:
권장 수정 예시: --- a/src/main/java/inha/gdgoc/domain/recruit/dto/request/PaymentUpdateRequest.java
+++ b/src/main/java/inha/gdgoc/domain/recruit/dto/request/PaymentUpdateRequest.java
@@
-public record PaymentUpdateRequest(
- boolean isPayed
-) {
+import jakarta.validation.constraints.NotNull;
+
+public record PaymentUpdateRequest(
+ @NotNull Boolean isPayed
+) {
}--- a/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java
+++ b/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java
@@
- public ResponseEntity<ApiResponse<Void, Void>> updatePayment(
- @PathVariable Long memberId,
- @RequestBody PaymentUpdateRequest paymentUpdateRequest
+) public ResponseEntity<ApiResponse<Void, Void>> updatePayment(
+ @PathVariable Long memberId,
+ @Valid @RequestBody PaymentUpdateRequest paymentUpdateRequest위 두 군데를 반영한 뒤에, 실제로 유효성 검사 예외가 컨트롤러로 전달되는지 통합테스트 등을 통해 반드시 확인해 주세요. 🤖 Prompt for AI Agents |
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package inha.gdgoc.domain.recruit.dto.response; | ||
|
|
||
| import inha.gdgoc.domain.recruit.entity.RecruitMember; | ||
|
|
||
| public record RecruitMemberSummaryResponse( | ||
| Long id, | ||
| String name, | ||
| String phoneNumber, | ||
| String major, | ||
| String studentId, | ||
| String admissionSemester, | ||
| Boolean isPayed | ||
| ) { | ||
|
|
||
| public static RecruitMemberSummaryResponse from(RecruitMember recruitMember) { | ||
| String semester = null; | ||
| if (recruitMember.getAdmissionSemester() != null) { | ||
| String enumName = recruitMember.getAdmissionSemester().name(); | ||
| semester = enumName.substring(1).replace('_', '-'); | ||
| } | ||
|
|
||
| return new RecruitMemberSummaryResponse( | ||
| recruitMember.getId(), | ||
| recruitMember.getName(), | ||
| recruitMember.getPhoneNumber(), | ||
| recruitMember.getMajor(), | ||
| recruitMember.getStudentId(), | ||
| semester, | ||
| recruitMember.getIsPayed() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||
| package inha.gdgoc.domain.recruit.entity; | ||||||||||||||
|
|
||||||||||||||
| import com.fasterxml.jackson.annotation.JsonFormat; | ||||||||||||||
| import inha.gdgoc.domain.recruit.enums.AdmissionSemester; | ||||||||||||||
| import inha.gdgoc.domain.recruit.enums.EnrolledClassification; | ||||||||||||||
| import inha.gdgoc.domain.recruit.enums.Gender; | ||||||||||||||
| import inha.gdgoc.global.entity.BaseEntity; | ||||||||||||||
|
|
@@ -67,13 +68,25 @@ public class RecruitMember extends BaseEntity { | |||||||||||||
| @Column(name = "major", nullable = false) | ||||||||||||||
| private String major; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "double_major", nullable = true) | ||||||||||||||
| @Column(name = "double_major") | ||||||||||||||
| private String doubleMajor; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "is_payed", nullable = false) | ||||||||||||||
| private Boolean isPayed; | ||||||||||||||
|
|
||||||||||||||
| @Enumerated(EnumType.STRING) | ||||||||||||||
| @Column(name = "admission_semester", nullable = false, length = 10) | ||||||||||||||
| private AdmissionSemester admissionSemester; | ||||||||||||||
|
Comment on lines
+77
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enum 매핑과 마이그레이션 값 불일치 가능 — 과거 데이터 로딩 실패 위험(중대) DB 마이그레이션은 과거 레코드에 Y24_1, Y24_2 … 형태를 백필합니다. 그런데 AdmissionSemester enum이 Y25_1, Y25_2, Y26_1, Y26_2 정도로 제한되어 있다면, JPA @Enumerated(EnumType.STRING) 매핑은 과거 값 로딩 시 즉시 IllegalArgumentException(No enum constant …)로 실패합니다. 목록 조회/검색 시 런타임 장애로 이어집니다. 해결 옵션:
필드만 String으로 전환하는 최소 수정안 예시: - @Enumerated(EnumType.STRING)
- @Column(name = "admission_semester", nullable = false, length = 10)
- private AdmissionSemester admissionSemester;
+ // 과거/미래 학기 값을 폭넓게 수용하기 위해 문자열로 저장합니다.
+ @Column(name = "admission_semester", nullable = false, length = 10)
+ private String admissionSemester;후속 영향:
원하시면 값 객체 + Converter 방식으로 안전하게 리팩터링하는 PR 패치를 드리겠습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| @Builder.Default | ||||||||||||||
| @OneToMany(mappedBy = "recruitMember", cascade = CascadeType.ALL, orphanRemoval = true) | ||||||||||||||
| private List<Answer> answers = new ArrayList<>(); | ||||||||||||||
|
|
||||||||||||||
| public void markPaid() { | ||||||||||||||
| this.isPayed = Boolean.TRUE; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public void markUnpaid() { | ||||||||||||||
| this.isPayed = Boolean.FALSE; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,5 @@ | ||||||||||||||
| package inha.gdgoc.domain.recruit.enums; | ||||||||||||||
|
|
||||||||||||||
| public enum AdmissionSemester { | ||||||||||||||
| Y25_1, Y25_2, Y26_1, Y26_2 | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain열거값 범위 한정으로 인한 런타임 예외 위험: 1월 처리와 향후 연도 대비 필요
권장 수정안(예시: 과거 24년 2학기, 미래 27년까지 확장): public enum AdmissionSemester {
- Y25_1, Y25_2, Y26_1, Y26_2
+ Y24_2, Y25_1, Y25_2, Y26_1, Y26_2, Y27_1, Y27_2
}추가로, 매년 연말 CI에서 다음 해 상수 누락을 검출하는 가드 테스트를 두는 것을 권장합니다. 다음 스크립트로 🏁 Script executed: #!/bin/bash
# AdmissionSemester enum 확인
echo '== AdmissionSemester enum =='
sed -n '1,200p' src/main/java/inha/gdgoc/domain/recruit/enums/AdmissionSemester.java
# SemesterCalculator 클래스 위치 확인
echo '== SemesterCalculator 클래스 위치 =='
rg -n 'class SemesterCalculator' -C3 src/main/java/inha/gdgoc/domain/recruit
# SemesterCalculator.of() 구현 확인
echo '== SemesterCalculator.of() 구현 =='
rg -nP 'public static.*AdmissionSemester of' -C5 src/main/java/inha/gdgoc/domain/recruitLength of output: 576
🏁 Script executed: #!/bin/bash
# 1) SemesterCalculator 클래스 파일 경로 조회
FILE=$(rg -l 'class SemesterCalculator' -g '*.java' .)
echo "SemesterCalculator 위치: $FILE"
# 2) 클래스 상단 200줄 출력
echo "== SemesterCalculator 초기 부분 =="
sed -n '1,200p' "$FILE"
# 3) of() 메서드 구현부 출력
echo "== of() 메서드 구현 =="
rg -n 'of\s*\(' -C5 "$FILE"
# 4) 연도·월 계산 로직 확인 (getYear, getMonthValue 탐색)
echo "== 연도·월 계산 참조 =="
rg -n -C3 'getYear|getMonthValue' "$FILE"Length of output: 2626 AdmissionSemester enum에 과거·미래 학기 추가 필요
대응 방안(예시): --- a/src/main/java/inha/gdgoc/domain/recruit/enums/AdmissionSemester.java
+++ b/src/main/java/inha/gdgoc/domain/recruit/enums/AdmissionSemester.java
@@ public enum AdmissionSemester {
- Y25_1, Y25_2, Y26_1, Y26_2
+ Y24_2, Y25_1, Y25_2, Y26_1, Y26_2, Y27_1, Y27_2
}
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package inha.gdgoc.global.dto.response; | ||
|
|
||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Sort; | ||
|
|
||
| public record PageMeta( | ||
| int page, | ||
| int size, | ||
| long totalElements, | ||
| int totalPages, | ||
| boolean hasNext, | ||
| boolean hasPrevious, | ||
| String sort, | ||
| String direction | ||
| ) { | ||
|
|
||
| public static PageMeta of(Page<?> page) { | ||
| String sortProps = page.getSort().stream() | ||
| .map(Sort.Order::getProperty) | ||
| .reduce((a, b) -> a + "," + b) | ||
| .orElse("createdAt"); | ||
|
|
||
| String dir = page.getSort().stream() | ||
| .findFirst() | ||
| .map(o -> o.getDirection().name()) | ||
| .orElse("DESC"); | ||
|
|
||
| return new PageMeta( | ||
| page.getNumber(), | ||
| page.getSize(), | ||
| page.getTotalElements(), | ||
| page.getTotalPages(), | ||
| page.hasNext(), | ||
| page.hasPrevious(), | ||
| sortProps, | ||
| dir | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package inha.gdgoc.global.util; | ||
|
|
||
| import inha.gdgoc.domain.recruit.enums.AdmissionSemester; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.ZoneId; | ||
|
|
||
| public final class SemesterCalculator { | ||
| private static final ZoneId KST = ZoneId.of("Asia/Seoul"); | ||
|
|
||
| private SemesterCalculator() {} | ||
|
|
||
| public static AdmissionSemester currentSemester() { | ||
| return of(LocalDate.now(KST)); | ||
| } | ||
|
|
||
| public static AdmissionSemester of(LocalDate date) { | ||
| int year = date.getYear(); | ||
| int month = date.getMonthValue(); | ||
|
|
||
| int yy; | ||
| int term; | ||
|
|
||
| if (month == 1) { | ||
| yy = (year - 1) % 100; | ||
| term = 2; | ||
| } else if (month <= 7) { | ||
| yy = year % 100; | ||
| term = 1; | ||
| } else { | ||
| yy = year % 100; | ||
| term = 2; | ||
| } | ||
|
|
||
| String enumName = String.format("Y%02d_%d", yy, term); | ||
| try { | ||
| return AdmissionSemester.valueOf(enumName); | ||
| } catch (Exception ex) { | ||
| throw new RuntimeException( | ||
| "AdmissionSemester enum에 상수 " + enumName + " 이(가) 정의되어 있지 않습니다. " + | ||
| "해당 연도/학기 상수를 추가하세요.", ex | ||
| ); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| -- 1) 컬럼 추가 (처음엔 NULL 허용) | ||
| ALTER TABLE recruit_member | ||
| ADD COLUMN admission_semester VARCHAR(10); | ||
|
|
||
| -- 2) 기존 데이터 백필 | ||
| -- 학기 규칙: 2~7월 = YYY_1, 8~12월 = YYY_2, 1월 = (전년도) YYY_2 | ||
| UPDATE recruit_member | ||
| SET admission_semester = CASE | ||
| WHEN EXTRACT(MONTH FROM created_at) BETWEEN 8 AND 12 | ||
| THEN 'Y' || to_char(created_at, 'YY') || '_2' | ||
| WHEN EXTRACT(MONTH FROM created_at) BETWEEN 2 AND 7 | ||
| THEN 'Y' || to_char(created_at, 'YY') || '_1' | ||
| WHEN EXTRACT(MONTH FROM created_at) = 1 | ||
| THEN 'Y' || to_char(created_at - INTERVAL '1 year', 'YY') || '_2' | ||
| END | ||
| WHERE admission_semester IS NULL; | ||
|
|
||
| -- 3) NOT NULL 전환 (형식 제약은 생략 가능) | ||
| ALTER TABLE recruit_member | ||
| ALTER COLUMN admission_semester SET NOT NULL; | ||
|
|
||
| -- (선택) 인덱스 | ||
| CREATE INDEX idx_recruit_member_admission_semester | ||
| ON recruit_member (admission_semester); |
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
목록 조회 API: 파라미터 검증 추가와 정렬 컬럼 화이트리스트 권장
PageRequest.of가 예외를 던지면 500으로 번질 수 있습니다. Bean Validation으로 400을 반환하도록 하는 것이 안전합니다.sort값은 스프링 데이터에서PropertyReferenceException을 유발할 수 있습니다. 허용 컬럼을 제한하세요.검증 추가(diff):
정렬 컬럼 화이트리스트(예시)(diff):
추가 import:
운영 수위 제안:
question = question == null ? null : question.trim();으로 공백만 있는 입력을 일찍 정규화할 수 있습니다.