Conversation
* DepartmentName에 COMPUTER_GRADUATE 추가 * ComputerScienceGraduateDept 구현 * [feat]: urlPrefix 구현 * [feat]: urlPrefix 적용 * [test]: ComputerScienceGraduateDeptTest 구현 * [refactor]: GraduateDeptInfoTest로 이동 * [feat]: isGrad 필드 추가 * [feat]: isGrad 적용 * [feat]: is_grad 필드 추가 flyway 적용 * [refactor]: getUrlPrefix -> getHostPrefix로 수정 * [refactor]: urlPrefix 삭제 * [feat]: noticeGraudationInfo 추가 * [refactor]: ComputerScienceGraduateDept 삭제 * [refactor]: isGrad 적용 * [refactor]: GraduateDeptInfoTest 개행 추가, TestFileLoader 적용 * [refactor]: isGrad -> graduate로 필드명 수정 * [refactor]: graduate 필드 Null 허용으로 수정 * [refactor]: getter 필드 위에 지정 * [refactor]: setter -> 비즈니스 함수로 수정 * [refactor]: 대학원/학사 DepartmentNoticeUpdater 분리 * [refactor]: 개행처리 * [refactor]: DepartmentNoticeScrapResult 삭제 * [refactor]: LatestPageNoticeApiClient 분리 * [refactor]: creatUrl/scrapHtml 분리 * [refactor]: graduate로 필드 수정 * [refactor]: latestPageGraduateNoticeApiClient 사용 * [refactor]: graduate -> graduated로 이름 수 * [refactor]: 일반 notice는 graduated 필드에 null로 저장 * [refactor]: getGraduated() 오타 수정 * [refactor]: deptInfo.isSupportGraduateScrap() 구현 - 대학원 과정을 지원 안하는 DeptInfo 방어 로직 * [refactor]: ? 개수 수정 * [refactor]: 일반 공지 graduated null로 반환 테스트 추가 * [refactor]: 컨트롤러 graduated 파라미터 required=false로 설정 * [refactor]: 공통 로직 getGraduateDeptInfoList 메서드 추출 * [feat]: noticeGraduationInfo 추가 * [feat]: Notice 관련 테스트에 graduated 적용 * [fix]: noticeGraduationInfo 오타 수정
* feat(AuthConfig): CORS 설정 추가 * feat(AuthConfig): CORS 설정 추가 * feat(Report): reporterId로 필드명 변경 * feat(AdminQueryService): 조회시 Page구현체 사용하도록 변경
* feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거
* feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 * feat: 학사일정 알림 토글 API 엔드포인트 및 Response Dto추가 * feat: 학사일정 알림 성공 메시지 추가 * feat: 학사일정 알림 토글 UseCase 추가 * feat: 학사일정 알림 토글 서비스 로직 추가 * feat: 학사일정 토글 도메인 테스트 추가 * feat: 학사일정 토글 인수 테스트 추가 * fix: FeatureFlagConfig Remote 설정 * fix: toggleAcademicEventNotification 메서드명 변경 -> updateUserAcademicEventNotification * fix: 불필요 반환 객체 제거(result, response) * fix: 학사일정 알림 변경 요청 시 설정값 받도록 수정 * fix: 학사일정 알림 설정 도메인 로직수정 * fix: 유저 인수테스트 및 테스트 수정 * fix: @NotNull javax -> jakarta 임포트 수정
|
Warning Rate limit exceeded@rlagkswn00 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 31 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (4)
Walkthrough관리자 조회 API의 응답을 페이지 DTO로 래핑하고, 내부 유즈케이스/리포지토리를 Page 기반으로 변경. 공지 조회에 대학원 여부(graduated) 필터를 도입하고, 도메인/포트/리포지토리/스크래퍼/업데이터 전반에 반영. 사용자 학사일정 알림 토글 엔드포인트 추가 및 컬럼/플래그/응답코드/라벨 도입. CORS 설정 추가. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant API as NoticeQueryApiV2
participant Svc as NoticeQueryService
participant Port as NoticeQueryPort
participant Repo as NoticeQueryRepository
Client->>API: GET /api/v2/notices?graduated=bool
API->>Svc: NoticeRangeLookupCommand(type, dept, important, graduated, page, size)
Svc->>Port: find*ByDepartment(dept, graduated, pageable)
Port->>Repo: SQL with WHERE department AND graduated
Repo-->>Port: List<NoticeDto>
Port-->>Svc: List<NoticeDto>
Svc-->>API: List<NoticeRangeLookupResult(graduated,...)]
API-->>Client: BaseResponse(data)
sequenceDiagram
autonumber
participant Scheduler as DepartmentGraduationNoticeUpdater
participant Dept as DeptInfo
participant Scraper as LatestPageGraduateNoticeApiClient
participant DB as NoticeRepository
participant Noti as NotificationService
rect rgba(200,230,255,0.2)
note over Scheduler: 매 20분/주기적 전체 갱신
Scheduler->>Dept: 대상 부서 목록 필터(graduate 지원)
par 각 부서 비동기
Scheduler->>Scraper: scrapLatestPageHtml()
Scraper-->>Scheduler: ScrapingResultDto
Scheduler->>DB: 비교 및 동기화(graduated=true)
DB-->>Scheduler: 신규/갱신 결과
Scheduler->>Noti: sendNotifications()
end
end
sequenceDiagram
autonumber
actor User
participant API as UserCommandApiV2
participant Svc as UserCommandService
participant DB as UserRepository
User->>API: PATCH /users/notifications/academic-events {enabled}
API->>Svc: UserAcademicEventNotificationCommand(token, enabled)
Svc->>DB: findByToken(token)
DB-->>Svc: User
Svc->>DB: user.updateAcademicNotificationEnabled(enabled)
API-->>User: BaseResponse(ACADEMIC_EVENT_NOTIFICATION_UPDATE_SUCCESS)
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupCommand.java (1)
11-13: NPE 위험: Boolean 언박싱.
important가 null이면isImportant()에서 언박싱 NPE가 발생합니다. 안전한 비교로 교체하세요.아래 패치를 적용해 주세요:
- public boolean isImportant() { - return this.important; - } + public boolean isImportant() { + return Boolean.TRUE.equals(this.important); + }src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java (1)
316-328: 긴급: deleteAllByIdsAndDepartment에 graduated 조건 누락 — 학부/대학원 공지 교차 삭제 위험deleteAllByIdsAndDepartment에 Boolean graduated 파라미터와 where절(departmentNotice.graduated.eq(graduated))을 추가하고, 포트/어댑터/호출부의 시그니처와 호출을 모두 동기화하세요.
수정 대상(예시):
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java — 메서드 시그니처에 Boolean graduated 추가 및 where절에 graduated 조건 추가.
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java — 인터페이스 시그니처 변경.
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java — 어댑터 시그니처/전달 변경.
- src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeCommandPort.java — 포트 시그니처 변경.
- src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java — 호출부에서 적절한 graduated 값 전달(예: 학부=false).
- src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java — 호출부에서 적절한 graduated 값 전달(예: 대학원/졸업=true).
구현 예시(diff):
- public void deleteAllByIdsAndDepartment(DepartmentName departmentName, List<String> articleIds) { + public void deleteAllByIdsAndDepartment(DepartmentName departmentName, Boolean graduated, List<String> articleIds) { if (articleIds.isEmpty()) { return; } queryFactory .delete(departmentNotice) - .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.articleId.in(articleIds))) + .where(departmentNotice.departmentName.eq(departmentName) + .and(departmentNotice.graduated.eq(graduated)) + .and(departmentNotice.articleId.in(articleIds))) .execute(); }src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java (1)
135-147: 졸업여부(graduated) 미반영 삭제 쿼리 — 데이터 오삭제 위험, 즉시 수정 필요동일 부서·동일 articleId가 학부/대학원(졸업여부 차이)에 동시에 존재할 경우 현재 deleteAllByIdsAndDepartment(DepartmentName, List)가 graduated 조건을 고려하지 않아 양쪽 레코드를 모두 삭제할 수 있습니다. 포트→어댑터→레포지토리→구현체 및 호출부 모두 graduated 파라미터를 추가해 삭제 범위를 제한하세요.
수정 포인트(파일):
- src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeCommandPort.java — deleteAllByIdsAndDepartment에 Boolean graduated 추가.
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java — 시그니처 변경 후 noticeRepository에 graduated 전달.
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java 및 src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java — deleteAllByIdsAndDepartment(DepartmentName, Boolean graduated, List)로 변경하고 쿼리에 graduated 조건 추가(예: WHERE department = :dept AND article_id IN (…) AND graduated = :graduated).
- DB 레이어(예: NoticeRepository — JPA/쿼리) — 해당 메서드/쿼리에 graduated 필터 반영.
- 호출부: src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java 및 src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java — 각각 적절한 graduated 값(false/true)을 전달하도록 수정.
제안된 호출부 변경 예시(DepartmentNoticeUpdater 내 호출부 — 포트 시그니처 변경 필요):
- if (!deletedNoticesArticleIds.isEmpty()) { - noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds); - } + if (!deletedNoticesArticleIds.isEmpty()) { + // Port 시그니처를 (DepartmentName, Boolean graduated, List<String>)로 변경 필요 + noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, graduated, deletedNoticesArticleIds); + }동일 변경을 DepartmentGraduationNoticeUpdater의 호출부에도 적용하세요.
우선순위: 즉시 수정 필요(데이터 손실 가능).
🧹 Nitpick comments (62)
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_elective/VolunteerCenterDept.java (1)
30-30: 매직 넘버(523) 상수화 제안의미를 드러내기 위해 게시판/사이트 ID를 클래스 상수로 추출하는 편이 유지보수에 유리합니다.
public class VolunteerCenterDept extends SanghuoCollege { + private static final int VOLUNTEER_NOTICE_BOARD_ID = 523; @@ - this.noticeScrapInfo = new NoticeScrapInfo(VOLUNTEER.getHostPrefix(), 523); + this.noticeScrapInfo = new NoticeScrapInfo(VOLUNTEER.getHostPrefix(), VOLUNTEER_NOTICE_BOARD_ID);src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/KBeautyIndustryFusionDept.java (1)
16-16: 공백만 변경 — 기능 영향 없음의미 변화는 없습니다. 팀 포매터 규칙에 부합하지 않는다면 불필요한 no-op diff를 줄이기 위해 자동 포매팅 설정을 확인해 주세요.
src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageNoticeApiClient.java (2)
80-81: 매직 넘버(1) 제거 및 상수 재사용페이지 시작 값은 이미 START_PAGE_NUM으로 관리되고 있습니다. 동일한 의미의 1을 직접 쓰지 말고 상수를 재사용해 일관성을 높이세요.
아래와 같이 수정 제안:
- return deptInfo.createUndergraduateRequestUrl(1, 1); + return deptInfo.createUndergraduateRequestUrl(START_PAGE_NUM, 1);(선택) 행 개수도 의미가 분명하지 않으므로 상수로 올려두면 가독성이 좋아집니다.
// 상단 상수 영역 private static final int COUNT_ROW_SIZE_FOR_TOTAL = 1; // ... // 사용처 return deptInfo.createUndergraduateRequestUrl(START_PAGE_NUM, COUNT_ROW_SIZE_FOR_TOTAL);
84-86: 학부 전용 URL로 고정됨 — 의도 확인 필요요청/뷰 URL이 모두 학부 전용 메서드(createUndergraduateRequestUrl/createUndergraduateViewUrl)로 고정되었습니다. 이 클래스가 “학부 전용” 클라이언트라면 OK지만, DeptInfo가 대학원 플래그/도메인을 처리할 수 있는 구조라면 분리(예: UndergradLatestPageNoticeApiClient vs GraduateLatestPageNoticeApiClient)하거나, DeptInfo의 속성에 따라 분기하도록 만드는 편이 안전합니다.
검증 요청:
- requestAll에서 총건수 조회 URL도 학부 전용으로 맞춰진 상태인데, 실제로 “학부 공지 총건수”만 필요하다는 요구사항이 맞는지 확인해주세요(대학원 포함 총건수와 다를 수 있음).
- 학부/대학원 페이지의 총건수 셀렉터(.util-search strong 혹은 .total_count)가 동일하게 노출되는지 한 케이스씩 점검 부탁드립니다.
(선택) 클래스가 학부 전용임을 명확히 하려면 클래스명/JavaDoc에 의도를 드러내세요.
/** * 학부 공지 최신 페이지 스크래핑 전용 클라이언트. */ public class LatestPageNoticeApiClient implements NoticeApiClient<...> { ... }src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java (1)
31-31: 졸업공지 스크랩 대상 추가 자체는 OK. 보드 ID/호스트 정확성 확인 부탁
- ENGLISH.getHostPrefix() + 게시판 ID 350이 실제 “영어영문학과 졸업/학사(졸업) 공지” 보드를 가리키는지 확인 필요.
- LatestPageNoticeHtmlParser/Client가 해당 보드의 마크업/페이징 형태와 호환되는지도 점검 권장.
- noticeGraduationInfo를 소비하는 다운스트림(리포지토리/서비스/필터)이 ENGLISH 학과에 대해 새 필드를 반영하도록 매핑/테스트가 있는지 확인 부탁.
또한 매직 넘버(350) 제거를 제안합니다.
적용 diff(해당 라인 교체):
- this.noticeGraduationInfo = new NoticeScrapInfo(ENGLISH.getHostPrefix(), 350); + this.noticeGraduationInfo = new NoticeScrapInfo(ENGLISH.getHostPrefix(), GRAD_BOARD_ID);클래스 상단(필드들보다 위)에 상수 추가:
private static final int GRAD_BOARD_ID = 350;추가로, 본 파일 내 347/3901도 동일하게 상수화하거나 설정(LatestPageNoticeProperties 등)로 이관하면 유지보수성이 좋아집니다.
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java (1)
27-31: 매직 넘버(4124, 361, 363) 상수화 제안가독성과 유지보수를 위해 보드/사이트 ID를 상수로 올려두면 좋겠습니다.
해당 구간 치환(diff):
- List<Integer> siteIds = List.of(4124); + List<Integer> siteIds = STAFF_SITE_IDS; - this.staffScrapInfo = new StaffScrapInfo(HISTORY.getHostPrefix(), siteIds); - this.noticeScrapInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), 361); + this.staffScrapInfo = new StaffScrapInfo(HISTORY.getHostPrefix(), siteIds); + this.noticeScrapInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), BOARD_NOTICE_ID); this.departmentName = HISTORY; - this.noticeGraduationInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), 363); + this.noticeGraduationInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), BOARD_GRADUATION_ID);클래스 상단(필드들 위) 추가:
private static final int BOARD_NOTICE_ID = 361; private static final int BOARD_GRADUATION_ID = 363; private static final List<Integer> STAFF_SITE_IDS = List.of(4124);src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java (1)
31-31: 졸업 공지 스크랩 추가(코드 769) 적합 — 보드 코드 검증 및 매직 넘버 상수화 제안의도와 일치합니다. 다음만 확인 부탁드립니다:
- 769가 실제 CHEMI_DIV 졸업 공지 게시판 ID가 맞는지(운영 사이트 기준) 및 변경 가능성.
- 상위(EngineeringCollege 등)에서
noticeGraduationInfo를 사용하는 경로(스케줄러/업데이터,graduated=true필터)가 정상 연계되는지.LatestPageNoticeProperties설정(페이지 크기/제한/쿠키 등)이 졸업 게시판에도 유효한지.선택: 409/769 매직 넘버 상수화 권장.
적용(선택) — 이 라인 내 변경:
- this.noticeGraduationInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), 769); + this.noticeGraduationInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), GRADUATION_BOARD_ID);클래스 상단에 상수 추가(선택):
public class ChemicalDivisionDept extends EngineeringCollege { + private static final int NOTICE_BOARD_ID = 409; + private static final int GRADUATION_BOARD_ID = 769;일반 공지 라인도 함께 교체(선택):
- this.noticeScrapInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), 409); + this.noticeScrapInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), NOTICE_BOARD_ID);src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java (1)
27-31: 매직 넘버(803, 421, 10199) 상수화 제안보드/사이트 ID는 상수로 끌어올리면 가독성과 유지보수성이 좋아집니다. 추후 사이트 개편 시 변경 범위를 최소화할 수 있습니다.
적용 예시:
public class PoliticalScienceDept extends SocialSciencesCollege { + private static final int NOTICE_BOARD_ID = 803; + private static final int GRADUATION_BOARD_ID = 421; + private static final java.util.List<Integer> STAFF_SITE_IDS = java.util.List.of(10199); + public PoliticalScienceDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, LatestPageNoticeProperties latestPageNoticeProperties ) { super(); this.noticeApiClient = latestPageNoticeApiClient; this.htmlParser = latestPageNoticeHtmlParser; this.latestPageNoticeProperties = latestPageNoticeProperties; - List<Integer> siteIds = List.of(10199); - this.staffScrapInfo = new StaffScrapInfo(POLITICS.getHostPrefix(), siteIds); - this.noticeScrapInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), 803); + this.staffScrapInfo = new StaffScrapInfo(POLITICS.getHostPrefix(), STAFF_SITE_IDS); + this.noticeScrapInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), NOTICE_BOARD_ID); this.departmentName = POLITICS; - this.noticeGraduationInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), 421); + this.noticeGraduationInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), GRADUATION_BOARD_ID); } }src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java (1)
32-32: 졸업 공지 보드 ID(487) 하드코딩 — 클래스 상수화 및 중앙화 권장
- 확인: 스크립트 결과 487은 src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java:32에서만 하드코딩되어 있습니다.
- 참고: 다른 학과들도 noticeGraduationInfo에 보드 ID를 개별 하드코딩하고 있으므로(스크립트로 다수 확인), 최소한 파일 단위 상수화 후 가능하면 Department→BoardId 매핑/프로퍼티로 중앙화하세요.
- hostPrefix 확인: LivingDesignDept는 noticeScrapInfo(962)와 noticeGraduationInfo(487) 모두 LIVING_DESIGN.getHostPrefix()를 사용하고 있어 별도 호스트/서브도메인 분리로 보이지 않습니다.
적용 예시(이 파일 한정):
- this.noticeGraduationInfo = new NoticeScrapInfo(LIVING_DESIGN.getHostPrefix(), 487); + this.noticeGraduationInfo = new NoticeScrapInfo(LIVING_DESIGN.getHostPrefix(), GRAD_BOARD_ID);클래스 상단 필드 영역에 추가:
private static final int GRAD_BOARD_ID = 487;src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java (1)
11-12: 가독성: UPDATE와 NOTIFY 섹션을 주석으로 구분 제안(선택).상수 수가 늘어날 경우를 대비해 카테고리 주석을 추가하면 탐색성이 좋아집니다.
아래처럼 간단히 주석을 삽입해 주세요:
- UPDATE_ACADEMIC_EVENT(new Feature("update_academic_event")), - NOTIFY_ACADEMIC_EVENT(new Feature("notify_academic_event")); + UPDATE_ACADEMIC_EVENT(new Feature("update_academic_event")), + // Notification features + NOTIFY_ACADEMIC_EVENT(new Feature("notify_academic_event"));src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java (1)
30-32: 매직 넘버(909, 905) 상수화 제안가독성/변경 용이성을 위해 게시판 ID를 상수로 두는 편이 좋습니다.
변경 diff(해당 범위 내 치환):
- this.noticeScrapInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), 909); + this.noticeScrapInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), NOTICE_BOARD_ID); this.departmentName = BIO_SCIENCE; - this.noticeGraduationInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), 905); + this.noticeGraduationInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), GRADUATION_BOARD_ID);클래스 내부에 상수 추가(선택, 범위 밖 보조 코드):
private static final int NOTICE_BOARD_ID = 909; private static final int GRADUATION_BOARD_ID = 905;src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java (1)
31-31: 졸업 공지 보드 ID(417) 상수화 권장 — 소비 지점 및 graduated 전파 확인됨매직 넘버 417을 의미 있는 상수로 추출하세요. 검증 결과 noticeGraduationInfo는 실제로 소비되며 graduated 플래그가 도메인/저장/쿼리 계층까지 전파됩니다.
- 문제 위치: src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java — this.noticeGraduationInfo = new NoticeScrapInfo(BIOLOGICAL.getHostPrefix(), 417);
- 소비·전파 확인 지점(참고):
- src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java (createGraduateListUrl/createGraduateViewUrl, isSupportGraduateScrap)
- src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java (convert(...)에서 graduated 설정)
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java (쿼리에서 graduated 필터)
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java (INSERT에 graduated 컬럼)
권장 변경 예시:
- this.noticeGraduationInfo = new NoticeScrapInfo(BIOLOGICAL.getHostPrefix(), 417); + this.noticeGraduationInfo = new NoticeScrapInfo(BIOLOGICAL.getHostPrefix(), BIO_GRADUATION_BOARD_ID);클래스 상단에 상수 추가 예시:
private static final int BIO_GRADUATION_BOARD_ID = 417;추가 검증(선택): 417이 외부 학과 사이트에서 실제로 “졸업/학사(졸업)” 게시판에 해당하는지 외부 확인 권장.
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java (1)
31-31: 매직 넘버 상수화 제안(가독성/유지보수성).
709를 의미 있는 상수로 치환해 주세요. 동일 파일의353도 같은 기준으로 정리 추천합니다.적용 예시(선택 1: 로컬 상수):
- this.noticeGraduationInfo = new NoticeScrapInfo(CHINESE.getHostPrefix(), 709); + this.noticeGraduationInfo = new NoticeScrapInfo(CHINESE.getHostPrefix(), GRAD_NOTICE_BBS_ID);클래스 상단(필드부)에 다음을 추가:
private static final int GRAD_NOTICE_BBS_ID = 709; // 중문과 졸업/대학원 공지 게시판 ID선택 2(권장): 공통 모듈(예: NoticeBoardIds.CHINESE.GRAD)로 중앙집중 상수화하여 타 학과와 동일 패턴 유지.
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java (1)
27-31: 매직 넘버(407, 767) 상수화 제안보드 ID를 상수로 추출하면 가독성↑/오입력↓/다른 학과와의 비교가 쉬워집니다. 반복 패턴이 많다면 상수/맵을 상위 레벨로 공통화하는 것도 고려해 주세요.
@RegisterDepartmentMap(key = ELEC_ELEC) public class ElectricalElectronicsDept extends EngineeringCollege { + private static final int NOTICE_BOARD_ID = 407; + private static final int GRADUATION_BOARD_ID = 767; + public ElectricalElectronicsDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, LatestPageNoticeProperties latestPageNoticeProperties ) { super(); this.noticeApiClient = latestPageNoticeApiClient; this.htmlParser = latestPageNoticeHtmlParser; this.latestPageNoticeProperties = latestPageNoticeProperties; List<Integer> siteIds = List.of(10974); this.staffScrapInfo = new StaffScrapInfo(ELEC_ELEC.getHostPrefix(), siteIds); - this.noticeScrapInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), 407); + this.noticeScrapInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), NOTICE_BOARD_ID); this.departmentName = ELEC_ELEC; - this.noticeGraduationInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), 767); + this.noticeGraduationInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), GRADUATION_BOARD_ID); } }src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java (1)
31-31: 매직 넘버(475) 상수화 제안가독성과 유지보수성을 위해 보드 ID는 상수로 분리해 주세요. 동일 파일 내 다른 ID(929, 11029)도 추후 일괄 상수화 권장.
- this.noticeGraduationInfo = new NoticeScrapInfo(FOOD_MARKETING.getHostPrefix(), 475); + this.noticeGraduationInfo = new NoticeScrapInfo(FOOD_MARKETING.getHostPrefix(), GRAD_NOTICE_BOARD_ID);클래스 상단(필드 선언부) 예시:
private static final int GRAD_NOTICE_BOARD_ID = 475;src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java (1)
31-31: 매직 넘버(990) 상수화 및 호스트·보드 매핑 검증 권고
- 위치: src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java:31.
- 조치: 990을 상수(예: NoticeBoardIds.GRADUATION)로 추출하고, ENGLISH_EDU.getHostPrefix()가 siteId=990을 실제로 제공하는지(부서 호스트 vs 중앙 포털 혼재 여부) 확인해서 URL 생성(createGraduateViewUrl/createGraduateSearchUrl)에 영향 없는지 검증.
- 용어: noticeGraduationInfo(‘graduation’)와 API/DTO상의 'graduated' 혼용으로 의미 혼동 소지 있으므로 변수/파라미터명 통일 검토.
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java (1)
31-31: 매직 넘버(602) 상수화 제안
- 유지보수성과 오타 방지를 위해 보드 ID를 상수로 추출하는 것을 권장합니다.
아래처럼 적용하면 됩니다.
public class MediaCommunicationDept extends LiberalArtCollege { + private static final int GRADUATION_BOARD_ID = 602; + public MediaCommunicationDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, LatestPageNoticeProperties latestPageNoticeProperties ) { super(); this.noticeApiClient = latestPageNoticeApiClient; this.htmlParser = latestPageNoticeHtmlParser; this.latestPageNoticeProperties = latestPageNoticeProperties; List<Integer> siteIds = List.of(7895); this.staffScrapInfo = new StaffScrapInfo(MEDIA_COMM.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(MEDIA_COMM.getHostPrefix(), 375); this.departmentName = MEDIA_COMM; - this.noticeGraduationInfo = new NoticeScrapInfo(MEDIA_COMM.getHostPrefix(), 602); + this.noticeGraduationInfo = new NoticeScrapInfo(MEDIA_COMM.getHostPrefix(), GRADUATION_BOARD_ID); } }src/main/java/com/kustacks/kuring/report/adapter/in/web/ReportCommandApiV2.java (2)
35-38: 요청 바디 Bean Validation 활성화 및 반환 타입 제네릭 명시클래스 수준
@Validated만으로는@RequestBody에 대한 Bean Validation이 자동 수행되지 않습니다. 파라미터에@Valid를 추가하세요. 또한ResponseEntity원시 타입 대신ResponseEntity<?>를 사용해 경고를 줄이는 것이 좋습니다.- public ResponseEntity report( + public ResponseEntity<?> report( @RequestHeader(FCM_TOKEN_HEADER_KEY) String userToken, - @RequestBody ReportRequest request + @RequestBody @jakarta.validation.Valid ReportRequest request ) {
29-29: 헤더 키 상수명 오해 소지 — 의미에 맞게 리네이밍 제안상수명이
FCM_TOKEN_HEADER_KEY인데 실제 값은"User-Token"입니다. 의미 불일치로 혼란을 줄 수 있어 리네이밍을 권장합니다.- private static final String FCM_TOKEN_HEADER_KEY = "User-Token"; + private static final String USER_TOKEN_HEADER_KEY = "User-Token"; ... - @RequestHeader(FCM_TOKEN_HEADER_KEY) String userToken, + @RequestHeader(USER_TOKEN_HEADER_KEY) String userToken,Also applies to: 36-36
src/main/java/com/kustacks/kuring/auth/AuthConfig.java (3)
47-52: 프로덕션/개발 오리진 분리 및 프로퍼티화 권장도메인 하드코딩 대신 프로파일/프로퍼티 기반(예:
application-*.yml)으로 관리하면 운영 도메인 변경·서브도메인 추가 시 재배포 없이 대응 가능합니다. 로컬(localhost,127.0.0.1)은 개발용 프로파일로 한정하는 것을 권장합니다.
54-54: 허용 헤더 와일드카드 축소 권장
"*"는 편리하지만 과도합니다. 실제 필요한 헤더만 명시(예:Authorization,Content-Type,X-Requested-With)하여 표면을 줄이세요.다음과 같이 축소를 제안합니다:
- config.setAllowedHeaders(List.of("*")); // 모든 요청 헤더 허용 + config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
53-53: 허용 메서드에 HEAD 추가 고려일부 클라이언트/프록시가 HEAD 요청을 먼저 보낼 수 있습니다. 안전한 범위 내에서 허용을 권장합니다.
- config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedMethods(List.of("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java (1)
27-31: 매직 넘버(10902, 914, 469) 상수화 + 지역 변수 제거 제안가독성과 유지보수성 향상을 위해 보드/사이트 ID를 클래스 상수로 승격하고, 단발성 지역 변수는 제거하는 편을 권장합니다.
적용 diff(해당 범위 내 변경):
- List<Integer> siteIds = List.of(10902); - this.staffScrapInfo = new StaffScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), siteIds); - this.noticeScrapInfo = new NoticeScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), 914); - this.departmentName = ANIMAL_SCIENCE; - this.noticeGraduationInfo = new NoticeScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), 469); + this.staffScrapInfo = new StaffScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), SITE_IDS); + this.noticeScrapInfo = new NoticeScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), NOTICE_BOARD_ID); + this.departmentName = ANIMAL_SCIENCE; + this.noticeGraduationInfo = new NoticeScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), GRAD_NOTICE_BOARD_ID);클래스 상단(필드 영역)에 아래 상수 추가:
private static final List<Integer> SITE_IDS = List.of(10902); private static final int NOTICE_BOARD_ID = 914; private static final int GRAD_NOTICE_BOARD_ID = 469;src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java (1)
31-31: 졸업 공지 스크랩 설정 추가 OK — 매직 넘버 정리 제안411 ID의 정확성만 검증해 주세요. 유사 변경이 다수 파일에 반복되므로 게시판 ID를 상수/매핑(예: enum 또는 properties)로 중앙집중 관리하는 것을 권장합니다.
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java (1)
31-31: 대학원 공지 스크랩 소스(boardId=427) 추가 OK — 상수/구성 분리 추천매직 넘버(427)는 다수 학과에 중복될 가능성이 커 보입니다. 학과별 graduate 게시판 ID를 중앙 맵/설정으로 관리하면 유지보수성이 좋아집니다(예: enum/맵 또는 yml 프로퍼티 로딩).
해당 게시판 ID(427)가 실제 대학원 공지 보드가 맞는지 1회 확인 부탁드립니다.
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java (1)
31-31: Locale 민감도: toUpperCase는 Locale.ROOT 사용 권장카테고리/학과명을 대문자로 고정할 때는 Locale.ROOT를 지정해 터키어 등 로케일 이슈를 회피하세요.
권장 수정(diff):
- ps.setString(2, notice.getCategoryName().toUpperCase()); + ps.setString(2, notice.getCategoryName().toUpperCase(java.util.Locale.ROOT)); ... - ps.setString(2, departmentNotice.getCategoryName().toUpperCase()); + ps.setString(2, departmentNotice.getCategoryName().toUpperCase(java.util.Locale.ROOT));Also applies to: 59-59
src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java (3)
42-43: 미사용 빈 제거LatestPageNoticeApiClient는 주입만 되고 사용되지 않습니다. 제거해 테스트 가독성과 노이즈를 줄이세요.
- @Autowired - private LatestPageNoticeApiClient latestPageNoticeApiClient;
49-53: 테스트 픽스처 로딩을 클래스패스 리소스로 전환 권장직접 파일 경로는 실행 경로 의존성이 생깁니다. classpath 리소스로 읽으면 더 견고합니다.
예시:
String html = new String( getClass().getResourceAsStream("/notice/graduate-cse-notice.html").readAllBytes(), java.nio.charset.StandardCharsets.UTF_8 ); Document doc = Jsoup.parse(html);
52-52: 모킹 범위 좁히기(선택)anyString/anyInt 대신 예상 URL/timeout으로 구체화하면 회귀 시 원인 파악이 쉬워집니다.
src/test/java/com/kustacks/kuring/acceptance/NoticeStep.java (1)
44-56: queryParam 사용 권장현재 pathParam을 쿼리스트링 치환에 사용 중입니다. queryParam 사용이 의도에 더 명확합니다.
적용 제안(diff):
- return RestAssured - .given().log().all() - .pathParam("type", category) - .pathParam("department", hostPrefix) - .pathParam("important", important) - .pathParam("graduated", graduated) - .pathParam("page", String.valueOf(page)) - .pathParam("size", "10") - .when().get("/api/v2/notices?type={type}&department={department}&important={important}&graduated={graduated}&page={page}&size={size}") + return RestAssured + .given().log().all() + .queryParam("type", category) + .queryParam("department", hostPrefix) + .queryParam("important", important) + .queryParam("graduated", graduated) + .queryParam("page", page) + .queryParam("size", 10) + .when().get("/api/v2/notices") .then().log().all() .extract();src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql (1)
1-2: graduated 컬럼 추가 OK — 조회 성능용 인덱스 제안대학원 여부 필터가 빈번하다면 복합 인덱스를 고려하세요(특히 학과 공지). DB에 따라 구문은 조정 필요.
예시(PostgreSQL):
-- 학과 공지 조회 가속 CREATE INDEX IF NOT EXISTS idx_notice_dept_graduated_posted_dt ON notice(department_name, graduated, posted_dt DESC); -- 카테고리 공지(필요 시) CREATE INDEX IF NOT EXISTS idx_notice_category_posted_dt ON notice(category_name, posted_dt DESC);또한 카테고리 공지는 graduated=NULL, 학과 공지는 TRUE/FALSE로 정규화하는 정책이라면, 쿼리에서 IS NULL/IS NOT NULL vs = TRUE/FALSE 혼용이 없도록 점검 부탁드립니다.
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java (1)
31-31: 대학원 공지 스크랩 소스(boardId=748) 추가 OK — 매핑 일관성 확인해당 ID가 실제 아키텍처 대학원 공지 보드와 매핑되는지 확인해 주세요. 다수 학과에 동일 패턴이 반복되므로 중앙화(상수/설정)도 함께 고려 바랍니다.
src/main/resources/db/migration/V250918__Add_academic_event_notification_to_user.sql (1)
2-3: NOT NULL + DEFAULT TRUE 적절 — 기존 데이터 백필/검증 권장대부분 DB는 기존 로우에 기본값을 채우지만, 환경별 동작 차이를 감안해 백필 쿼리/검증을 권장합니다. 대량 발송 시 필터링 조건에 사용할 예정이면 인덱스도 검토해 주세요.
검증/보강 예시:
-- (옵션) 기존 로우 백필 보장 UPDATE "user" SET academic_event_notification_enabled = TRUE WHERE academic_event_notification_enabled IS NULL; -- (옵션) 발송 대상 조회 최적화 CREATE INDEX IF NOT EXISTS idx_user_academic_event_notify_true ON "user"(academic_event_notification_enabled);src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java (1)
26-26: StaffScrapInfo 초기화에서 하드코딩된 값 사용다른 학과들은
DEPARTMENT_NAME.getHostPrefix()를 사용하는 반면, 이 클래스에서는 "kure"라는 하드코딩된 문자열을 사용하고 있습니다. 일관성을 위해REAL_ESTATE.getHostPrefix()를 사용하는 것이 좋겠습니다.다음과 같이 수정하는 것을 고려해보세요:
- this.staffScrapInfo = new StaffScrapInfo("kure", siteIds); + this.staffScrapInfo = new StaffScrapInfo(REAL_ESTATE.getHostPrefix(), siteIds);src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeRangeLookupResponse.java (1)
13-15: graduated 필드 추가 — null 안전성 보강 제안외부 응답 스키마에 Boolean graduated가 추가되었습니다. 상류에서 null이 올 수 있다면 클라이언트 호환성을 위해 from 매핑에서 기본값(false)을 적용하는 편이 안전합니다.
- result.important(), - result.graduated(), + result.important(), + (result.graduated() != null ? result.graduated() : Boolean.FALSE), result.commentCount()상류 DTO/엔티티의 graduated 기본값이 NOT NULL로 보장되는지(마이그레이션 default/backfill) 확인 부탁드립니다.
Also applies to: 25-26
src/test/java/com/kustacks/kuring/user/domain/UserTest.java (1)
189-208: 토글 테스트 보강 제안: 기본값(assert) 추가생성 직후 기본값(true)을 한 줄 확인하면 테스트 의도가 더 명확해집니다.
// given - true User user = createUser(1L, "token"); + assertThat(user.getAcademicEventNotificationEnabled()).isTrue();null 입력 시 동작(예외/허용)에 대한 기대를 명확히 하고, 그에 맞는 테스트를 추가하는 것도 권장합니다.
src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java (1)
201-201: 인덱싱 전 사이즈 가드 추가 제안alerts가 비어있을 경우 테스트가 취약합니다. 크기 검증 후 인덱싱하세요.
- int alertId = 예약_알림_조회(accessToken).jsonPath().getInt("data.alerts[0].id"); + var 조회응답 = 예약_알림_조회(accessToken); + assertThat(조회응답.jsonPath().getList("data.alerts")).isNotEmpty(); + int alertId = 조회응답.jsonPath().getInt("data.alerts[0].id");src/main/java/com/kustacks/kuring/user/domain/User.java (1)
64-67: 학사일정 알림 필드 추가 — DB 기본값/백필 확인 및 불필요한 래퍼 제거 검토
- @column(nullable = false)인데 Boolean 래퍼를 사용하면 null 주입 여지가 남습니다. primitive boolean으로 전환을 검토해 주세요(스키마/마이그레이션이 NOT NULL + DEFAULT TRUE를 보장한다는 전제).
- JPA DDL을 쓰지 않는다면 마이그레이션에서 default true 및 기존 로우 백필이 수행됐는지 확인 필요.
src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java (1)
86-90: null 안전성 보완 권장 (enabled가 Wrapper Boolean)웹 레이어에서 @NotNull 검증이 누락될 가능성 대비, 서비스에서도 방어 로직을 추가해 주세요.
다음과 같이 빠르게 보강 가능합니다:
@@ @Override public void updateAcademicEventNotification(UserAcademicEventNotificationCommand command) { - User user = findUserByToken(command.userToken()); - user.updateAcademicNotificationEnabled(command.enabled()); + if (command.enabled() == null) { + throw new IllegalArgumentException("enabled must not be null"); + } + User user = findUserByToken(command.userToken()); + user.updateAcademicNotificationEnabled(command.enabled()); }또는 커맨드의 타입을
boolean으로 바꾸는 방식을 아래 DTO 코멘트에 제안했습니다.src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java (1)
25-36: Page 기반 페이지네이션 전환 적절 + 읽기 전용 트랜잭션 제안QueryDSL 쿼리/카운트 분리와 PageableExecutionUtils 사용 적절합니다. 읽기 경로 최적화를 위해 메서드에 readOnly 트랜잭션을 권장합니다.
@@ - public Page<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable) { + @Transactional(readOnly = true) + public Page<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable) { var query = queryFactory.select(new QFeedbackDto(feedback.content, feedback.user.id, feedback.createdAt)) .from(feedback) .orderBy(feedback.createdAt.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()); var countQuery = queryFactory.select(feedback.count()) .from(feedback); return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchOne); }src/main/java/com/kustacks/kuring/user/application/port/in/dto/UserAcademicEventNotificationCommand.java (1)
3-6: 서비스 계층 null 방어를 단순화하려면 primitive 사용 고려애플리케이션 포트 DTO에서는
boolean사용을 권장합니다(웹 검증과 무관하게 NPE 리스크 제거).-public record UserAcademicEventNotificationCommand( - String userToken, - Boolean enabled -) { +public record UserAcademicEventNotificationCommand( + String userToken, + boolean enabled +) { }기존 호출부는 오토언박싱으로 그대로 동작합니다(웹 DTO에서 @NotNull 유지).
src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java (2)
43-45: Page → List 변환은 getContent()가 더 명확Streamable 경유보다 의도가 분명합니다.
- List<FeedbackDto> feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(PageRequest.of(0, 3)) - .stream().toList(); + List<FeedbackDto> feedbackDtos = userPersistenceAdapter + .findAllFeedbackByPageRequest(PageRequest.of(0, 3)) + .getContent();
66-68: 검증 의도 명확화 제안페이지 콘텐츠 크기 vs 총건수 중 무엇을 검증하는지 명시해 주세요. 총건수 검증 시 아래처럼 권장합니다.
- Page<FeedbackDto> feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(PageRequest.of(0, 10)); - assertThat(feedbackDtos).hasSize(5); + Page<FeedbackDto> feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(PageRequest.of(0, 10)); + assertThat(feedbackDtos.getTotalElements()).isEqualTo(5);src/test/java/com/kustacks/kuring/worker/scrap/KuisHomepageNoticeScraperTemplateTest.java (1)
71-73: NoticeDto 생성자 변경 반영 OK, 카테고리 값은 실제 상수로 사용하는 것을 권장graduated 플래그(false) 추가 반영은 적절합니다. 다만 테스트 안정성을 위해 category에 임의 문자열 "category" 대신 실제 도메인 상수 값(e.g., CategoryName.BACHELOR.name())을 사용하면 후속 스키마/검증 변경에 덜 취약합니다.
적용 예:
- NoticeDto noticeDto = new NoticeDto(1L, "1", "2024-01-01 00:00:00", - "http://example.com", "제목", "category", true, false, 2L); + NoticeDto noticeDto = new NoticeDto(1L, "1", "2024-01-01 00:00:00", + "http://example.com", "제목", CategoryName.BACHELOR.name(), true, false, 2L);src/main/java/com/kustacks/kuring/notice/application/port/out/dto/NoticeDto.java (1)
30-31: graduated 필드에 @nullable 명시 — null 의미(미지정/전체) 계약 강화; QNoticeDto 호출부는 이미 시그니처와 일치함DTO 필드와 생성자 파라미터에 @nullable을 추가하고 필드 주석으로 null 의미를 남기세요. new QNoticeDto 호출부(NoticeQueryRepositoryImpl 및 테스트)는 현재 Expressions.nullExpression() 또는 departmentNotice.graduated를 전달해 시그니처와 일치합니다.
package com.kustacks.kuring.notice.application.port.out.dto; +import org.springframework.lang.Nullable; @@ - private Boolean graduated; + @Nullable + private Boolean graduated; @@ - String category, Boolean important, Boolean graduated, Long commentCount + String category, Boolean important, @Nullable Boolean graduated, Long commentCount ) { @@ - this.important = important; - this.graduated = graduated; + this.important = important; + this.graduated = graduated; // null: 미지정(전체)src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java (1)
102-106: 학과 일반 공지 조회에 graduated 필터 추가 OK동일하게 일치합니다. graduated=true 케이스도 추후 보강하면 커버리지가 좋아집니다.
src/test/java/com/kustacks/kuring/acceptance/UserStep.java (1)
340-347: expectedEnabled 파라미터 미사용 — 호출부 존재(수정 필요)UserStep.java (라인 ~340)에서 expectedEnabled가 사용되지 않습니다. 호출부: src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java:518, 524.
- 옵션 A — 파라미터 제거(응답이 항상 data=null이면): UserStep.java 시그니처에서 expectedEnabled 제거 후 호출부의 두 번째 인자(false/true) 삭제.
- 옵션 B — 검증 추가(응답에서 enabled를 반환할 가능성이 있으면): 메서드에 response의 enabled 값을 추출해 expectedEnabled와 비교하는 assertion을 추가(단, data가 null일 경우 기존 null 검증 유지).
src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java (4)
49-58: 비동기 파이프라인 예외 전파/로그 누락.
supplyAsync→thenApply→thenAccept체인에exceptionally가 없어 실패 시 침묵합니다. 로깅을 추가해 장애 분석 가능성을 높이세요.아래처럼 예외 처리 추가를 권장합니다.
).thenAccept( notificationService::sendNotifications - ); + ).exceptionally(ex -> { + log.error("학과별(대학원) 업데이트 실패 - dept={}", deptInfo.getDeptName(), ex); + return null; + });Also applies to: 73-76
94-113: 중복 DB 조회 제거로 성능 개선.동일 학과에 대해 페이지 루프마다 saved IDs를 매번 조회합니다. 루프 밖에서 1회 조회 후 재사용하세요.
- List<DepartmentNotice> newNoticeList = new ArrayList<>(); - for (ComplexNoticeFormatDto scrapResult : scrapResults) { - - // DB에서 모든 중요 공지를 가져와서 - List<Integer> savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum, true); - - // db와 싱크를 맞춘다 - List<DepartmentNotice> newImportantNotices = saveNewNotices(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true, true); - newNoticeList.addAll(newImportantNotices); - - // DB에서 모든 일반 공지 id를 가져와서 - List<Integer> savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum, true); - - // db와 싱크를 맞춘다 - List<DepartmentNotice> newNormalNotices = saveNewNotices(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false, true); - newNoticeList.addAll(newNormalNotices); - } + List<DepartmentNotice> newNoticeList = new ArrayList<>(); + List<Integer> savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum, true); + List<Integer> savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum, true); + for (ComplexNoticeFormatDto scrapResult : scrapResults) { + newNoticeList.addAll( + saveNewNotices(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true, true) + ); + newNoticeList.addAll( + saveNewNotices(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false, true) + ); + }
115-119: 파라미터 명칭 일관성(naming).
graduate→graduated로 명칭 일치시키면 가독성이 좋아집니다(타 전역 필드/쿼리와 동일 용어).-private List<DepartmentNotice> saveNewNotices(..., boolean important, boolean graduate) { - List<DepartmentNotice> newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(..., important, graduate); +private List<DepartmentNotice> saveNewNotices(..., boolean important, boolean graduated) { + List<DepartmentNotice> newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(..., important, graduated);-private void synchronizationWithDb(..., boolean important, boolean graduate) { - List<DepartmentNotice> newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(..., important, graduate); +private void synchronizationWithDb(..., boolean important, boolean graduated) { + List<DepartmentNotice> newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(..., important, graduated);Also applies to: 144-149
61-79: Feature flag 범위 분리 고려(옵션).전체 업데이트 플래그가 학사/대학원 모두에 공통이면 운영 제어가 거칠어질 수 있습니다.
UPDATE_DEPARTMENT_GRADUATE_NOTICE등으로 분리하면 롤백·AB 테스트에 유리합니다.src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java (1)
56-58: graduated 플래그 전달은 적절. 정렬 전제 제거(=Set 멤버십 확인)로 안전성/성능 개선 제안savedArticleIds가 정렬되어 있다는 전제가 깨지면 binarySearch 결과가 잘못될 수 있습니다. 해시셋 멤버십 확인으로 바꾸면 정렬 의존성이 사라지고 평균 O(1) 성능입니다. 또한 append‑only 용도면 ArrayList가 더 적합합니다.
적용 예시:
- List<DepartmentNotice> newNotices = new LinkedList<>(); // 뒤에 추가만 계속 하기 때문에 arrayList가 아닌 Linked List 사용 O(1) + List<DepartmentNotice> newNotices = new ArrayList<>(scrapResults.size()); + Set<Integer> savedIdSet = new HashSet<>(savedArticleIds); - if (Collections.binarySearch(savedArticleIds, Integer.valueOf(notice.getArticleId())) < 0) { // 정렬되어있다, 이진탐색으로 O(logN)안에 수행 + Integer articleId = Integer.parseInt(notice.getArticleId()); + if (!savedIdSet.contains(articleId)) { DepartmentNotice newDepartmentNotice = convert(notice, departmentNameEnum, important, graduated); newNotices.add(newDepartmentNotice); }추가 import:
import java.util.ArrayList; import java.util.HashSet; import java.util.Set;Also applies to: 63-63
src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java (1)
134-134: 테이블명 백틱 감싸기 고려예약어/특수문자 테이블명 대비해 백틱으로 감싸는 것이 안전합니다(테스트 환경이라도 습관화 권장).
- jdbcTemplate.execute("ALTER TABLE " + tableName + " CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); + jdbcTemplate.execute("ALTER TABLE `" + tableName + "` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java (1)
103-110: 부서 기사 ID 조회도 graduated 적용 일관성 유지ID 조회 경로에도 동일 플래그가 반영되어 좋습니다. 호출부 정렬 의존 제거(Set 사용)와 함께 쓰면 더 안전합니다.
src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java (4)
26-28: 대학원 스크랩 의존성/널 세이프티 확보 필요noticeGraduationInfo 또는 latestPageGraduateNoticeApiClient 주입 누락 시 NPE 위험. 최소한 가드(지원 안 하면 no-op) 또는 명시적 예외를 추가하세요.
38-44: 대학원 스크랩 엔트리포인트에 가드 추가지원하지 않는 학과에서 호출되면 NPE가 납니다. isSupportGraduateScrap() 체크 후 미지원 시 빈 리스트 반환을 권장합니다.
[Suggest diff]- public List<ScrapingResultDto> scrapGraduateLatestPageHtml() { - return latestPageGraduateNoticeApiClient.request(this); - } + public List<ScrapingResultDto> scrapGraduateLatestPageHtml() { + if (!isSupportGraduateScrap()) { + return java.util.Collections.emptyList(); + } + return latestPageGraduateNoticeApiClient.request(this); + } @@ - public List<ScrapingResultDto> scrapGraduateAllPageHtml() { - return latestPageGraduateNoticeApiClient.requestAll(this); - } + public List<ScrapingResultDto> scrapGraduateAllPageHtml() { + if (!isSupportGraduateScrap()) { + return java.util.Collections.emptyList(); + } + return latestPageGraduateNoticeApiClient.requestAll(this); + }추가 import가 필요 없습니다(정규명 사용).
67-77: UG/GR URL 빌더 중복 제거로직이 동일하고 대상 NoticeScrapInfo만 다릅니다. 중복을 줄여 유지보수성을 높이세요.
+ private String createRequestUrl(NoticeScrapInfo info, int page, int row) { + return UriComponentsBuilder + .fromUriString(latestPageNoticeProperties.listUrl()) + .queryParam("page", page) + .queryParam("row", row) + .buildAndExpand( + info.getSiteName(), + info.getSiteName(), + info.getSiteId() + ).toUriString(); + } + + private String createViewUrl(NoticeScrapInfo info) { + return latestPageNoticeProperties.viewUrl() + .replaceAll("\\{department\\}", info.getSiteName()) + .replace("{siteId}", String.valueOf(info.getSiteId())); + } @@ - public String createUndergraduateRequestUrl(int page, int row) { - return UriComponentsBuilder - .fromUriString(latestPageNoticeProperties.listUrl()) - .queryParam("page", page) - .queryParam("row", row) - .buildAndExpand( - noticeScrapInfo.getSiteName(), - noticeScrapInfo.getSiteName(), - noticeScrapInfo.getSiteId() - ).toUriString(); - } + public String createUndergraduateRequestUrl(int page, int row) { + return createRequestUrl(noticeScrapInfo, page, row); + } @@ - public String createUndergraduateViewUrl() { - return latestPageNoticeProperties.viewUrl() - .replaceAll("\\{department\\}", noticeScrapInfo.getSiteName()) - .replace("{siteId}", String.valueOf(noticeScrapInfo.getSiteId())); - } + public String createUndergraduateViewUrl() { + return createViewUrl(noticeScrapInfo); + } @@ - public String createGraduateRequestUrl(int page, int row) { - return UriComponentsBuilder - .fromUriString(latestPageNoticeProperties.listUrl()) - .queryParam("page", page) - .queryParam("row", row) - .buildAndExpand( - noticeGraduationInfo.getSiteName(), - noticeGraduationInfo.getSiteName(), - noticeGraduationInfo.getSiteId() - ).toUriString(); - } + public String createGraduateRequestUrl(int page, int row) { + return createRequestUrl(noticeGraduationInfo, page, row); + } @@ - public String createGraduateViewUrl() { - return latestPageNoticeProperties.viewUrl() - .replaceAll("\\{department\\}", noticeGraduationInfo.getSiteName()) - .replace("{siteId}", String.valueOf(noticeGraduationInfo.getSiteId())); - } + public String createGraduateViewUrl() { + return createViewUrl(noticeGraduationInfo); + }Also applies to: 79-83, 85-96, 97-101
107-109: 지원 여부 판단에 클라이언트 존재도 포함noticeGraduationInfo만 확인하면 주입 누락 시 런타임 오류 가능. 클라이언트까지 함께 확인하세요.
- public boolean isSupportGraduateScrap() { - return this.noticeGraduationInfo != null; - } + public boolean isSupportGraduateScrap() { + return this.noticeGraduationInfo != null && this.latestPageGraduateNoticeApiClient != null; + }src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java (2)
22-26: 하드코딩된 페이징/타임아웃을 프로퍼티로 외부화운영 중 조정이 필요합니다. properties/FeatureFlag로 주입받도록 변경을 권장합니다.
67-77: 총 게시글 수 파싱 견고성 강화천 단위 구분자/공백 등으로 Integer.parseInt가 실패할 수 있습니다. 숫자만 추출해 파싱하세요.
- assert totalNoticeSizeElement != null; - return Integer.parseInt(totalNoticeSizeElement.ownText()); + assert totalNoticeSizeElement != null; + String digits = totalNoticeSizeElement.ownText().replaceAll("[^0-9]", ""); + return Integer.parseInt(digits);src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java (1)
65-65: DTO 투영의 널 패딩은 타입 지정 권장Expressions.nullExpression() 대신 명시적 타입으로 지정하면 제네릭/런타임 타입 안정성이 높아집니다.
- Expressions.nullExpression(), + Expressions.nullExpression(Boolean.class), @@ - Expressions.nullExpression(), + Expressions.nullExpression(Boolean.class), @@ - Expressions.nullExpression(), + Expressions.nullExpression(Boolean.class),Also applies to: 129-129, 161-161
src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java (1)
83-105: 중복 DB 조회 최소화 — 부서별 article id 조회를 루프 외부로 이동scrapperTemplate.scrap이 List를 반환(여러 스크랩 결과 가능)하므로 현재 for 루프마다 동일 부서의 noticeQueryPort.findImportantArticleIdsByDepartment / findNormalArticleIdsByDepartment를 반복 호출합니다. savedImportantArticleIds와 savedNormalArticleIds 조회를 for 루프 바깥으로 옮겨 호출 수를 줄이세요.
위치: src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.compareLatestAndUpdateDB (대략 83–105, 118–132).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (93)
src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminQueryApiV2.java(3 hunks)src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminAlertListResponse.java(1 hunks)src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminFeedbackListResponse.java(1 hunks)src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminReportListResponse.java(1 hunks)src/main/java/com/kustacks/kuring/admin/application/port/in/AdminQueryUseCase.java(1 hunks)src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java(1 hunks)src/main/java/com/kustacks/kuring/admin/application/service/AdminQueryService.java(2 hunks)src/main/java/com/kustacks/kuring/auth/AuthConfig.java(4 hunks)src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java(1 hunks)src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java(1 hunks)src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java(1 hunks)src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeRangeLookupResponse.java(2 hunks)src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java(6 hunks)src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java(2 hunks)src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java(1 hunks)src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java(6 hunks)src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupCommand.java(1 hunks)src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupResult.java(1 hunks)src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeQueryPort.java(1 hunks)src/main/java/com/kustacks/kuring/notice/application/port/out/dto/NoticeDto.java(2 hunks)src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java(3 hunks)src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java(1 hunks)src/main/java/com/kustacks/kuring/notice/domain/DepartmentNotice.java(2 hunks)src/main/java/com/kustacks/kuring/report/adapter/in/web/ReportCommandApiV2.java(2 hunks)src/main/java/com/kustacks/kuring/report/application/port/in/dto/AdminReportsResult.java(2 hunks)src/main/java/com/kustacks/kuring/report/domain/Report.java(2 hunks)src/main/java/com/kustacks/kuring/user/adapter/in/web/UserCommandApiV2.java(5 hunks)src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserAcademicEventNotificationRequest.java(1 hunks)src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java(2 hunks)src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java(1 hunks)src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java(2 hunks)src/main/java/com/kustacks/kuring/user/application/port/in/UserCommandUseCase.java(2 hunks)src/main/java/com/kustacks/kuring/user/application/port/in/dto/UserAcademicEventNotificationCommand.java(1 hunks)src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java(2 hunks)src/main/java/com/kustacks/kuring/user/domain/User.java(2 hunks)src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageNoticeApiClient.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java(5 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/CommunicationDesignDept.java(0 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java(2 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java(2 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/KBeautyIndustryFusionDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_elective/VolunteerCenterDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java(1 hunks)src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java(1 hunks)src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java(3 hunks)src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java(3 hunks)src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql(1 hunks)src/main/resources/db/migration/V250918__Add_academic_event_notification_to_user.sql(1 hunks)src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java(3 hunks)src/test/java/com/kustacks/kuring/acceptance/AdminStep.java(2 hunks)src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java(4 hunks)src/test/java/com/kustacks/kuring/acceptance/NoticeStep.java(2 hunks)src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java(2 hunks)src/test/java/com/kustacks/kuring/acceptance/UserStep.java(2 hunks)src/test/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeRepositoryTest.java(1 hunks)src/test/java/com/kustacks/kuring/notice/domain/DepartmentNoticeTest.java(2 hunks)src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java(3 hunks)src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java(3 hunks)src/test/java/com/kustacks/kuring/user/domain/UserTest.java(2 hunks)src/test/java/com/kustacks/kuring/worker/scrap/KuisHomepageNoticeScraperTemplateTest.java(1 hunks)src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java(1 hunks)src/test/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupportTest.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/CommunicationDesignDept.java
🧰 Additional context used
🧬 Code graph analysis (7)
src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java (1)
src/test/java/com/kustacks/kuring/acceptance/UserStep.java (1)
UserStep(23-349)
src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java (1)
src/test/java/com/kustacks/kuring/support/TestFileLoader.java (1)
TestFileLoader(8-15)
src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java (2)
src/main/java/com/kustacks/kuring/common/exception/InternalLogicException.java (1)
InternalLogicException(5-13)src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageNoticeApiClient.java (1)
Slf4j(18-97)
src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java (2)
src/test/java/com/kustacks/kuring/acceptance/CategoryStep.java (1)
CategoryStep(14-82)src/test/java/com/kustacks/kuring/acceptance/NoticeStep.java (1)
NoticeStep(13-188)
src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java (3)
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java (1)
RequiredArgsConstructor(32-401)src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java (1)
Slf4j(29-148)src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java (1)
Slf4j(16-135)
src/main/java/com/kustacks/kuring/notice/domain/DepartmentNotice.java (2)
src/main/java/com/kustacks/kuring/notice/application/port/out/dto/NoticeDto.java (1)
Getter(9-57)src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java (1)
Getter(16-116)
src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java (1)
src/test/java/com/kustacks/kuring/acceptance/AdminStep.java (1)
AdminStep(13-121)
| List<String> findAllToken(); | ||
| List<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable); | ||
|
|
||
| Page<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable); |
There was a problem hiding this comment.
🧩 Analysis chain
Port 시그니처 변경으로 인한 하위 구현/호출부 전파 확인 필요
List→Page로 변경은 어댑터 구현체, 서비스, 컨트롤러, 응답 DTO 래핑(Page DTO)까지 연쇄 영향입니다. 전파 누락 시 런타임 에러 발생합니다.
다음 스크립트로 구현체/호출부의 시그니처 및 사용처를 점검해 주세요:
🏁 Script executed:
#!/bin/bash
# AdminUserFeedbackPort 구현 및 사용처 점검
set -euo pipefail
echo "== Port 구현 클래스 =="
rg -nP --type=java -C2 '\bimplements\s+AdminUserFeedbackPort\b'
echo -e "\n== 메소드 선언/호출부 =="
rg -nP --type=java -C2 '\bfindAllFeedbackByPageRequest\s*\('
echo -e "\n== 컨트롤러/응답 DTO에서 Page 래핑 여부(예: PageDto, PageResponse 등) =="
rg -nP --type=java -C2 '\bPage(<?)|PageDto|PageResponse|toPage|of\(.*Page<'
echo -e "\n== Admin API 문서/스웨거 스펙 반영 여부 =="
rg -nP --type=java -C2 '(@Operation|@ApiResponse|@Schema).*feedback' || trueLength of output: 17
추가 검증을 위해 다음 스크립트를 실행해 구현체, 호출부, Page 래핑 및 Swagger 스펙 반영 여부를 다시 확인해 주세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Interface 위치 검색 =="
rg -n 'interface\s+AdminUserFeedbackPort'
echo -e "\n== AdminUserFeedbackPort 전체 참조 검색 =="
rg -n 'AdminUserFeedbackPort'
echo -e "\n== 메소드 선언 및 사용 검색 =="
rg -n 'findAllFeedbackByPageRequest'
echo -e "\n== implements 키워드로 구현체 검색 =="
rg -n 'implements.*AdminUserFeedbackPort'
echo -e "\n== 컨트롤러/서비스에서 Page<> 래핑 확인 =="
rg -n 'Page<.*>' -C1 src/main/java/com/kustacks/kuring
echo -e "\n== Swagger 어노테이션에서 feedback 반영 여부 =="
rg -n '(@Operation|@ApiResponse|@Schema).*feedback'Length of output: 9830
확인 완료 — Port 시그니처(Page) 변경이 전파됨
AdminUserFeedbackPort, 구현체 및 호출부에서 Page 반환으로 정상 반영됨 (예: src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java, src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java, src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java, src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java, src/main/java/com/kustacks/kuring/admin/application/service/AdminQueryService.java, src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminFeedbackListResponse.java).
문제: 테스트 코드에 아직 List로 할당한 사용처가 남아 있음 — src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java (List feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(...))를 Page로 변경하거나 .getContent()로 추출하여 수정 필요.
🤖 Prompt for AI Agents
In
src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java
around the line where List<FeedbackDto> feedbackDtos =
userPersistenceAdapter.findAllFeedbackByPageRequest(...); the test still assigns
the port result to a List while the port now returns a Page; update the test to
either (A) change the variable type to Page<FeedbackDto> and adjust assertions
to use page methods (e.g., getContent(), getTotalElements(), getTotalPages()) or
(B) call .getContent() on the returned Page to obtain a List for existing
assertions; also add the required import for
org.springframework.data.domain.Page if you choose option A and update any
size/contains assertions to reference the content or page metadata accordingly.
| // 예를 들어, 프론트에서 Authorization 헤더에 담긴 토큰을 읽어야 할 경우 아래와 같이 설정합니다. | ||
| config.setExposedHeaders(List.of("Authorization", "Location")); | ||
|
|
||
| config.setAllowCredentials(true); |
There was a problem hiding this comment.
allowCredentials=true 재검토 (쿠키 미사용 시 보안 여지)
쿠키/세션을 사용하지 않고 Authorization 헤더만 사용한다면 setAllowCredentials(false)가 원칙적으로 더 안전합니다. 불필요한 크로스 사이트 쿠키 전송을 차단하세요.
다음 변경을 제안합니다:
- config.setAllowCredentials(true);
+ // 쿠키 기반 인증이 필요한 경우에만 true
+ config.setAllowCredentials(false);📝 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.
| config.setAllowCredentials(true); | |
| // 쿠키 기반 인증이 필요한 경우에만 true | |
| config.setAllowCredentials(false); |
🤖 Prompt for AI Agents
In src/main/java/com/kustacks/kuring/auth/AuthConfig.java around line 60, the
CORS config currently calls config.setAllowCredentials(true) which is unsafe if
the app does not use cookies/sessions; change it to
config.setAllowCredentials(false). Update any client code/tests that rely on
sending credentials with cross-origin requests (remove credentials: 'include' or
adjust to same-origin), and ensure Access-Control-Allow-Credentials is no longer
exposed for cross-site requests.
| @Parameter(description = "대학원 여부") @RequestParam(name = "graduated", required = false) Boolean graduated, | ||
| @Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page, | ||
| @Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size | ||
| ) { | ||
| NoticeRangeLookupCommand command = new NoticeRangeLookupCommand(type, department, important, page, size); | ||
| NoticeRangeLookupCommand command = new NoticeRangeLookupCommand(type, department, important, graduated, page, size); | ||
| List<NoticeRangeLookupResponse> searchResults = noticeQueryUseCase.getNotices(command) |
There was a problem hiding this comment.
🧩 Analysis chain
graduated 파라미터의 의미와 null 처리 계약을 명확히 해주세요.
nullable(Boolean)로 전달되어 하위 계층에서 null 의미(예: 전체, 필터 미적용 등)를 어떻게 해석하는지 명시/검증이 필요합니다. 스웨거 설명에도 기본 동작을 문서화하세요. 필요 시 Optional 또는 enum(ALL/UNDERGRAD/GRAD)로의 전환도 고려해볼 만합니다.
아래 스크립트로 graduated 처리 경로를 점검해 주세요(UseCase/Port/Repository에서 null 안전 여부 확인).
🏁 Script executed:
#!/bin/bash
rg -n --type=java -C2 '\bgraduated\b'Length of output: 35847
graduated 파라미터 null 처리 — 긴급 수정 필요
Controller가 @RequestParam(required = false) Boolean graduated로 nullable을 허용하지만, 하위 계층에서 null 검사 없이 departmentNotice.graduated.eq(graduated)를 직접 사용하고 있습니다. graduated == null일 때 의도된 “필터 미적용(전체 조회)”이 아니라 쿼리 조건이 잘못 적용되어 결과가 누락될 위험이 있습니다.
수정(필수)
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java
- 메서드: findImportantArticleIdsByDepartment, findNormalArticleIdsByDepartment, findImportantNoticesByDepartment, findNormalNoticesByDepartmentWithOffset
- 조치: QueryDSL where 절에서 departmentNotice.graduated.eq(graduated) 를 그대로 호출하지 말고 graduated != null 일 때만 predicate를 추가하거나 적절히 isNull()/isTrue()/isFalse()로 분기 처리.
- src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java
- 스웨거/파라미터 설명에 graduated의 의미(예: null = 전체/필터 미적용)를 명확히 문서화하거나 API 계약을 변경(기본값 설정/enum/Optional 사용)하여 ambiguity 제거.
- src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java 및 port 계층
- API 명세에 따라 파라미터 전달 경로 전체를 일관되게 검증/정리.
권장 대안(간단)
- QueryDSL: BooleanExpression graduatedPredicate = graduated == null ? null : departmentNotice.graduated.eq(graduated); 를 사용해 null이면 predicate를 추가하지 않기.
- 또는 API를 Optional 또는 enum(ALL/UNDERGRAD/GRAD)으로 변경해 명시적 계약을 만들기.
🤖 Prompt for AI Agents
In src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java
around lines 52-57 and in the repository/service layers, the nullable
@RequestParam Boolean graduated is passed into QueryDSL as
departmentNotice.graduated.eq(graduated) which breaks when graduated == null;
change the persistence predicates in
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java
(methods: findImportantArticleIdsByDepartment, findNormalArticleIdsByDepartment,
findImportantNoticesByDepartment, findNormalNoticesByDepartmentWithOffset) to
only add a graduated predicate when graduated != null (e.g., build a
BooleanExpression that's null when graduated is null or branch to use
isTrue()/isFalse()), and update the API layer (NoticeQueryApiV2.java)
Swagger/param docs to state that null means "no filter / all", plus adjust
NoticeQueryService and port interfaces to propagate/validate this contract
consistently (or alternatively switch the API to Optional<Boolean> or an
explicit enum to remove ambiguity).
| ps.setBoolean(10, departmentNotice.getGraduated()); | ||
| ps.setString(11, "DepartmentNotice"); | ||
| } |
There was a problem hiding this comment.
NPE 위험: ps.setBoolean에 null 오토언박싱 가능성
DepartmentNotice.getGraduated()가 Boolean(래퍼)라면 null일 때 오토언박싱 NPE가 납니다. null-safe 바인딩으로 교체하세요.
적용 제안(diff):
- ps.setBoolean(10, departmentNotice.getGraduated());
+ Boolean graduated = departmentNotice.getGraduated();
+ if (graduated == null) {
+ ps.setNull(10, Types.BOOLEAN);
+ } else {
+ ps.setBoolean(10, graduated);
+ }📝 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.
| ps.setBoolean(10, departmentNotice.getGraduated()); | |
| ps.setString(11, "DepartmentNotice"); | |
| } | |
| Boolean graduated = departmentNotice.getGraduated(); | |
| if (graduated == null) { | |
| ps.setNull(10, Types.BOOLEAN); | |
| } else { | |
| ps.setBoolean(10, graduated); | |
| } | |
| ps.setString(11, "DepartmentNotice"); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java
around lines 67 to 69, the call ps.setBoolean(10,
departmentNotice.getGraduated()) can NPE if getGraduated() returns a null
Boolean; replace with a null-safe binding: if departmentNotice.getGraduated() is
null call ps.setNull(10, java.sql.Types.BOOLEAN) else call ps.setBoolean(10,
departmentNotice.getGraduated()), or alternatively use ps.setObject(10,
departmentNotice.getGraduated(), java.sql.Types.BOOLEAN) to handle nulls safely.
| List<NoticeDto> findImportantNoticesByDepartment(DepartmentName departmentName, Boolean graduated); | ||
|
|
||
| List<NoticeDto> findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable); | ||
| List<NoticeDto> findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Boolean graduated, Pageable pageable); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
Boolean 매개변수 도입으로 null=무필터 의미 가능 — 구현부 일치 여부 점검 권장
Impl의 where 절이 graduated가 null일 때 조건을 생략하도록 구현됐는지 확인 부탁드립니다.
점검 스크립트:
Also applies to: 32-35
🏁 Script executed:
#!/bin/bash
rg -n -C3 -P 'class\s+NoticeQueryRepositoryImpl\b'
rg -n -C2 -P 'findImportantNoticesByDepartment\([^)]*Boolean\s+graduated' --type=java
rg -n -C2 -P 'findNormalNoticesByDepartmentWithOffset\([^)]*Boolean\s+graduated' --type=java
rg -n -C3 -P '\bgraduated\b.*(isNull|isTrue|isFalse|eq)' --type=javaLength of output: 12711
graduated가 null일 경우 필터를 생략하도록 수정 필요
NoticeQueryRepositoryImpl에서 graduated를 항상 필터로 적용(.and(departmentNotice.graduated.eq(graduated)))하고 있어 "null = 무필터" 의도와 불일치합니다.
위치: src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java — 228, 241, 275, 309.
조치: where(...)에 조건부 predicate를 추가(예: graduated != null ? departmentNotice.graduated.eq(graduated) : null)하거나 Predicate 빌더로 null일 때 생략, 또는 파라미터를 primitive boolean으로 변경하세요.
| if (!deletedNoticesArticleIds.isEmpty()) { | ||
| noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
삭제 쿼리에서 graduated 조건 누락 가능성 — 비졸업 공지까지 오삭제 위험.
현재 deleteAllByIdsAndDepartment(departmentNameEnum, ids)는 graduated 조건이 없어, 동일 articleId를 가진 학사/대학원 공지가 함께 삭제될 수 있습니다. graduated까지 포함하여 정확히 삭제하세요.
적용 예시(이 파일 내 호출부 변경):
- noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds);
+ noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds, true);레포지토리/포트 계층(별도 파일)도 다음처럼 확장해야 합니다.
// Port
void deleteAllByIdsAndDepartment(DepartmentName departmentName, List<String> articleIds, boolean graduated);
// Repository 구현
queryFactory.delete(departmentNotice)
.where(departmentNotice.departmentName.eq(departmentName)
.and(departmentNotice.articleId.in(articleIds))
.and(departmentNotice.graduated.eq(graduated)))
.execute();해당 메서드 다른 호출부도 함께 갱신 필요합니다.
🏁 Script executed:
#!/bin/bash
rg -n --type=java -C2 'deleteAllByIdsAndDepartment\('Length of output: 4410
심각: deleteAllByIdsAndDepartment에 graduated 조건 누락 — 오삭제 위험
deleteAllByIdsAndDepartment(...)가 현재 departmentName + articleIds만으로 삭제하므로 학사/대학원(졸업 여부) 구분 없이 함께 삭제될 수 있습니다. 포트 → 어댑터 → 레포지토리 구현 → 호출부를 모두 졸업 여부(boolean graduated) 파라미터로 확장하고, DELETE 쿼리에 graduated 필터를 추가하세요.
수정 포인트(정확한 위치 확인됨)
- 호출부 변경
- src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java:154
- 변경 예: noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds, true);
- src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java:145
- 해당 호출은 졸업 아님(false) 전달: noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds, false);
- src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java:154
- 포트·어댑터·인터페이스 시그니처 변경
- src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeCommandPort.java:15 — 시그니처에 boolean graduated 추가
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java:43 — repository 호출에 graduated 전달
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java:36 — 시그니처에 boolean graduated 추가
- 레포지토리 구현(쿼리) 수정
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java:318
- articleIds.isEmpty() 체크 유지, DELETE 쿼리에 .and(departmentNotice.graduated.eq(graduated)) 추가
- (예시) queryFactory.delete(departmentNotice).where(departmentNotice.departmentName.eq(departmentName) .and(departmentNotice.articleId.in(articleIds)) .and(departmentNotice.graduated.eq(graduated))).execute();
- src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java:318
해당 호출부는 이미 두 곳(DepartmentNoticeUpdater, DepartmentGraduationNoticeUpdater)에서 사용되고 있으므로 모든 호출을 동기화해야 합니다.
🤖 Prompt for AI Agents
In
src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java
around lines 153-156, the call to
noticeCommandPort.deleteAllByIdsAndDepartment(...) omits the graduated flag
causing potential cross-deletion of undergrad/grad notices; update this call to
pass graduated=true. Also update the parallel call in
src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java
around line 145 to pass graduated=false. Then change the port and adapter
signatures to accept a boolean graduated:
src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeCommandPort.java
(add boolean graduated),
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java
(propagate graduated to repository), and
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java
(add boolean graduated). Finally modify the repository implementation at
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java
(around line 318) to keep the articleIds.isEmpty() guard and include
.and(departmentNotice.graduated.eq(graduated)) in the delete query so the DB
delete filters by departmentName, articleIds and graduated.
| log.info("******** 학과별 (학사) 최신 공지 업데이트 시작 ********"); | ||
|
|
||
| for (DeptInfo deptInfo : deptInfoList) { | ||
| CompletableFuture | ||
| .supplyAsync( | ||
| () -> updateDepartmentAsync(deptInfo, DeptInfo::scrapLatestPageHtml), | ||
| noticeUpdaterThreadTaskExecutor | ||
| ).thenApply( | ||
| scrapResults -> compareLatestAndUpdateDB(scrapResults, deptInfo.getDeptName()) | ||
| ).thenAccept( | ||
| notificationService::sendNotifications | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
🧩 Analysis chain
CompletableFuture 예외 미처리 및 Future 미수집 — 누수/침묵 실패 위험
비동기 체인에서 발생한 예외가 삼켜지고, 반환 Future를 보관하지 않아 스케줄 간 중첩 시 추적이 어렵습니다. 최소한 exceptionally 로깅을 추가하세요.
- for (DeptInfo deptInfo : deptInfoList) {
- CompletableFuture
- .supplyAsync(
- () -> updateDepartmentAsync(deptInfo, DeptInfo::scrapLatestPageHtml),
- noticeUpdaterThreadTaskExecutor
- ).thenApply(
- scrapResults -> compareLatestAndUpdateDB(scrapResults, deptInfo.getDeptName())
- ).thenAccept(
- notificationService::sendNotifications
- );
+ for (DeptInfo deptInfo : deptInfoList) {
+ CompletableFuture
+ .supplyAsync(
+ () -> updateDepartmentAsync(deptInfo, DeptInfo::scrapLatestPageHtml),
+ noticeUpdaterThreadTaskExecutor
+ ).thenApply(scrapResults -> compareLatestAndUpdateDB(scrapResults, deptInfo.getDeptName()))
+ .thenAccept(notificationService::sendNotifications)
+ .exceptionally(ex -> {
+ log.error("Dept latest update failed: dept={}, error={}", deptInfo.getDeptName(), ex.toString());
+ return null;
+ });
}🏁 Script executed:
#!/bin/bash
# 최신 업데이트 체인에서 exceptionally 미적용 호출이 남아있는지 검사
rg -nP --type=java -C2 '\.supplyAsync\([^)]*\)\)\.thenApply\([^)]*\)\.thenAccept\([^)]*\)(?!\.exceptionally)' src/main/javaLength of output: 83
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "검색: supplyAsync, thenApply, thenAccept, .exceptionally 사용 위치 (컨텍스트 포함)"
rg -n --type=java 'supplyAsync\(|thenApply\(|thenAccept\(|\.exceptionally\(' src/main/java -C3 || trueLength of output: 12355
DepartmentNoticeUpdater: CompletableFuture 체인에 예외 처리 및 반환 Future 누락
src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java:49–56
- supplyAsync→thenApply→thenAccept 체인 마지막에
추가
.exceptionally(ex -> { log.error("Dept 최신 공지 업데이트 실패: dept={}, error={}", deptInfo.getDeptName(), ex); return null; })
- 반환된 CompletableFuture를 List 등에 수집해 스케줄 간 추적 및 관리 강화 필요
🤖 Prompt for AI Agents
In
src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java
around lines 45 to 56, the CompletableFuture chain created for each DeptInfo
lacks exception handling and the returned Future isn't collected; update the
chain to append an .exceptionally(...) handler that logs the dept name and error
(returning a safe value or null) and ensure each CompletableFuture returned by
supplyAsync(...).thenApply(...).thenAccept(...) is captured into a
List<CompletableFuture<?>> (or similar) so callers can track and manage them
(e.g., waitAll or inspect results) instead of fire-and-forgetting.
| if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_NOTICE.getFeature())) { | ||
| log.info("******** 학과별 전체 공지 업데이트 시작 ********"); | ||
| log.info("******** 학과별 (학사) 전체 공지 업데이트 시작 ********"); | ||
|
|
||
| for (DeptInfo deptInfo : deptInfoList) { | ||
| if (deptInfo.isSameDepartment(REAL_ESTATE)) { | ||
| continue; | ||
| } | ||
|
|
||
| CompletableFuture | ||
| .supplyAsync(() -> updateDepartmentAsync(deptInfo, DeptInfo::scrapAllPageHtml), noticeUpdaterThreadTaskExecutor) | ||
| .thenAccept(scrapResults -> compareAllAndUpdateDB(scrapResults, deptInfo.getDeptName())); | ||
|
|
||
| } | ||
|
|
There was a problem hiding this comment.
전체 업데이트(updateAll)에서는 완료 보장 필요
주 1회 전체 동기화는 완료 여부가 중요합니다. Future를 수집해 allOf().join()으로 완료를 보장하고, 예외 로깅을 추가하세요.
- for (DeptInfo deptInfo : deptInfoList) {
+ java.util.List<java.util.concurrent.CompletableFuture<Void>> futures = new java.util.ArrayList<>();
+ for (DeptInfo deptInfo : deptInfoList) {
if (deptInfo.isSameDepartment(REAL_ESTATE)) {
continue;
}
- CompletableFuture
- .supplyAsync(() -> updateDepartmentAsync(deptInfo, DeptInfo::scrapAllPageHtml), noticeUpdaterThreadTaskExecutor)
- .thenAccept(scrapResults -> compareAllAndUpdateDB(scrapResults, deptInfo.getDeptName()));
+ java.util.concurrent.CompletableFuture<Void> cf =
+ CompletableFuture
+ .supplyAsync(() -> updateDepartmentAsync(deptInfo, DeptInfo::scrapAllPageHtml), noticeUpdaterThreadTaskExecutor)
+ .thenAccept(scrapResults -> compareAllAndUpdateDB(scrapResults, deptInfo.getDeptName()))
+ .exceptionally(ex -> {
+ log.error("Dept full update failed: dept={}, error={}", deptInfo.getDeptName(), ex.toString());
+ return null;
+ });
+ futures.add(cf);
}
-
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();📝 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.
| if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_NOTICE.getFeature())) { | |
| log.info("******** 학과별 전체 공지 업데이트 시작 ********"); | |
| log.info("******** 학과별 (학사) 전체 공지 업데이트 시작 ********"); | |
| for (DeptInfo deptInfo : deptInfoList) { | |
| if (deptInfo.isSameDepartment(REAL_ESTATE)) { | |
| continue; | |
| } | |
| CompletableFuture | |
| .supplyAsync(() -> updateDepartmentAsync(deptInfo, DeptInfo::scrapAllPageHtml), noticeUpdaterThreadTaskExecutor) | |
| .thenAccept(scrapResults -> compareAllAndUpdateDB(scrapResults, deptInfo.getDeptName())); | |
| } | |
| if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_NOTICE.getFeature())) { | |
| log.info("******** 학과별 (학사) 전체 공지 업데이트 시작 ********"); | |
| java.util.List<java.util.concurrent.CompletableFuture<Void>> futures = new java.util.ArrayList<>(); | |
| for (DeptInfo deptInfo : deptInfoList) { | |
| if (deptInfo.isSameDepartment(REAL_ESTATE)) { | |
| continue; | |
| } | |
| java.util.concurrent.CompletableFuture<Void> cf = | |
| CompletableFuture | |
| .supplyAsync(() -> updateDepartmentAsync(deptInfo, DeptInfo::scrapAllPageHtml), noticeUpdaterThreadTaskExecutor) | |
| .thenAccept(scrapResults -> compareAllAndUpdateDB(scrapResults, deptInfo.getDeptName())) | |
| .exceptionally(ex -> { | |
| log.error("Dept full update failed: dept={}, error={}", deptInfo.getDeptName(), ex.toString()); | |
| return null; | |
| }); | |
| futures.add(cf); | |
| } | |
| CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); |
| "departmentNotice1", CategoryName.DEPARTMENT, false, "https://www.example.com", DepartmentName.ADMINISTRATION, false); | ||
| DepartmentNotice departmentNotice2 = new DepartmentNotice("4", "2024-01-24 17:27:05", "2023-04-03 17:27:05", | ||
| "departmentNotice2", CategoryName.DEPARTMENT, false, "https://www.example.com", DepartmentName.ADMINISTRATION); | ||
| "departmentNotice2", CategoryName.DEPARTMENT, false, "https://www.example.com", DepartmentName.ADMINISTRATION, true); | ||
| noticeCommandPort.saveAllDepartmentNotices(List.of(departmentNotice1, departmentNotice2)); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
🧩 Analysis chain
graduated 추가 반영 OK, 마이그레이션 제약/인덱스 점검 권장
- true/false가 섞인 DepartmentNotice를 저장하고 있으므로, DB 마이그레이션에서 컬럼 제약(NOT NULL, DEFAULT)과 조회 성능 인덱스가 중요합니다.
- 부서별 공지 조회가 graduated로 필터링된다면 (department, graduated, posted_date) 복합 인덱스를 고려하세요.
권장 인덱스(예시, 실제 컬럼명에 맞춰 조정):
CREATE INDEX IF NOT EXISTS ix_notice_dept_grad_posted
ON notice (department_name, graduated, posted_date DESC);마이그레이션 파일에서 제약/인덱스를 확인해 주세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a --type f 'V*__Add_graduated*_notice.sql' src/main/resources/db/migration || true
rg -n 'graduated' src/main/resources/db/migration -C2 || true
rg -nP 'NOT\s+NULL|DEFAULT\s+\w+|CREATE\s+INDEX' src/main/resources/db/migration -C2 || trueLength of output: 8039
graduated 컬럼 마이그레이션 확인 — 제약·인덱스 추가 권장
- 확인: src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql 에서
ADD COLUMN graduated BOOLEAN NULL;로 추가됨 — NOT NULL/DEFAULT 없음. - 확인: 마이그레이션들에서 notice 테이블용 복합 인덱스(부서, graduated, posted_date)를 생성하는 구문 없음.
- 권장 조치: (1) NULL 허용이 의도된 것이 아니라면 마이그레이션에 NOT NULL DEFAULT FALSE 및 기존 데이터 backfill 추가, (2) 부서별 공지 조회에서 graduated로 필터링한다면 복합 인덱스 추가 마이그레이션 생성.
예시 인덱스:
CREATE INDEX IF NOT EXISTS ix_notice_dept_grad_posted
ON notice (department_name, graduated, posted_date DESC);컬럼명은 실제 스키마에 맞춰 조정할 것.
🤖 Prompt for AI Agents
In
src/test/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeRepositoryTest.java
around lines 67 to 70, the review flags that the DB migration
V250905__Add_graduated_to_notice.sql added "graduated BOOLEAN NULL" without NOT
NULL/DEFAULT and no composite index for department-based queries; update the
migration(s) to (1) alter the graduated column to NOT NULL DEFAULT FALSE and
include a backfill step to set existing NULLs to FALSE, and (2) add a new
migration that creates a composite index on (department_name, graduated,
posted_date DESC) (or use your actual column names) to support efficient
department+graduated+posted_date queries.
| private List<DepartmentNotice> buildImportantDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important, boolean graduated) { | ||
| return Stream.iterate(0, i -> i + 1) | ||
| .limit(cnt) | ||
| .map(i -> new DepartmentNotice("depart_import_article_" + i, "2023-04-03 00:00:1"+i, "2023-04-03 00:00:1"+i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName)) | ||
| .map(i -> new DepartmentNotice((graduated ? "grad_depart_import_article_" : "depart_import_article_") + i, "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated)) | ||
| .toList(); | ||
| } | ||
|
|
||
| private List<DepartmentNotice> buildNormalDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important) { | ||
| private List<DepartmentNotice> buildNormalDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important, boolean graduated) { | ||
| return Stream.iterate(0, i -> i + 1) | ||
| .limit(cnt) | ||
| .map(i -> new DepartmentNotice("depart_normal_article_" + i, "2023-04-03 00:00:1"+i, "2023-04-03 00:00:1"+i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName)) | ||
| .map(i -> new DepartmentNotice((graduated ? "grad_depart_normal_article_" : "depart_normal_article_") + i, "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated)) | ||
| .toList(); | ||
| } |
There was a problem hiding this comment.
DepartmentNotice articleId가 영문 접두사 포함 — 숫자 파싱 로직과 충돌 가능
운영 코드에서 부서 공지 articleId를 Integer로 파싱/정렬하는 경로가 있습니다. 현재 시딩 값(예: "grad_depart_import_article_0")은 NumberFormatException을 유발할 수 있습니다. 테스트 시딩을 숫자 문자열로 변경하세요(범위 분리로 충돌 방지).
예시 수정:
- .map(i -> new DepartmentNotice((graduated ? "grad_depart_import_article_" : "depart_import_article_") + i, "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated))
+ .map(i -> new DepartmentNotice(String.valueOf((graduated ? 110000 : 120000) + i), "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated))- .map(i -> new DepartmentNotice((graduated ? "grad_depart_normal_article_" : "depart_normal_article_") + i, "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated))
+ .map(i -> new DepartmentNotice(String.valueOf((graduated ? 210000 : 220000) + i), "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated))📝 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.
| private List<DepartmentNotice> buildImportantDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important, boolean graduated) { | |
| return Stream.iterate(0, i -> i + 1) | |
| .limit(cnt) | |
| .map(i -> new DepartmentNotice("depart_import_article_" + i, "2023-04-03 00:00:1"+i, "2023-04-03 00:00:1"+i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName)) | |
| .map(i -> new DepartmentNotice((graduated ? "grad_depart_import_article_" : "depart_import_article_") + i, "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated)) | |
| .toList(); | |
| } | |
| private List<DepartmentNotice> buildNormalDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important) { | |
| private List<DepartmentNotice> buildNormalDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important, boolean graduated) { | |
| return Stream.iterate(0, i -> i + 1) | |
| .limit(cnt) | |
| .map(i -> new DepartmentNotice("depart_normal_article_" + i, "2023-04-03 00:00:1"+i, "2023-04-03 00:00:1"+i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName)) | |
| .map(i -> new DepartmentNotice((graduated ? "grad_depart_normal_article_" : "depart_normal_article_") + i, "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated)) | |
| .toList(); | |
| } | |
| private List<DepartmentNotice> buildImportantDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important, boolean graduated) { | |
| return Stream.iterate(0, i -> i + 1) | |
| .limit(cnt) | |
| .map(i -> new DepartmentNotice(String.valueOf((graduated ? 110000 : 120000) + i), "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated)) | |
| .toList(); | |
| } | |
| private List<DepartmentNotice> buildNormalDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important, boolean graduated) { | |
| return Stream.iterate(0, i -> i + 1) | |
| .limit(cnt) | |
| .map(i -> new DepartmentNotice(String.valueOf((graduated ? 210000 : 220000) + i), "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName, graduated)) | |
| .toList(); | |
| } |
🤖 Prompt for AI Agents
In src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java around
lines 254-266, the seeded DepartmentNotice articleId values include alphabetic
prefixes (e.g., "grad_depart_import_article_0") which will cause
Integer.parseInt/NumberFormatException in production paths that parse/sort by
numeric articleId; change the articleId to be pure numeric strings to avoid
parsing errors — for example generate sequential numeric strings and use
distinct numeric ranges (offsets) for graduated vs non-graduated and for
important vs normal to keep them unique and non-overlapping; update both
buildImportantDepartmentNotice and buildNormalDepartmentNotice to produce only
numeric articleId strings accordingly.
|
* version 2.15.3 (#304) * Feat: 컴퓨터공학부 대학원 공지 Scrap 추가 (#291) * DepartmentName에 COMPUTER_GRADUATE 추가 * ComputerScienceGraduateDept 구현 * [feat]: urlPrefix 구현 * [feat]: urlPrefix 적용 * [test]: ComputerScienceGraduateDeptTest 구현 * [refactor]: GraduateDeptInfoTest로 이동 * [feat]: isGrad 필드 추가 * [feat]: isGrad 적용 * [feat]: is_grad 필드 추가 flyway 적용 * [refactor]: getUrlPrefix -> getHostPrefix로 수정 * [refactor]: urlPrefix 삭제 * [feat]: noticeGraudationInfo 추가 * [refactor]: ComputerScienceGraduateDept 삭제 * [refactor]: isGrad 적용 * [refactor]: GraduateDeptInfoTest 개행 추가, TestFileLoader 적용 * [refactor]: isGrad -> graduate로 필드명 수정 * [refactor]: graduate 필드 Null 허용으로 수정 * [refactor]: getter 필드 위에 지정 * [refactor]: setter -> 비즈니스 함수로 수정 * [refactor]: 대학원/학사 DepartmentNoticeUpdater 분리 * [refactor]: 개행처리 * [refactor]: DepartmentNoticeScrapResult 삭제 * [refactor]: LatestPageNoticeApiClient 분리 * [refactor]: creatUrl/scrapHtml 분리 * [refactor]: graduate로 필드 수정 * [refactor]: latestPageGraduateNoticeApiClient 사용 * [refactor]: graduate -> graduated로 이름 수 * [refactor]: 일반 notice는 graduated 필드에 null로 저장 * [refactor]: getGraduated() 오타 수정 * [refactor]: deptInfo.isSupportGraduateScrap() 구현 - 대학원 과정을 지원 안하는 DeptInfo 방어 로직 * [refactor]: ? 개수 수정 * [refactor]: 일반 공지 graduated null로 반환 테스트 추가 * [refactor]: 컨트롤러 graduated 파라미터 required=false로 설정 * [refactor]: 공통 로직 getGraduateDeptInfoList 메서드 추출 * [feat]: noticeGraduationInfo 추가 * [feat]: Notice 관련 테스트에 graduated 적용 * [fix]: noticeGraduationInfo 오타 수정 * Feat/cors setting * feat(AuthConfig): CORS 설정 추가 * feat(AuthConfig): CORS 설정 추가 * feat(Report): reporterId로 필드명 변경 * feat(AdminQueryService): 조회시 Page구현체 사용하도록 변경 * Feat: 유저 학사일정 알림 설정 관련 기본 설정 추가 (#302) * feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 * Feat: 학사일정 알림 설정 API 추가 (#303) * feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 * feat: 학사일정 알림 토글 API 엔드포인트 및 Response Dto추가 * feat: 학사일정 알림 성공 메시지 추가 * feat: 학사일정 알림 토글 UseCase 추가 * feat: 학사일정 알림 토글 서비스 로직 추가 * feat: 학사일정 토글 도메인 테스트 추가 * feat: 학사일정 토글 인수 테스트 추가 * fix: FeatureFlagConfig Remote 설정 * fix: toggleAcademicEventNotification 메서드명 변경 -> updateUserAcademicEventNotification * fix: 불필요 반환 객체 제거(result, response) * fix: 학사일정 알림 변경 요청 시 설정값 받도록 수정 * fix: 학사일정 알림 설정 도메인 로직수정 * fix: 유저 인수테스트 및 테스트 수정 * fix: @NotNull javax -> jakarta 임포트 수정 * fix: 교직원 스크랩 DTO변환간 중복 Key 문제 해결 * fix: 불필요 로깅 삭제 * fix: 불필요 주석 삭제 --------- Co-authored-by: 양지윤 <155087951+jiyun921@users.noreply.github.com> Co-authored-by: Jiwoo Kim <zbqmgldjfh@gmail.com> * fix: 학사일정 스크랩 주기 원복 * feat: 최초 사용자 토큰 저장 시 학사일정 알림 구독하도록 수정 * feat: 학상일정 알림 설정 시 FCM 구독/구독해제 설정 추가 * feat: 학사일정 알림 토픽 추가 * feat: 학사일정 알림 스케쥴 로직 추가 * feat: 금일 학사일정 DB 조회 로직 구현 * feat: 학사일정 알림 전송 비지니스 로직 추가 * feat: 학사일정 알림 전송 관련 도메인 로직 추가 * feat: FeatureFlagsSupport 객체 추가 * test: IntegrationTestSupport 통합 테스트 수행 전 FeatureFlag 초기화 추가 * feat: FeatureFlagsSupport 객체 추가로 인한 불필요 Mocking 제거 * feat: User 도메인 테스트 추가 * test: AcademicEventNotificationServiceTest 추가 * test: AcademicEventNotificationSchedulerTest 추가 * fix: 메세지 리터럴 변수명 불일치 수정 * fix: 잘못된 try Catch문 삭제 * fix: Refrence Type으로 수정 * fix: Boolean 비교 equals 적용 이걸 까먹네 바본가 * fix: 기본 토픽 구독하는 메서드명 수정 * fix: 학사일정 알림 메시지 포맷 변경 * fix: 학사일정 알림 발송 간 에러 발생시 로직 수정 --------- Co-authored-by: 양지윤 <155087951+jiyun921@users.noreply.github.com> Co-authored-by: Jiwoo Kim <zbqmgldjfh@gmail.com>
* Feat: 컴퓨터공학부 대학원 공지 Scrap 추가 (#291) * DepartmentName에 COMPUTER_GRADUATE 추가 * ComputerScienceGraduateDept 구현 * [feat]: urlPrefix 구현 * [feat]: urlPrefix 적용 * [test]: ComputerScienceGraduateDeptTest 구현 * [refactor]: GraduateDeptInfoTest로 이동 * [feat]: isGrad 필드 추가 * [feat]: isGrad 적용 * [feat]: is_grad 필드 추가 flyway 적용 * [refactor]: getUrlPrefix -> getHostPrefix로 수정 * [refactor]: urlPrefix 삭제 * [feat]: noticeGraudationInfo 추가 * [refactor]: ComputerScienceGraduateDept 삭제 * [refactor]: isGrad 적용 * [refactor]: GraduateDeptInfoTest 개행 추가, TestFileLoader 적용 * [refactor]: isGrad -> graduate로 필드명 수정 * [refactor]: graduate 필드 Null 허용으로 수정 * [refactor]: getter 필드 위에 지정 * [refactor]: setter -> 비즈니스 함수로 수정 * [refactor]: 대학원/학사 DepartmentNoticeUpdater 분리 * [refactor]: 개행처리 * [refactor]: DepartmentNoticeScrapResult 삭제 * [refactor]: LatestPageNoticeApiClient 분리 * [refactor]: creatUrl/scrapHtml 분리 * [refactor]: graduate로 필드 수정 * [refactor]: latestPageGraduateNoticeApiClient 사용 * [refactor]: graduate -> graduated로 이름 수 * [refactor]: 일반 notice는 graduated 필드에 null로 저장 * [refactor]: getGraduated() 오타 수정 * [refactor]: deptInfo.isSupportGraduateScrap() 구현 - 대학원 과정을 지원 안하는 DeptInfo 방어 로직 * [refactor]: ? 개수 수정 * [refactor]: 일반 공지 graduated null로 반환 테스트 추가 * [refactor]: 컨트롤러 graduated 파라미터 required=false로 설정 * [refactor]: 공통 로직 getGraduateDeptInfoList 메서드 추출 * [feat]: noticeGraduationInfo 추가 * [feat]: Notice 관련 테스트에 graduated 적용 * [fix]: noticeGraduationInfo 오타 수정 * Feat/cors setting * feat(AuthConfig): CORS 설정 추가 * feat(AuthConfig): CORS 설정 추가 * feat(Report): reporterId로 필드명 변경 * feat(AdminQueryService): 조회시 Page구현체 사용하도록 변경 * Feat: 유저 학사일정 알림 설정 관련 기본 설정 추가 (#302) * feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 * Feat: 학사일정 알림 설정 API 추가 (#303) * feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 * feat: 학사일정 알림 토글 API 엔드포인트 및 Response Dto추가 * feat: 학사일정 알림 성공 메시지 추가 * feat: 학사일정 알림 토글 UseCase 추가 * feat: 학사일정 알림 토글 서비스 로직 추가 * feat: 학사일정 토글 도메인 테스트 추가 * feat: 학사일정 토글 인수 테스트 추가 * fix: FeatureFlagConfig Remote 설정 * fix: toggleAcademicEventNotification 메서드명 변경 -> updateUserAcademicEventNotification * fix: 불필요 반환 객체 제거(result, response) * fix: 학사일정 알림 변경 요청 시 설정값 받도록 수정 * fix: 학사일정 알림 설정 도메인 로직수정 * fix: 유저 인수테스트 및 테스트 수정 * fix: @NotNull javax -> jakarta 임포트 수정 * fix: 교직원 스크랩 DTO변환간 중복 Key 문제 해결 * fix: 불필요 로깅 삭제 * fix: 불필요 주석 삭제 * Feat: 학사일정 알림 스케쥴링 로직 추가 (#305) * version 2.15.3 (#304) * Feat: 컴퓨터공학부 대학원 공지 Scrap 추가 (#291) * DepartmentName에 COMPUTER_GRADUATE 추가 * ComputerScienceGraduateDept 구현 * [feat]: urlPrefix 구현 * [feat]: urlPrefix 적용 * [test]: ComputerScienceGraduateDeptTest 구현 * [refactor]: GraduateDeptInfoTest로 이동 * [feat]: isGrad 필드 추가 * [feat]: isGrad 적용 * [feat]: is_grad 필드 추가 flyway 적용 * [refactor]: getUrlPrefix -> getHostPrefix로 수정 * [refactor]: urlPrefix 삭제 * [feat]: noticeGraudationInfo 추가 * [refactor]: ComputerScienceGraduateDept 삭제 * [refactor]: isGrad 적용 * [refactor]: GraduateDeptInfoTest 개행 추가, TestFileLoader 적용 * [refactor]: isGrad -> graduate로 필드명 수정 * [refactor]: graduate 필드 Null 허용으로 수정 * [refactor]: getter 필드 위에 지정 * [refactor]: setter -> 비즈니스 함수로 수정 * [refactor]: 대학원/학사 DepartmentNoticeUpdater 분리 * [refactor]: 개행처리 * [refactor]: DepartmentNoticeScrapResult 삭제 * [refactor]: LatestPageNoticeApiClient 분리 * [refactor]: creatUrl/scrapHtml 분리 * [refactor]: graduate로 필드 수정 * [refactor]: latestPageGraduateNoticeApiClient 사용 * [refactor]: graduate -> graduated로 이름 수 * [refactor]: 일반 notice는 graduated 필드에 null로 저장 * [refactor]: getGraduated() 오타 수정 * [refactor]: deptInfo.isSupportGraduateScrap() 구현 - 대학원 과정을 지원 안하는 DeptInfo 방어 로직 * [refactor]: ? 개수 수정 * [refactor]: 일반 공지 graduated null로 반환 테스트 추가 * [refactor]: 컨트롤러 graduated 파라미터 required=false로 설정 * [refactor]: 공통 로직 getGraduateDeptInfoList 메서드 추출 * [feat]: noticeGraduationInfo 추가 * [feat]: Notice 관련 테스트에 graduated 적용 * [fix]: noticeGraduationInfo 오타 수정 * Feat/cors setting * feat(AuthConfig): CORS 설정 추가 * feat(AuthConfig): CORS 설정 추가 * feat(Report): reporterId로 필드명 변경 * feat(AdminQueryService): 조회시 Page구현체 사용하도록 변경 * Feat: 유저 학사일정 알림 설정 관련 기본 설정 추가 (#302) * feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 * Feat: 학사일정 알림 설정 API 추가 (#303) * feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 * feat: 학사일정 알림 토글 API 엔드포인트 및 Response Dto추가 * feat: 학사일정 알림 성공 메시지 추가 * feat: 학사일정 알림 토글 UseCase 추가 * feat: 학사일정 알림 토글 서비스 로직 추가 * feat: 학사일정 토글 도메인 테스트 추가 * feat: 학사일정 토글 인수 테스트 추가 * fix: FeatureFlagConfig Remote 설정 * fix: toggleAcademicEventNotification 메서드명 변경 -> updateUserAcademicEventNotification * fix: 불필요 반환 객체 제거(result, response) * fix: 학사일정 알림 변경 요청 시 설정값 받도록 수정 * fix: 학사일정 알림 설정 도메인 로직수정 * fix: 유저 인수테스트 및 테스트 수정 * fix: @NotNull javax -> jakarta 임포트 수정 * fix: 교직원 스크랩 DTO변환간 중복 Key 문제 해결 * fix: 불필요 로깅 삭제 * fix: 불필요 주석 삭제 --------- Co-authored-by: 양지윤 <155087951+jiyun921@users.noreply.github.com> Co-authored-by: Jiwoo Kim <zbqmgldjfh@gmail.com> * fix: 학사일정 스크랩 주기 원복 * feat: 최초 사용자 토큰 저장 시 학사일정 알림 구독하도록 수정 * feat: 학상일정 알림 설정 시 FCM 구독/구독해제 설정 추가 * feat: 학사일정 알림 토픽 추가 * feat: 학사일정 알림 스케쥴 로직 추가 * feat: 금일 학사일정 DB 조회 로직 구현 * feat: 학사일정 알림 전송 비지니스 로직 추가 * feat: 학사일정 알림 전송 관련 도메인 로직 추가 * feat: FeatureFlagsSupport 객체 추가 * test: IntegrationTestSupport 통합 테스트 수행 전 FeatureFlag 초기화 추가 * feat: FeatureFlagsSupport 객체 추가로 인한 불필요 Mocking 제거 * feat: User 도메인 테스트 추가 * test: AcademicEventNotificationServiceTest 추가 * test: AcademicEventNotificationSchedulerTest 추가 * fix: 메세지 리터럴 변수명 불일치 수정 * fix: 잘못된 try Catch문 삭제 * fix: Refrence Type으로 수정 * fix: Boolean 비교 equals 적용 이걸 까먹네 바본가 * fix: 기본 토픽 구독하는 메서드명 수정 * fix: 학사일정 알림 메시지 포맷 변경 * fix: 학사일정 알림 발송 간 에러 발생시 로직 수정 --------- Co-authored-by: 양지윤 <155087951+jiyun921@users.noreply.github.com> Co-authored-by: Jiwoo Kim <zbqmgldjfh@gmail.com> * Fix: 학과 공지 업데이트 오류 수정, 학과 목록 조회 api 수정 (#310) * [feat]: 학과별(학사) 전체 공지 업데이트 테스트 구현 * [refactor]: 변수 이름 graduated로 수정 * [fix]: latestPageGraduateNoticeApiClient 주입 누락 수정 * [feat]: 학과 목록 조회 - graduateSupported 추가 * [refactor]: 학과 목록 조회 - DeptInfo 주입 방식으로 수정 * [feat]: 매체연기학과 추가 구현 * [refactor]: import 수정 * [fix]: 학과 개수 변경 반영 * Feat: OCI 환경 CI/CD배포 파이프라인 추가 (#311) * feat: OCI CI/CD 워크플로우 * fix: plain 파일 제외 하도록 수정 * fix: dev 환경 ci/cd workflow 수정(decrpt 추가 및 sh 명령어 수정) (#312) * feat: decrpt secrets 실행 시 env 추가 (#313) * Feat: 학사일정 카테고리 및 알림 전송 분류 추가 (#308) * feat: 학사일정 카테고리 추가 * fix: 학사일정 카테고리 이넘화로 인한 수정 * feat: 학사일정 카테고리 분류 클래스 추가 * feat: 학사일정 알림 분류기 추가 * feat: 학사일정 제목 정규화 클래스 추가 * feat: AcademicEventConverter 내 알림 및 카테고리 분류 추가 * fix: 학사일정 카테고리 추가로 인한 테스트 변경 * fix: 학사일정 카테고리 추가로 인한 테스트 변경 * fix: 학사일정 스크랩 간 공휴일 제거로 인한 검증 횟수 변경 * test: AcademicEventCategorizerTest 추가 * test: AcademicEventNotificationClassifierTest 추가 * test: AcademicEventSummaryNormalizerTest 추가 * test: AcademicEventConverterTest 카테고리, 알림 구분 테스트 추가 * feat: 학사일정 스크랩 주기 변경(월1회 -> 주1회) * fix: 학사일정 업데이트 스케줄링 간 이벤트 건수 로그레벨 변경 * fix: 소나큐브 대응(Private Constructor) * fix: 코드래빗 대응(DisplayName 변경) * fix: 소나큐브 대응(ReDOS문제) * fix: 소나큐브 대응(ReDOS문제) * fix: 학사일정 카테고리 이름 공백 제거 * fix: 학사일정 엔티티 변환 시 Optional 반환하도록 수정 * fix: 푸시알림 메시지 생성 시 data 추가 * fix: 로그 오타 수정 * feat: Normalizer 패턴 변경(글자수 제한, Possessive Quantifiers) * fix: AcademicEventCategorizer private 생성자 추가 * Feat: 어드민 기능- 전체 사용자 토픽 구독 API 추가 (#314) * feat: User.getFcmToken getter 추가 * feat: 사용자 모든 구독 정보 가져오는 Repository Method 추가 * remove: 불필요 메서드 삭제 * remove: 모든 사용자 토픽 구독 처리 서비스 로직 추가 * remove: 모든 사용자 토픽 구독 처리 API추가 * remove: 모든 사용자 토픽 구독 처리 인수테스트 추가 * fix: 재구독 시 트랜잭션 어노테이션 제거 * fix: 재구독 성공/실패 개수 로깅 * feat: 학사일정 카테고리 컬럼 사이즈 30으로 변경 * Fix: 학사일정 알림 및 업데이트 시간 수정 (#315) * fix: 학사일정 알림 스케쥴 시간 변경(매일 아침 9시) * fix: 학사일정 업데이트 시간 수정(매주 월요일 오전 5시) * Feat: 대학원 스크랩 Feature Flag 적용 (#316) * fix: 대학원 공지사항 스크랩 피처플래그 추가 * feat: 대학원 공지사항 스크랩 피처플래그 적용 --------- Co-authored-by: 양지윤 <155087951+jiyun921@users.noreply.github.com> Co-authored-by: Jiwoo Kim <zbqmgldjfh@gmail.com>



#️⃣ 이슈
#281
📌 요약
🛠️ 상세
💬 기타
Summary by CodeRabbit
New Features
Chores
Style