Skip to content

version 2.15.3#304

Merged
rlagkswn00 merged 8 commits intomainfrom
develop
Sep 21, 2025
Merged

version 2.15.3#304
rlagkswn00 merged 8 commits intomainfrom
develop

Conversation

@rlagkswn00
Copy link
Member

@rlagkswn00 rlagkswn00 commented Sep 21, 2025

#️⃣ 이슈

#281

📌 요약

  • 학사일정 관련 DB 및 엔티티 변경
  • 학사일정 알림 설정 API 추가
  • FeatureFlags 빈주입 변경

🛠️ 상세

💬 기타

Summary by CodeRabbit

  • New Features

    • 학과 공지 조회에 대학원 여부(graduated) 필터 추가 및 응답에 graduated 필드 제공
    • 사용자 학사일정 알림 설정 토글 API 추가
    • 관리자 피드백/알림/신고 조회 응답을 페이지네이션 메타(hasNext, totalElements, totalPages) 포함 래퍼로 개선
    • 대학원 학과 공지 수집·동기화 및 알림 발송 지원
  • Chores

    • 전역 CORS 설정 추가로 클라이언트 연동 안정성 향상
  • Style

    • 일부 학과 한글명 정정 (예: 영상학과, 교육학과)

jiyun921 and others added 4 commits September 12, 2025 15:26
* 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 임포트 수정
@rlagkswn00 rlagkswn00 self-assigned this Sep 21, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 21, 2025

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 675e8a1 and ebae030.

📒 Files selected for processing (4)
  • src/main/java/com/kustacks/kuring/auth/AuthConfig.java (2 hunks)
  • src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java (2 hunks)
  • src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java (2 hunks)
  • src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java (2 hunks)

Walkthrough

관리자 조회 API의 응답을 페이지 DTO로 래핑하고, 내부 유즈케이스/리포지토리를 Page 기반으로 변경. 공지 조회에 대학원 여부(graduated) 필터를 도입하고, 도메인/포트/리포지토리/스크래퍼/업데이터 전반에 반영. 사용자 학사일정 알림 토글 엔드포인트 추가 및 컬럼/플래그/응답코드/라벨 도입. CORS 설정 추가.

Changes

Cohort / File(s) Summary
Admin API 페이징 래핑
src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminQueryApiV2.java, .../dto/AdminAlertListResponse.java, .../dto/AdminFeedbackListResponse.java, .../dto/AdminReportListResponse.java, .../application/port/in/AdminQueryUseCase.java, .../application/service/AdminQueryService.java, .../application/port/out/AdminUserFeedbackPort.java, src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java, src/test/java/com/kustacks/kuring/acceptance/AdminStep.java, .../user/adapter/out/persistence/UserQueryRepository*.java, .../user/adapter/out/persistence/UserPersistenceAdapter.java, .../user/adapter/out/persistence/UserRepositoryTest.java
관리자 피드백/알림/신고 조회를 List → Page 반환으로 변경하고, 응답을 ListResponse DTO로 래핑. 관련 포트/서비스/리포지토리/테스트 시그니처 및 JSON 경로 업데이트.
Notice 대학원(graduated) 필터 및 스키마
.../notice/adapter/in/web/NoticeQueryApiV2.java, .../notice/adapter/in/web/dto/NoticeRangeLookupResponse.java, .../notice/application/port/in/dto/NoticeRangeLookupCommand.java, .../NoticeRangeLookupResult.java, .../notice/application/port/out/NoticeQueryPort.java, .../notice/adapter/out/persistence/NoticeQueryRepository*.java, .../NoticePersistenceAdapter.java, .../notice/adapter/out/persistence/NoticeJdbcRepository.java, .../notice/application/port/out/dto/NoticeDto.java, .../notice/application/service/NoticeQueryService.java, .../notice/domain/DepartmentNotice.java, .../notice/domain/DepartmentName.java, src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql, src/test/java/com/kustacks/kuring/acceptance/Notice*.java, src/test/java/com/kustacks/kuring/worker/scrap/KuisHomepageNoticeScraperTemplateTest.java
공지 조회에 graduated 필드를 추가(명령/결과/응답). 부서 공지 쿼리/ID 조회에 graduated 매개변수 도입. JDBC 배치 인서트에 graduated 컬럼 바인딩. 도메인/DTO에 필드 추가. DB 마이그레이션. 관련 테스트/경로 수정.
Graduate 공지 스크래핑/업데이트 파이프라인
.../worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java, .../worker/scrap/client/notice/LatestPageNoticeApiClient.java, .../worker/scrap/deptinfo/DeptInfo.java, .../worker/scrap/deptinfo/*/*.java, .../worker/scrap/deptinfo/*/*/*.java, .../worker/update/notice/DepartmentGraduationNoticeUpdater.java, .../worker/update/notice/DepartmentNoticeUpdater.java, .../worker/update/notice/NoticeUpdateSupport.java, src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java, src/test/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupportTest.java, src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java
대학원 공지용 API 클라이언트/URL 빌더/부서 설정 추가. 스케줄러(증분/풀) 도입 및 graduated 흐름 전파. 업데이트/필터링 로직에 graduated 인자 확장. 다수 부서에 noticeGraduationInfo 초기화. 테스트/데이터 초기화 갱신.
사용자 학사일정 알림 토글
.../user/adapter/in/web/UserCommandApiV2.java, .../user/adapter/in/web/dto/UserAcademicEventNotificationRequest.java, .../user/application/port/in/UserCommandUseCase.java, .../user/application/port/in/dto/UserAcademicEventNotificationCommand.java, .../user/application/service/UserCommandService.java, .../user/domain/User.java, src/main/resources/db/migration/V250918__Add_academic_event_notification_to_user.sql, src/test/java/com/kustacks/kuring/acceptance/User*.java, src/test/java/com/kustacks/kuring/user/domain/UserTest.java
PATCH API 추가로 사용자 학사일정 알림 on/off. 커맨드/서비스/도메인 필드/업데이터 추가. 사용자 테이블에 컬럼 마이그레이션. 수용 테스트 및 단위 테스트 추가.
리포트 필드 명 변경 및 커맨드 확장
.../report/application/port/in/dto/AdminReportsResult.java, .../report/domain/Report.java, .../report/adapter/in/web/ReportCommandApiV2.java, .../admin/application/service/AdminQueryService.java
userId → reporterId로 명칭 변경(도메인/DTO/매핑). 신고 코멘트 커맨드 생성자 확장(targetId, userToken, content, targetType).
공통/인프라 보완
.../auth/AuthConfig.java, .../common/dto/ResponseCodeAndMessages.java, .../common/featureflag/KuringFeatures.java
전역 CORS 필터 등록. 응답코드에 ACADEMIC_EVENT_NOTIFICATION_UPDATE_SUCCESS 추가. 기능 플래그에 NOTIFY_ACADEMIC_EVENT 추가.

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)
Loading
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
Loading
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)
Loading

Possibly related PRs

Suggested labels

⭐ Feat

Poem

바람 따라 졸업 깃발 펄럭, 알림 한 점 톡!
페이지는 Page로 말하고, 목록은 예쁘게 포옥.
학과엔 대학원별 새 길, 토글은 토닥톡.
토끼는 귀를 세우고, 크론엔 뛰어가 쏙!
삐약-삐약? 아니, 깡총! 🐰✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.51% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive 제목 "version 2.15.3"은 짧지만 변경 내용의 핵심을 설명하지 않는 일반적인 버전 라벨입니다. 이번 PR은 학사일정 DB·엔티티 변경, 학사일정 알림 API 추가, 졸업 공지 스크래핑/업데이트 로직 등 여러 기능적 변경을 포함하므로 제목만으로 주된 변경점을 파악하기 어렵습니다. 따라서 제목이 PR의 목적을 충분히 전달하지 못해 평가를 단정하기엔 정보가 불충분합니다. 해결책으로 제목을 핵심 변경사항 위주로 한 문장으로 요약해 주세요; 예: "학사일정 알림 API 추가 및 졸업 공지 스크래핑·DB 변경 (v2.15.3)". 가능한 경우 핵심 영향 범위(API 추가, DB/엔티티 변경, 스케줄러 등)를 포함하면 리뷰어가 히스토리를 빠르게 이해할 수 있습니다.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rlagkswn00 rlagkswn00 added the 🚀 Deploy Production deploy production server label Sep 21, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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).

graduategraduated로 명칭 일치시키면 가독성이 좋아집니다(타 전역 필드/쿼리와 동일 용어).

-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

📥 Commits

Reviewing files that changed from the base of the PR and between c651a4f and 675e8a1.

📒 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);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 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' || true

Length 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);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 52 to 57
@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)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 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).

Comment on lines 67 to 69
ps.setBoolean(10, departmentNotice.getGraduated());
ps.setString(11, "DepartmentNotice");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 24 to 27
List<NoticeDto> findImportantNoticesByDepartment(DepartmentName departmentName, Boolean graduated);

List<NoticeDto> findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable);
List<NoticeDto> findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Boolean graduated, Pageable pageable);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 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=java

Length 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으로 변경하세요.

Comment on lines 153 to 156
if (!deletedNoticesArticleIds.isEmpty()) {
noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 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/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();

해당 호출부는 이미 두 곳(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.

Comment on lines 45 to 56
log.info("******** 학과별 (학사) 최신 공지 업데이트 시작 ********");

for (DeptInfo deptInfo : deptInfoList) {
CompletableFuture
.supplyAsync(
() -> updateDepartmentAsync(deptInfo, DeptInfo::scrapLatestPageHtml),
noticeUpdaterThreadTaskExecutor
).thenApply(
scrapResults -> compareLatestAndUpdateDB(scrapResults, deptInfo.getDeptName())
).thenAccept(
notificationService::sendNotifications
);
Copy link

Choose a reason for hiding this comment

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

🛠️ 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/java

Length 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 || true

Length 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.

Comment on lines 63 to 75
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()));

}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

전체 업데이트(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.

Suggested change
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();

Comment on lines 67 to 70
"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));
Copy link

Choose a reason for hiding this comment

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

🛠️ 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 || true

Length 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.

Comment on lines 254 to 266
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();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

@sonarqubecloud
Copy link

@github-actions
Copy link

Unit Test Results

  69 files  ±0    69 suites  ±0   1m 21s ⏱️ -1s
395 tests +2  389 ✔️ +2  6 💤 ±0  0 ±0 
398 runs  +2  392 ✔️ +2  6 💤 ±0  0 ±0 

Results for commit ebae030. ± Comparison against base commit c651a4f.

@rlagkswn00 rlagkswn00 merged commit 62b7653 into main Sep 21, 2025
6 of 7 checks passed
rlagkswn00 added a commit that referenced this pull request Sep 26, 2025
* 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>
rlagkswn00 added a commit that referenced this pull request Oct 8, 2025
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀 Deploy Production deploy production server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants