From 0762d72e99697db411c899c2ea3412cbc6551581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=A7=80=EC=9C=A4?= <155087951+jiyun921@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:26:50 +0900 Subject: [PATCH 01/16] =?UTF-8?q?Feat:=20=EC=BB=B4=ED=93=A8=ED=84=B0?= =?UTF-8?q?=EA=B3=B5=ED=95=99=EB=B6=80=20=EB=8C=80=ED=95=99=EC=9B=90=20?= =?UTF-8?q?=EA=B3=B5=EC=A7=80=20Scrap=20=EC=B6=94=EA=B0=80=20=20(#291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 오타 수정 --- .../adapter/in/web/NoticeQueryApiV2.java | 3 +- .../in/web/dto/NoticeRangeLookupResponse.java | 4 +- .../out/persistence/NoticeJdbcRepository.java | 12 +- .../persistence/NoticePersistenceAdapter.java | 16 +- .../persistence/NoticeQueryRepository.java | 8 +- .../NoticeQueryRepositoryImpl.java | 25 +- .../port/in/dto/NoticeRangeLookupCommand.java | 1 + .../port/in/dto/NoticeRangeLookupResult.java | 3 +- .../application/port/out/NoticeQueryPort.java | 8 +- .../application/port/out/dto/NoticeDto.java | 5 +- .../service/NoticeQueryService.java | 4 +- .../kuring/notice/domain/DepartmentName.java | 9 +- .../notice/domain/DepartmentNotice.java | 21 +- .../LatestPageGraduateNoticeApiClient.java | 97 + .../notice/LatestPageNoticeApiClient.java | 6 +- .../worker/scrap/deptinfo/DeptInfo.java | 44 +- .../architecture/ArchitectureDept.java | 1 + .../art_design/CommunicationDesignDept.java | 1 - .../art_design/IndustrialDesignDept.java | 3 +- .../deptinfo/art_design/LivingDesignDept.java | 1 + .../art_design/MovingImageFilmDept.java | 1 + .../education/EducationalTechnologyDept.java | 3 +- .../education/EnglishEducationDept.java | 1 + .../education/PhysicalEducationDept.java | 1 + .../deptinfo/engineering/BiologicalDept.java | 1 + .../engineering/ChemicalDivisionDept.java | 3 +- .../engineering/CivilEnvironmentDept.java | 3 +- .../engineering/ComputerScienceDept.java | 1 + .../ElectricalElectronicsDept.java | 1 + .../deptinfo/engineering/IndustrialDept.java | 1 + .../KBeautyIndustryFusionDept.java | 2 +- .../BioMedicalScienceDept.java | 1 + .../ku_integrated_science/CosmeticsDept.java | 1 + .../ku_integrated_science/EnergyDept.java | 1 + .../deptinfo/liberal_art/ChineseDept.java | 1 + .../liberal_art/CultureContentDept.java | 1 + .../deptinfo/liberal_art/EnglishDept.java | 1 + .../deptinfo/liberal_art/GeologyDept.java | 1 + .../deptinfo/liberal_art/HistoryDept.java | 1 + .../deptinfo/liberal_art/KoreanDept.java | 1 + .../liberal_art/MediaCommunicationDept.java | 1 + .../deptinfo/liberal_art/PhilosophyDept.java | 1 + .../deptinfo/real_estate/RealEstateDept.java | 3 +- .../AnimalScienceTechnologyDept.java | 1 + .../BiologicalSciencesDept.java | 1 + .../FoodMarketingSafetyDept.java | 1 + .../sanghuo_elective/VolunteerCenterDept.java | 2 +- .../deptinfo/science/MathematicsDept.java | 1 + .../scrap/deptinfo/science/PhysicsDept.java | 3 +- .../InternationalTradeDept.java | 1 + .../social_science/PoliticalScienceDept.java | 1 + .../PublicAdministrationDept.java | 3 +- .../DepartmentGraduationNoticeUpdater.java | 157 + .../notice/DepartmentNoticeUpdater.java | 55 +- .../update/notice/NoticeUpdateSupport.java | 8 +- .../V250905__Add_graduated_to_notice.sql | 2 + .../acceptance/NoticeAcceptanceTest.java | 36 +- .../kuring/acceptance/NoticeStep.java | 10 +- .../out/persistence/NoticeRepositoryTest.java | 4 +- .../notice/domain/DepartmentNoticeTest.java | 6 +- .../kuring/support/DatabaseConfigurator.java | 37 +- ...KuisHomepageNoticeScraperTemplateTest.java | 2 +- .../GraduateDeptInfoTest.java | 90 + .../notice/NoticeUpdateSupportTest.java | 2 +- .../resources/notice/graduate-cse-notice.html | 4108 +++++++++++++++++ 65 files changed, 4700 insertions(+), 135 deletions(-) create mode 100644 src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java create mode 100644 src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java create mode 100644 src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql create mode 100644 src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java create mode 100644 src/test/resources/notice/graduate-cse-notice.html diff --git a/src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java b/src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java index 8396ec083..7737c32c1 100644 --- a/src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java +++ b/src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java @@ -49,10 +49,11 @@ public ResponseEntity>> getNotices( @Parameter(description = "공지 타입") @RequestParam(name = "type") String type, @Parameter(description = "학과는 hostPrefix 로 전달") @RequestParam(name = "department", required = false) String department, @Parameter(description = "중요도") @RequestParam(name = "important", defaultValue = "false") Boolean important, + @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 searchResults = noticeQueryUseCase.getNotices(command) .stream() .map(NoticeRangeLookupResponse::from) diff --git a/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeRangeLookupResponse.java b/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeRangeLookupResponse.java index 37558b355..21acf9737 100644 --- a/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeRangeLookupResponse.java +++ b/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeRangeLookupResponse.java @@ -10,8 +10,9 @@ public record NoticeRangeLookupResponse( String subject, String category, Boolean important, + Boolean graduated, Long commentCount -){ +) { public static NoticeRangeLookupResponse from(NoticeRangeLookupResult result) { return new NoticeRangeLookupResponse( result.id(), @@ -21,6 +22,7 @@ public static NoticeRangeLookupResponse from(NoticeRangeLookupResult result) { result.subject(), result.category(), result.important(), + result.graduated(), result.commentCount() ); } diff --git a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java index c4de81d3c..395a1a798 100644 --- a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java +++ b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeJdbcRepository.java @@ -11,6 +11,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Types; import java.util.List; @Repository @@ -21,7 +22,7 @@ class NoticeJdbcRepository { @Transactional public void saveAllCategoryNotices(List notices) { - jdbcTemplate.batchUpdate("INSERT INTO notice (article_id, category_name, important, embedded, posted_dt, subject, updated_dt, url, dtype) values (?, ?, ?, ?, ?, ?, ?, ?, 'Notice')", + jdbcTemplate.batchUpdate("INSERT INTO notice (article_id, category_name, important, embedded, posted_dt, subject, updated_dt, url, graduated, dtype) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { @@ -34,6 +35,8 @@ public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(6, notice.getSubject()); ps.setString(7, notice.getUpdatedDate()); ps.setString(8, notice.getUrl()); + ps.setNull(9, Types.BOOLEAN); + ps.setString(10, "Notice"); } @Override @@ -45,7 +48,9 @@ public int getBatchSize() { @Transactional public void saveAllDepartmentNotices(List departmentNotices) { - jdbcTemplate.batchUpdate("INSERT INTO notice (article_id, category_name, important, embedded, posted_dt, subject, updated_dt, url, department_name, dtype) values (?, ?, ?, ?, ?, ?, ?, ?, ?, 'DepartmentNotice')", + jdbcTemplate.batchUpdate( + "INSERT INTO notice (article_id, category_name, important, embedded, posted_dt, subject, updated_dt, url, department_name, graduated, dtype) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { @@ -59,6 +64,8 @@ public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(7, departmentNotice.getUpdatedDate()); ps.setString(8, departmentNotice.getUrl()); ps.setString(9, DepartmentName.fromName(departmentNotice.getDepartmentName()).name()); + ps.setBoolean(10, departmentNotice.getGraduated()); + ps.setString(11, "DepartmentNotice"); } @Override @@ -67,4 +74,5 @@ public int getBatchSize() { } }); } + } diff --git a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java index 2ff4b395f..971acdf3f 100644 --- a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java +++ b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java @@ -80,13 +80,13 @@ public List findNormalArticleIdsByCategory(CategoryName categoryName) { } @Override - public List findImportantNoticesByDepartment(DepartmentName departmentName) { - return this.noticeRepository.findImportantNoticesByDepartment(departmentName); + public List findImportantNoticesByDepartment(DepartmentName departmentName, Boolean graduated) { + return this.noticeRepository.findImportantNoticesByDepartment(departmentName, graduated); } @Override - public List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable) { - return this.noticeRepository.findNormalNoticesByDepartmentWithOffset(departmentName, pageable); + public List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Boolean graduated, Pageable pageable) { + return this.noticeRepository.findNormalNoticesByDepartmentWithOffset(departmentName, graduated, pageable); } @Override @@ -100,13 +100,13 @@ public List findNormalArticleIdsByCategoryName(CategoryName categoryName } @Override - public List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum) { - return this.noticeRepository.findImportantArticleIdsByDepartment(departmentNameEnum); + public List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum, Boolean graduated) { + return this.noticeRepository.findImportantArticleIdsByDepartment(departmentNameEnum, graduated); } @Override - public List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum) { - return this.noticeRepository.findNormalArticleIdsByDepartment(departmentNameEnum); + public List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum, Boolean graduated) { + return this.noticeRepository.findNormalArticleIdsByDepartment(departmentNameEnum, graduated); } @Override diff --git a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java index 6b22d6664..6313c1584 100644 --- a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java +++ b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepository.java @@ -21,17 +21,17 @@ interface NoticeQueryRepository { void deleteAllByIdsAndCategory(CategoryName categoryName, List articleIds); - List findImportantNoticesByDepartment(DepartmentName departmentName); + List findImportantNoticesByDepartment(DepartmentName departmentName, Boolean graduated); - List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable); + List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Boolean graduated, Pageable pageable); List findImportantArticleIdsByCategoryName(CategoryName categoryName); List findNormalArticleIdsByCategoryName(CategoryName categoryName); - List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum); + List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum, Boolean graduated); - List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum); + List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum, Boolean graduated); void deleteAllByIdsAndDepartment(DepartmentName departmentName, List articleIds); diff --git a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java index 2d0ee1637..0aa35a4e2 100644 --- a/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java +++ b/src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java @@ -62,6 +62,7 @@ public List findNoticesByCategoryWithOffset(CategoryName categoryName notice.subject, notice.categoryName.stringValue().toLowerCase(), notice.important, + Expressions.nullExpression(), commentCount ) ).from(notice) @@ -125,6 +126,7 @@ public List findNotYetEmbeddingNoticeByDate(CategoryName categoryName notice.subject, notice.categoryName.stringValue().toLowerCase(), notice.important, + Expressions.nullExpression(), commentCount ) ).from(notice) @@ -156,6 +158,7 @@ public Optional findNoticeReadModelByArticleId(Long id) { notice.subject, notice.categoryName.stringValue().toLowerCase(), notice.important, + Expressions.nullExpression(), commentCount ) ).from(notice) @@ -216,31 +219,33 @@ public List findNormalArticleIdsByCategoryName(CategoryName categoryName @Transactional(readOnly = true) @Override - public List findImportantArticleIdsByDepartment(DepartmentName departmentName) { + public List findImportantArticleIdsByDepartment(DepartmentName departmentName, Boolean graduated) { return queryFactory .select(departmentNotice.articleId.castToNum(Integer.class)) .from(departmentNotice) .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.eq(true))) + .and(departmentNotice.important.eq(true)) + .and(departmentNotice.graduated.eq(graduated))) .orderBy(departmentNotice.articleId.castToNum(Integer.class).asc()) .fetch(); } @Transactional(readOnly = true) @Override - public List findNormalArticleIdsByDepartment(DepartmentName departmentName) { + public List findNormalArticleIdsByDepartment(DepartmentName departmentName, Boolean graduated) { return queryFactory .select(departmentNotice.articleId.castToNum(Integer.class)) .from(departmentNotice) .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.eq(false))) + .and(departmentNotice.important.eq(false)) + .and(departmentNotice.graduated.eq(graduated))) .orderBy(departmentNotice.articleId.castToNum(Integer.class).asc()) .fetch(); } @Transactional(readOnly = true) @Override - public List findImportantNoticesByDepartment(DepartmentName departmentName) { + public List findImportantNoticesByDepartment(DepartmentName departmentName, Boolean graduated) { StringTemplate postedDate = Expressions.stringTemplate( DATE_FORMAT_TEMPLATE, departmentNotice.noticeDateTime.postedDate, @@ -260,19 +265,21 @@ public List findImportantNoticesByDepartment(DepartmentName departmen departmentNotice.subject, departmentNotice.categoryName.stringValue().toLowerCase(), departmentNotice.important, + departmentNotice.graduated, commentCount ) ) .from(departmentNotice) .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.isTrue())) + .and(departmentNotice.important.isTrue()) + .and(departmentNotice.graduated.eq(graduated))) .orderBy(departmentNotice.noticeDateTime.postedDate.desc()) .fetch(); } @Transactional(readOnly = true) @Override - public List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable) { + public List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Boolean graduated, Pageable pageable) { StringTemplate postedDate = Expressions.stringTemplate( DATE_FORMAT_TEMPLATE, departmentNotice.noticeDateTime.postedDate, @@ -292,12 +299,14 @@ public List findNormalNoticesByDepartmentWithOffset(DepartmentName de departmentNotice.subject, departmentNotice.categoryName.stringValue().toLowerCase(), departmentNotice.important, + departmentNotice.graduated, commentCount ) ) .from(departmentNotice) .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.isFalse())) + .and(departmentNotice.important.isFalse()) + .and(departmentNotice.graduated.eq(graduated))) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .orderBy(departmentNotice.noticeDateTime.postedDate.desc()) diff --git a/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupCommand.java b/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupCommand.java index 8f804020f..51efd5887 100644 --- a/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupCommand.java +++ b/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupCommand.java @@ -4,6 +4,7 @@ public record NoticeRangeLookupCommand( String type, String department, Boolean important, + Boolean graduated, int page, int size ) { diff --git a/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupResult.java b/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupResult.java index 6b83ae30d..aebb2c334 100644 --- a/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupResult.java +++ b/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeRangeLookupResult.java @@ -8,6 +8,7 @@ public record NoticeRangeLookupResult( String subject, String category, Boolean important, + Boolean graduated, Long commentCount -){ +) { } diff --git a/src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeQueryPort.java b/src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeQueryPort.java index a26cc6a82..8765550b1 100644 --- a/src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeQueryPort.java +++ b/src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeQueryPort.java @@ -19,17 +19,17 @@ public interface NoticeQueryPort { List findNormalArticleIdsByCategory(CategoryName categoryName); - List findImportantNoticesByDepartment(DepartmentName departmentName); + List findImportantNoticesByDepartment(DepartmentName departmentName, Boolean graduated); - List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable); + List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Boolean graduated, Pageable pageable); List findImportantArticleIdsByCategoryName(CategoryName categoryName); List findNormalArticleIdsByCategoryName(CategoryName categoryName); - List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum); + List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum, Boolean graduated); - List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum); + List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum, Boolean graduated); List findAllByBookmarkIds(List ids); diff --git a/src/main/java/com/kustacks/kuring/notice/application/port/out/dto/NoticeDto.java b/src/main/java/com/kustacks/kuring/notice/application/port/out/dto/NoticeDto.java index 323145ec4..d4b70627e 100644 --- a/src/main/java/com/kustacks/kuring/notice/application/port/out/dto/NoticeDto.java +++ b/src/main/java/com/kustacks/kuring/notice/application/port/out/dto/NoticeDto.java @@ -27,12 +27,14 @@ public class NoticeDto { private Boolean important; + private Boolean graduated; + private Long commentCount; @QueryProjection public NoticeDto( Long id, String articleId, String postedDate, String url, String subject, - String category, Boolean important, Long commentCount + String category, Boolean important, Boolean graduated, Long commentCount ) { Assert.notNull(id, "id must not be null"); Assert.notNull(articleId, "articleId must not be null"); @@ -49,6 +51,7 @@ public NoticeDto( this.subject = subject; this.category = category; this.important = important; + this.graduated = graduated; this.commentCount = commentCount; } } diff --git a/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java b/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java index 9f0f7e5de..ec67ae59c 100644 --- a/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java +++ b/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java @@ -163,7 +163,7 @@ private List getDepartmentNoticeRangeLookup(NoticeRange if (command.isImportant()) { return noticeQueryPort - .findImportantNoticesByDepartment(departmentName) + .findImportantNoticesByDepartment(departmentName, command.graduated()) .stream() .map(NoticeQueryService::convertPortResult) .toList(); @@ -172,6 +172,7 @@ private List getDepartmentNoticeRangeLookup(NoticeRange return noticeQueryPort .findNormalNoticesByDepartmentWithOffset( departmentName, + command.graduated(), PageRequest.of(command.page(), command.size()) ).stream() .map(NoticeQueryService::convertPortResult) @@ -229,6 +230,7 @@ private static NoticeRangeLookupResult convertPortResult(NoticeDto dto) { dto.getSubject(), dto.getCategory(), dto.getImportant(), + dto.getGraduated(), dto.getCommentCount() ); } diff --git a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java index 9736a3f3a..60321a65b 100644 --- a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java +++ b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java @@ -76,7 +76,7 @@ public enum DepartmentName { APPAREL_DESIGN("apparel_design", "apparel", "의상디자인학과"), LIVING_DESIGN("living_design", "livingdesign", "리빙디자인학과"), CONT_ART("contemporary_art", "contemporaryart", "현대미술학과"), - MOV_IMAGE("moving_image_film", "movingimages", "영상영화학과"), + MOV_IMAGE("moving_image_film", "movingimages", "영상학과"), JAPANESE_EDU("japanese_education", "japan", "일어교육과"), MATH_EDU("mathematics_education", "mathedu", "수학교육과"), @@ -84,19 +84,20 @@ public enum DepartmentName { MUSIC_EDU("music_education", "music", "음악교육과"), EDU_TECH("education_technology", "edutech", "교육공학과"), ENGLISH_EDU("english_education", "englishedu", "영어교육과"), - EDUCATION("education", "edu", "교직과"), + EDUCATION("education", "edu", "교육학과"), ELE_EDU_CENTER("elective_education_center", "sgedu", "교양교육센터"), VOLUNTEER("volunteer_center", "kuvolunteer", "사회봉사센터"), - LIBERAL_STUDIES("liberal_studies", "kusls", "KU자유전공학부"); + LIBERAL_STUDIES("liberal_studies", "kusls", "KU자유전공학부"), + ; private static final Map NAME_MAP; private static final Map HOST_PREFIX_MAP; private static final Map KOR_NAME_MAP; static { NAME_MAP = Collections.unmodifiableMap(Arrays.stream(DepartmentName.values()) - .collect(Collectors.toMap(DepartmentName::getName, DepartmentName::name))); + .collect(Collectors.toMap(DepartmentName::getName, DepartmentName::name))); HOST_PREFIX_MAP = Collections.unmodifiableMap(Arrays.stream(DepartmentName.values()) .collect(Collectors.toMap(DepartmentName::getHostPrefix, DepartmentName::name))); diff --git a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNotice.java b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNotice.java index 0f49f07e5..4bd0eacbe 100644 --- a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNotice.java +++ b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNotice.java @@ -1,13 +1,14 @@ package com.kustacks.kuring.notice.domain; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.NoArgsConstructor; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + import java.util.Objects; @Entity @@ -18,20 +19,26 @@ public class DepartmentNotice extends Notice { @Enumerated(EnumType.STRING) private DepartmentName departmentName; + @Getter + @Column(name = "graduated") + private Boolean graduated; + @Builder public DepartmentNotice(String articleId, String postedDate, String updatedDate, String subject, CategoryName categoryName, Boolean important, - String fullUrl, DepartmentName departmentName) - { + String fullUrl, DepartmentName departmentName, Boolean graduated) { super(articleId, postedDate, updatedDate, subject, categoryName, important, fullUrl); this.departmentName = departmentName; + this.graduated = graduated; } public String getDepartmentName() { return departmentName.getName(); } - public String getDepartmentKorName() { return departmentName.getKorName(); } + public String getDepartmentKorName() { + return departmentName.getKorName(); + } @Override public boolean equals(Object o) { diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java b/src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java new file mode 100644 index 000000000..c3cfa5ffa --- /dev/null +++ b/src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageGraduateNoticeApiClient.java @@ -0,0 +1,97 @@ +package com.kustacks.kuring.worker.scrap.client.notice; + +import com.kustacks.kuring.common.exception.InternalLogicException; +import com.kustacks.kuring.common.exception.code.ErrorCode; +import com.kustacks.kuring.worker.dto.ScrapingResultDto; +import com.kustacks.kuring.worker.scrap.client.NormalJsoupClient; +import com.kustacks.kuring.worker.scrap.deptinfo.DeptInfo; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +public class LatestPageGraduateNoticeApiClient implements NoticeApiClient { + + private static final int START_PAGE_NUM = 1; // page는 인자가 1부터 시작 + private static final int ROW_NUMBERS_PER_PAGE = 20; + private static final int LATEST_SCRAP_TIMEOUT = 2000; // 2초 + private static final int LATEST_SCRAP_ALL_TIMEOUT = 60000; // 1분 + + private final NormalJsoupClient jsoupClient; + + public LatestPageGraduateNoticeApiClient(NormalJsoupClient normalJsoupClient) { + this.jsoupClient = normalJsoupClient; + } + + @Override + public List request(DeptInfo deptInfo) throws InternalLogicException { + try { + ScrapingResultDto resultDto = getScrapingResultDto(deptInfo, ROW_NUMBERS_PER_PAGE, LATEST_SCRAP_TIMEOUT); + return List.of(resultDto); + } catch (IOException e) { + throw new InternalLogicException(ErrorCode.NOTICE_SCRAPER_CANNOT_SCRAP, e); + } catch (NullPointerException | IndexOutOfBoundsException e) { + throw new InternalLogicException(ErrorCode.NOTICE_SCRAPER_CANNOT_PARSE, e); + } + } + + @Override + public List requestAll(DeptInfo deptInfo) throws InternalLogicException { + try { + String url = buildUrlForTotalNoticeCount(deptInfo); + int totalNoticeSize = getTotalNoticeSize(url); + + ScrapingResultDto resultDto = getScrapingResultDto(deptInfo, totalNoticeSize, LATEST_SCRAP_ALL_TIMEOUT); + return List.of(resultDto); + } catch (IOException e) { + log.warn("Department Scrap all IOException: {}", e.getMessage()); + } catch (NullPointerException | IndexOutOfBoundsException e) { + throw new InternalLogicException(ErrorCode.NOTICE_SCRAPER_CANNOT_PARSE, e); + } + + return Collections.emptyList(); + } + + @Override + public ScrapingResultDto requestSinglePageWithUrl(DeptInfo noticeInfo, String url) { + throw new InternalLogicException(ErrorCode.NOTICE_SCRAPER_CANNOT_PARSE); + } + + public int getTotalNoticeSize(String url) throws IOException, IndexOutOfBoundsException, NullPointerException { + Document document = jsoupClient.get(url, LATEST_SCRAP_TIMEOUT); + + Element totalNoticeSizeElement = document.selectFirst(".util-search strong"); + if (totalNoticeSizeElement == null) { + totalNoticeSizeElement = document.selectFirst(".total_count"); + } + + assert totalNoticeSizeElement != null; + return Integer.parseInt(totalNoticeSizeElement.ownText()); + } + + private String buildUrlForTotalNoticeCount(DeptInfo deptInfo) { + return deptInfo.createGraduateRequestUrl(1, 1); + } + + private ScrapingResultDto getScrapingResultDto(DeptInfo deptInfo, int rowSize, int timeout) throws IOException { + String requestUrl = deptInfo.createGraduateRequestUrl(START_PAGE_NUM, rowSize); + String viewUrl = deptInfo.createGraduateViewUrl(); + + StopWatch stopWatch = new StopWatch(deptInfo.getDeptName() + "Request"); + stopWatch.start(); + + Document document = jsoupClient.get(requestUrl, timeout); + + stopWatch.stop(); + log.debug("[{}] takes {}millis to respond", deptInfo.getDeptName(), stopWatch.getTotalTimeMillis()); + + return new ScrapingResultDto(document, viewUrl); + } +} diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageNoticeApiClient.java b/src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageNoticeApiClient.java index c8802cb1d..e58da675a 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageNoticeApiClient.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/client/notice/LatestPageNoticeApiClient.java @@ -77,12 +77,12 @@ public int getTotalNoticeSize(String url) throws IOException, IndexOutOfBoundsEx } private String buildUrlForTotalNoticeCount(DeptInfo deptInfo) { - return deptInfo.createRequestUrl(1, 1); + return deptInfo.createUndergraduateRequestUrl(1, 1); } private ScrapingResultDto getScrapingResultDto(DeptInfo deptInfo, int rowSize, int timeout) throws IOException { - String requestUrl = deptInfo.createRequestUrl(START_PAGE_NUM, rowSize); - String viewUrl = deptInfo.createViewUrl(); + String requestUrl = deptInfo.createUndergraduateRequestUrl(START_PAGE_NUM, rowSize); + String viewUrl = deptInfo.createUndergraduateViewUrl(); StopWatch stopWatch = new StopWatch(deptInfo.getDeptName() + "Request"); stopWatch.start(); diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java index 4cda32b4d..83a5c393a 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/DeptInfo.java @@ -1,11 +1,12 @@ package com.kustacks.kuring.worker.scrap.deptinfo; import com.kustacks.kuring.notice.domain.DepartmentName; -import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; -import com.kustacks.kuring.worker.scrap.client.notice.NoticeApiClient; import com.kustacks.kuring.worker.dto.ScrapingResultDto; import com.kustacks.kuring.worker.parser.notice.NoticeHtmlParserTemplate; import com.kustacks.kuring.worker.parser.notice.RowsDto; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; +import com.kustacks.kuring.worker.scrap.client.notice.NoticeApiClient; +import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import lombok.Getter; import org.jsoup.nodes.Document; import org.springframework.web.util.UriComponentsBuilder; @@ -22,6 +23,9 @@ public class DeptInfo { protected StaffScrapInfo staffScrapInfo; protected DepartmentName departmentName; protected String collegeName; + protected NoticeScrapInfo noticeGraduationInfo; + protected LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient; + public List scrapLatestPageHtml() { return noticeApiClient.request(this); @@ -31,6 +35,15 @@ public List scrapAllPageHtml() { return noticeApiClient.requestAll(this); } + public List scrapGraduateLatestPageHtml() { + return latestPageGraduateNoticeApiClient.request(this); + } + + public List scrapGraduateAllPageHtml() { + return latestPageGraduateNoticeApiClient.requestAll(this); + } + + public RowsDto parse(Document document) { return htmlParser.parse(document); } @@ -51,7 +64,7 @@ public String getStaffSiteName() { return this.staffScrapInfo.getSiteName(); } - public String createRequestUrl(int page, int row) { + public String createUndergraduateRequestUrl(int page, int row) { return UriComponentsBuilder .fromUriString(latestPageNoticeProperties.listUrl()) .queryParam("page", page) @@ -63,19 +76,42 @@ public String createRequestUrl(int page, int row) { ).toUriString(); } - public String createViewUrl() { + public String createUndergraduateViewUrl() { return latestPageNoticeProperties.viewUrl() .replaceAll("\\{department\\}", noticeScrapInfo.getSiteName()) .replace("{siteId}", String.valueOf(noticeScrapInfo.getSiteId())); } + 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 createGraduateViewUrl() { + return latestPageNoticeProperties.viewUrl() + .replaceAll("\\{department\\}", noticeGraduationInfo.getSiteName()) + .replace("{siteId}", String.valueOf(noticeGraduationInfo.getSiteId())); + } + public boolean isSupportStaffScrap() { return !this.staffScrapInfo.getSiteIds().isEmpty(); } + public boolean isSupportGraduateScrap() { + return this.noticeGraduationInfo != null; + } + @Override public String toString() { return departmentName.getName(); } + } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java index 436cfc35b..6bceb5695 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java @@ -28,5 +28,6 @@ public ArchitectureDept( this.staffScrapInfo = new StaffScrapInfo(ARCHITECTURE.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(ARCHITECTURE.getHostPrefix(), 397); this.departmentName = ARCHITECTURE; + this.noticeGraduationInfo = new NoticeScrapInfo(ARCHITECTURE.getHostPrefix(), 748); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/CommunicationDesignDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/CommunicationDesignDept.java index df5c3176a..9a1616fe9 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/CommunicationDesignDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/CommunicationDesignDept.java @@ -7,7 +7,6 @@ import com.kustacks.kuring.worker.scrap.deptinfo.RegisterDepartmentMap; import com.kustacks.kuring.worker.scrap.deptinfo.StaffScrapInfo; -import java.util.Collections; import java.util.List; import static com.kustacks.kuring.notice.domain.DepartmentName.COMM_DESIGN; diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java index 09b891ff7..ad08ef9bb 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java @@ -25,8 +25,9 @@ public IndustrialDesignDept( this.latestPageNoticeProperties = latestPageNoticeProperties; List siteIds = List.of(17316); - this.staffScrapInfo = new StaffScrapInfo(IND_DESIGN.getHostPrefix(),siteIds); + this.staffScrapInfo = new StaffScrapInfo(IND_DESIGN.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(IND_DESIGN.getHostPrefix(), 4017); this.departmentName = IND_DESIGN; + this.noticeGraduationInfo = new NoticeScrapInfo(IND_DESIGN.getHostPrefix(), 5683); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java index 3ce395c86..0d74bacfd 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java @@ -29,5 +29,6 @@ public LivingDesignDept( this.staffScrapInfo = new StaffScrapInfo(LIVING_DESIGN.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(LIVING_DESIGN.getHostPrefix(), 962); this.departmentName = LIVING_DESIGN; + this.noticeGraduationInfo = new NoticeScrapInfo(LIVING_DESIGN.getHostPrefix(), 487); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java index 0d4f7e99c..80681242c 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java @@ -28,5 +28,6 @@ public MovingImageFilmDept( this.staffScrapInfo = new StaffScrapInfo(MOV_IMAGE.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(MOV_IMAGE.getHostPrefix(), 491); this.departmentName = MOV_IMAGE; + this.noticeGraduationInfo = new NoticeScrapInfo(MOV_IMAGE.getHostPrefix(), 5710); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java index 0d815d0c2..2ea220eb9 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java @@ -25,8 +25,9 @@ public EducationalTechnologyDept( this.latestPageNoticeProperties = latestPageNoticeProperties; List siteIds = List.of(16532); - this.staffScrapInfo = new StaffScrapInfo(EDU_TECH.getHostPrefix(),siteIds); + this.staffScrapInfo = new StaffScrapInfo(EDU_TECH.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(EDU_TECH.getHostPrefix(), 4020); this.departmentName = EDU_TECH; + this.noticeGraduationInfo = new NoticeScrapInfo(EDU_TECH.getHostPrefix(), 4092); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java index 6fb6ed253..8ca1a7811 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java @@ -28,5 +28,6 @@ public EnglishEducationDept( this.staffScrapInfo = new StaffScrapInfo(ENGLISH_EDU.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(ENGLISH_EDU.getHostPrefix(), 505); this.departmentName = ENGLISH_EDU; + this.noticeGraduationInfo = new NoticeScrapInfo(ENGLISH_EDU.getHostPrefix(), 990); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java index 0ec932ea3..7f07403d3 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java @@ -28,5 +28,6 @@ public PhysicalEducationDept( this.staffScrapInfo = new StaffScrapInfo(PHY_EDU.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(PHY_EDU.getHostPrefix(), 501); this.departmentName = PHY_EDU; + this.noticeGraduationInfo = new NoticeScrapInfo(PHY_EDU.getHostPrefix(), 979); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java index 16c6adcdf..e9dbf5ed3 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java @@ -28,5 +28,6 @@ public BiologicalDept( this.staffScrapInfo = new StaffScrapInfo(BIOLOGICAL.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(BIOLOGICAL.getHostPrefix(), 790); this.departmentName = BIOLOGICAL; + this.noticeGraduationInfo = new NoticeScrapInfo(BIOLOGICAL.getHostPrefix(), 417); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java index 46f4f7b9d..62b0c3d42 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java @@ -13,7 +13,7 @@ @RegisterDepartmentMap(key = CHEMI_DIV) public class ChemicalDivisionDept extends EngineeringCollege { - + public ChemicalDivisionDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, @@ -28,5 +28,6 @@ public ChemicalDivisionDept( this.staffScrapInfo = new StaffScrapInfo(CHEMI_DIV.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), 409); this.departmentName = CHEMI_DIV; + this.noticeGraduationInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), 769); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java index a524a34fc..6f1fc6588 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java @@ -13,7 +13,7 @@ @RegisterDepartmentMap(key = CIVIL_ENV) public class CivilEnvironmentDept extends EngineeringCollege { - + public CivilEnvironmentDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, @@ -28,5 +28,6 @@ public CivilEnvironmentDept( this.staffScrapInfo = new StaffScrapInfo(CIVIL_ENV.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(CIVIL_ENV.getHostPrefix(), 401); this.departmentName = CIVIL_ENV; + this.noticeGraduationInfo = new NoticeScrapInfo(CIVIL_ENV.getHostPrefix(), 756); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java index e798c8d18..800087074 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java @@ -28,5 +28,6 @@ public ComputerScienceDept( this.staffScrapInfo = new StaffScrapInfo(COMPUTER.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(COMPUTER.getHostPrefix(), 775); this.departmentName = COMPUTER; + this.noticeGraduationInfo = new NoticeScrapInfo(COMPUTER.getHostPrefix(), 411); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java index c6a493c10..d741aa8bf 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java @@ -28,5 +28,6 @@ public ElectricalElectronicsDept( this.staffScrapInfo = new StaffScrapInfo(ELEC_ELEC.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), 407); this.departmentName = ELEC_ELEC; + this.noticeGraduationInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), 767); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java index 7572054ca..7be82c118 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java @@ -28,5 +28,6 @@ public IndustrialDept( this.staffScrapInfo = new StaffScrapInfo(INDUSTRIAL.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(INDUSTRIAL.getHostPrefix(), 413); this.departmentName = INDUSTRIAL; + this.noticeGraduationInfo = new NoticeScrapInfo(INDUSTRIAL.getHostPrefix(), 778); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/KBeautyIndustryFusionDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/KBeautyIndustryFusionDept.java index 9386e89b6..232870946 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/KBeautyIndustryFusionDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/KBeautyIndustryFusionDept.java @@ -13,7 +13,7 @@ @RegisterDepartmentMap(key = KBEAUTY) public class KBeautyIndustryFusionDept extends EngineeringCollege { - + public KBeautyIndustryFusionDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java index 35144b167..dd1a8ec79 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java @@ -28,5 +28,6 @@ public BioMedicalScienceDept( this.staffScrapInfo = new StaffScrapInfo(BIO_MEDICAL.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(BIO_MEDICAL.getHostPrefix(), 880); this.departmentName = BIO_MEDICAL; + this.noticeGraduationInfo = new NoticeScrapInfo(BIO_MEDICAL.getHostPrefix(), 883); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java index ca9c7a958..554d41997 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java @@ -28,5 +28,6 @@ public CosmeticsDept( this.staffScrapInfo = new StaffScrapInfo(COSMETICS.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(COSMETICS.getHostPrefix(), 457); this.departmentName = COSMETICS; + this.noticeGraduationInfo = new NoticeScrapInfo(COSMETICS.getHostPrefix(), 873); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java index 4e816ae7e..14dd1b1d6 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java @@ -28,5 +28,6 @@ public EnergyDept( this.staffScrapInfo = new StaffScrapInfo(ENERGY.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(ENERGY.getHostPrefix(), 451); this.departmentName = ENERGY; + this.noticeGraduationInfo = new NoticeScrapInfo(ENERGY.getHostPrefix(), 848); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java index 8c95c00bb..4e5cce466 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java @@ -28,5 +28,6 @@ public ChineseDept( this.staffScrapInfo = new StaffScrapInfo(CHINESE.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(CHINESE.getHostPrefix(), 353); this.departmentName = CHINESE; + this.noticeGraduationInfo = new NoticeScrapInfo(CHINESE.getHostPrefix(), 709); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java index 6110186bc..92d574bd5 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java @@ -28,5 +28,6 @@ public CultureContentDept( this.staffScrapInfo = new StaffScrapInfo(CULTURE_CONT.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(CULTURE_CONT.getHostPrefix(), 661); this.departmentName = CULTURE_CONT; + this.noticeGraduationInfo = new NoticeScrapInfo(CULTURE_CONT.getHostPrefix(), 379); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java index fa89fc733..f98c364fa 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java @@ -28,5 +28,6 @@ public EnglishDept( this.staffScrapInfo = new StaffScrapInfo(ENGLISH.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(ENGLISH.getHostPrefix(), 347); this.departmentName = ENGLISH; + this.noticeGraduationInfo = new NoticeScrapInfo(ENGLISH.getHostPrefix(), 350); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java index faf29efa1..a0d1b1436 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java @@ -28,5 +28,6 @@ public GeologyDept( this.staffScrapInfo = new StaffScrapInfo(GEOLOGY.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(GEOLOGY.getHostPrefix(), 373); this.departmentName = GEOLOGY; + this.noticeGraduationInfo = new NoticeScrapInfo(GEOLOGY.getHostPrefix(), 716); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java index b78c6a28f..c235971c6 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java @@ -28,5 +28,6 @@ public HistoryDept( this.staffScrapInfo = new StaffScrapInfo(HISTORY.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), 361); this.departmentName = HISTORY; + this.noticeGraduationInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), 363); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java index 4a18019f4..0ddd61b31 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java @@ -28,5 +28,6 @@ public KoreanDept( this.staffScrapInfo = new StaffScrapInfo(KOREAN.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(KOREAN.getHostPrefix(), 334); this.departmentName = KOREAN; + this.noticeGraduationInfo = new NoticeScrapInfo(KOREAN.getHostPrefix(), 332); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java index 1b349bab5..b7605c3a5 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java @@ -28,5 +28,6 @@ public MediaCommunicationDept( 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); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java index b93203a25..b87f6d873 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java @@ -28,5 +28,6 @@ public PhilosophyDept( this.staffScrapInfo = new StaffScrapInfo(PHILOSOPHY.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(PHILOSOPHY.getHostPrefix(), 356); this.departmentName = PHILOSOPHY; + this.noticeGraduationInfo = new NoticeScrapInfo(PHILOSOPHY.getHostPrefix(), 360); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java index 4b83d956c..6a4bfcb14 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java @@ -23,8 +23,9 @@ public RealEstateDept(RealEstateNoticeApiClient realEstateNoticeApiClient, this.latestPageNoticeProperties = latestPageNoticeProperties; List siteIds = List.of(13949); - this.staffScrapInfo = new StaffScrapInfo("kure",siteIds); + this.staffScrapInfo = new StaffScrapInfo("kure", siteIds); this.noticeScrapInfo = new NoticeScrapInfo(REAL_ESTATE.getHostPrefix(), 1563); this.departmentName = REAL_ESTATE; + this.noticeGraduationInfo = new NoticeScrapInfo(REAL_ESTATE.getHostPrefix(), 1565); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java index f74c61433..e7b108ab9 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java @@ -28,5 +28,6 @@ public AnimalScienceTechnologyDept( 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); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java index b60c3f625..5c5219fed 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java @@ -29,5 +29,6 @@ public BiologicalSciencesDept( this.staffScrapInfo = new StaffScrapInfo(BIO_SCIENCE.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), 909); this.departmentName = BIO_SCIENCE; + this.noticeGraduationInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), 905); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java index ef5e8a99d..4dbdc41fd 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java @@ -28,5 +28,6 @@ public FoodMarketingSafetyDept( this.staffScrapInfo = new StaffScrapInfo(FOOD_MARKETING.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(FOOD_MARKETING.getHostPrefix(), 929); this.departmentName = FOOD_MARKETING; + this.noticeGraduationInfo = new NoticeScrapInfo(FOOD_MARKETING.getHostPrefix(), 475); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_elective/VolunteerCenterDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_elective/VolunteerCenterDept.java index 5fe2764fb..54b21cb48 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_elective/VolunteerCenterDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_elective/VolunteerCenterDept.java @@ -26,7 +26,7 @@ public VolunteerCenterDept( this.latestPageNoticeProperties = latestPageNoticeProperties; List siteIds = Collections.emptyList(); - this.staffScrapInfo = new StaffScrapInfo(null,siteIds); + this.staffScrapInfo = new StaffScrapInfo(null, siteIds); this.noticeScrapInfo = new NoticeScrapInfo(VOLUNTEER.getHostPrefix(), 523); this.departmentName = VOLUNTEER; } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java index 12dd75613..65d1158a8 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java @@ -28,5 +28,6 @@ public MathematicsDept( this.staffScrapInfo = new StaffScrapInfo(MATH.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(MATH.getHostPrefix(), 727); this.departmentName = MATH; + this.noticeGraduationInfo = new NoticeScrapInfo(MATH.getHostPrefix(), 391); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java index 33455ceb9..2ff07d0d4 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java @@ -24,8 +24,9 @@ public PhysicsDept( this.latestPageNoticeProperties = latestPageNoticeProperties; List siteIds = List.of(9780); - this.staffScrapInfo = new StaffScrapInfo(PHYSICS.getHostPrefix(),siteIds); + this.staffScrapInfo = new StaffScrapInfo(PHYSICS.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(PHYSICS.getHostPrefix(), 393); this.departmentName = PHYSICS; + this.noticeGraduationInfo = new NoticeScrapInfo(PHYSICS.getHostPrefix(), 735); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java index 8494af972..220cecedb 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java @@ -28,5 +28,6 @@ public InternationalTradeDept( this.staffScrapInfo = new StaffScrapInfo(INT_TRADE.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(INT_TRADE.getHostPrefix(), 429); this.departmentName = INT_TRADE; + this.noticeGraduationInfo = new NoticeScrapInfo(INT_TRADE.getHostPrefix(), 815); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java index 3ad0e3068..36be5fa70 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java @@ -28,5 +28,6 @@ public PoliticalScienceDept( this.staffScrapInfo = new StaffScrapInfo(POLITICS.getHostPrefix(), siteIds); this.noticeScrapInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), 803); this.departmentName = POLITICS; + this.noticeGraduationInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), 421); } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java index 44b206870..83a3187d6 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java @@ -25,8 +25,9 @@ public PublicAdministrationDept( this.latestPageNoticeProperties = latestPageNoticeProperties; List siteIds = List.of(10264); - this.staffScrapInfo = new StaffScrapInfo("kupa",siteIds); + this.staffScrapInfo = new StaffScrapInfo("kupa", siteIds); this.noticeScrapInfo = new NoticeScrapInfo(ADMINISTRATION.getHostPrefix(), 855); this.departmentName = ADMINISTRATION; + this.noticeGraduationInfo = new NoticeScrapInfo(ADMINISTRATION.getHostPrefix(), 427); } } diff --git a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java new file mode 100644 index 000000000..be7bda345 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java @@ -0,0 +1,157 @@ +package com.kustacks.kuring.worker.update.notice; + +import com.kustacks.kuring.common.featureflag.FeatureFlags; +import com.kustacks.kuring.common.featureflag.KuringFeatures; +import com.kustacks.kuring.message.application.service.FirebaseNotificationService; +import com.kustacks.kuring.notice.application.port.out.NoticeCommandPort; +import com.kustacks.kuring.notice.application.port.out.NoticeQueryPort; +import com.kustacks.kuring.notice.domain.DepartmentName; +import com.kustacks.kuring.notice.domain.DepartmentNotice; +import com.kustacks.kuring.worker.dto.ComplexNoticeFormatDto; +import com.kustacks.kuring.worker.dto.ScrapingResultDto; +import com.kustacks.kuring.worker.scrap.DepartmentNoticeScraperTemplate; +import com.kustacks.kuring.worker.scrap.deptinfo.DeptInfo; +import com.kustacks.kuring.worker.update.notice.dto.response.CommonNoticeFormatDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import static com.kustacks.kuring.notice.domain.DepartmentName.REAL_ESTATE; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DepartmentGraduationNoticeUpdater { + + private final List deptInfoList; + private final DepartmentNoticeScraperTemplate scrapperTemplate; + private final NoticeQueryPort noticeQueryPort; + private final NoticeCommandPort noticeCommandPort; + private final ThreadPoolTaskExecutor noticeUpdaterThreadTaskExecutor; + private final FirebaseNotificationService notificationService; + private final NoticeUpdateSupport noticeUpdateSupport; + private final FeatureFlags featureFlags; + + @Scheduled(cron = "0 15/20 7-19 * * *", zone = "Asia/Seoul") // 학교 공지는 오전 7:15 ~ 오후 7:55분 사이에 20분마다 업데이트 된다. + public void update() { + log.info("******** 학과별 (대학원) 최신 공지 업데이트 시작 ********"); + + List graduateDeptInfoList = getGraduateDeptInfoList(); + + for (DeptInfo deptInfo : graduateDeptInfoList) { + CompletableFuture + .supplyAsync( + () -> updateDepartmentAsync(deptInfo, DeptInfo::scrapGraduateLatestPageHtml), + noticeUpdaterThreadTaskExecutor + ).thenApply( + scrapResults -> compareLatestAndUpdateDB(scrapResults, deptInfo.getDeptName()) + ).thenAccept( + notificationService::sendNotifications + ); + } + } + + @Scheduled(cron = "0 0 1 * * 6", zone = "Asia/Seoul") // 전체 업데이트는 매주 토요일 오전 1시에 한다. + public void updateAll() { + if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_NOTICE.getFeature())) { + log.info("******** 학과별 (대학원) 전체 공지 업데이트 시작 ********"); + + List graduateDeptInfoList = getGraduateDeptInfoList(); + + for (DeptInfo deptInfo : graduateDeptInfoList) { + if (deptInfo.isSameDepartment(REAL_ESTATE)) { + continue; + } + + CompletableFuture + .supplyAsync(() -> updateDepartmentAsync(deptInfo, DeptInfo::scrapGraduateAllPageHtml), noticeUpdaterThreadTaskExecutor) + .thenAccept(scrapResults -> compareAllAndUpdateDB(scrapResults, deptInfo.getDeptName())); + } + + } + } + + private List getGraduateDeptInfoList() { + return deptInfoList.stream() + .filter(DeptInfo::isSupportGraduateScrap) + .toList(); + } + + private List updateDepartmentAsync(DeptInfo deptInfo, Function> decisionMaker) { + return scrapperTemplate.scrap(deptInfo, decisionMaker); + } + + private List compareLatestAndUpdateDB(List scrapResults, String departmentName) { + DepartmentName departmentNameEnum = DepartmentName.fromKor(departmentName); + + List newNoticeList = new ArrayList<>(); + for (ComplexNoticeFormatDto scrapResult : scrapResults) { + + // DB에서 모든 중요 공지를 가져와서 + List savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum, true); + + // db와 싱크를 맞춘다 + List newImportantNotices = saveNewNotices(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true, true); + newNoticeList.addAll(newImportantNotices); + + // DB에서 모든 일반 공지 id를 가져와서 + List savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum, true); + + // db와 싱크를 맞춘다 + List newNormalNotices = saveNewNotices(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false, true); + newNoticeList.addAll(newNormalNotices); + } + + return newNoticeList; + } + + private List saveNewNotices(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduate) { + List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduate); + noticeCommandPort.saveAllDepartmentNotices(newNotices); + return newNotices; + } + + private void compareAllAndUpdateDB(List scrapResults, String departmentName) { + if (scrapResults.isEmpty()) { + return; + } + + DepartmentName departmentNameEnum = DepartmentName.fromKor(departmentName); + + for (ComplexNoticeFormatDto scrapResult : scrapResults) { + + // DB에서 최신 중요 공지를 가져와서 + List savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum, true); + + // db와 싱크를 맞춘다 + synchronizationWithDb(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true, true); + + // DB에서 모든 일반 공지의 id를 가져와서 + List savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum, true); + + // db와 싱크를 맞춘다 + synchronizationWithDb(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false, true); + } + } + + private void synchronizationWithDb(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduate) { + List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduate); + + List latestNoticeIds = noticeUpdateSupport.extractDepartmentNoticeIds(scrapResults); + + List deletedNoticesArticleIds = noticeUpdateSupport.filteringSoonDeleteDepartmentNoticeIds(savedArticleIds, latestNoticeIds); + + noticeCommandPort.saveAllDepartmentNotices(newNotices); + + if (!deletedNoticesArticleIds.isEmpty()) { + noticeCommandPort.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds); + } + } +} diff --git a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java index 5f96e206f..31a5cbf9c 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java @@ -42,37 +42,37 @@ public class DepartmentNoticeUpdater { @Scheduled(cron = "0 15/20 7-19 * * *", zone = "Asia/Seoul") // 학교 공지는 오전 7:15 ~ 오후 7:55분 사이에 20분마다 업데이트 된다. public void update() { - if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_NOTICE.getFeature())) { - log.info("******** 학과별 최신 공지 업데이트 시작 ********"); + log.info("******** 학과별 (학사) 최신 공지 업데이트 시작 ********"); + + 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 - ); - } } } @Scheduled(cron = "0 0 23 * * 5", zone = "Asia/Seoul") // 전체 업데이트는 매주 금요일 오후 11시에 한다. public void updateAll() { 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())); + } + } } @@ -85,26 +85,27 @@ private List compareLatestAndUpdateDB(List newNoticeList = new ArrayList<>(); for (ComplexNoticeFormatDto scrapResult : scrapResults) { + // DB에서 모든 중요 공지를 가져와서 - List savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum); + List savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum, false); // db와 싱크를 맞춘다 - List newImportantNotices = saveNewNotices(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true); + List newImportantNotices = saveNewNotices(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true, false); newNoticeList.addAll(newImportantNotices); // DB에서 모든 일반 공지 id를 가져와서 - List savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum); + List savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum, false); // db와 싱크를 맞춘다 - List newNormalNotices = saveNewNotices(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false); + List newNormalNotices = saveNewNotices(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false, false); newNoticeList.addAll(newNormalNotices); } return newNoticeList; } - private List saveNewNotices(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important) { - List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important); + private List saveNewNotices(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduated) { + List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduated); noticeCommandPort.saveAllDepartmentNotices(newNotices); return newNotices; } @@ -118,21 +119,21 @@ private void compareAllAndUpdateDB(List scrapResults, St for (ComplexNoticeFormatDto scrapResult : scrapResults) { // DB에서 최신 중요 공지를 가져와서 - List savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum); + List savedImportantArticleIds = noticeQueryPort.findImportantArticleIdsByDepartment(departmentNameEnum, false); // db와 싱크를 맞춘다 - synchronizationWithDb(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true); + synchronizationWithDb(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true, false); // DB에서 모든 일반 공지의 id를 가져와서 - List savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum); + List savedNormalArticleIds = noticeQueryPort.findNormalArticleIdsByDepartment(departmentNameEnum, false); // db와 싱크를 맞춘다 - synchronizationWithDb(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false); + synchronizationWithDb(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false, false); } } - private void synchronizationWithDb(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important) { - List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important); + private void synchronizationWithDb(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduated) { + List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduated); List latestNoticeIds = noticeUpdateSupport.extractDepartmentNoticeIds(scrapResults); diff --git a/src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java b/src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java index ca258a6ab..2abc1fc1a 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java +++ b/src/main/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupport.java @@ -53,13 +53,14 @@ public List filteringSoonSaveDepartmentNotices( List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, - boolean important + boolean important, + boolean graduated ) { List newNotices = new LinkedList<>(); // 뒤에 추가만 계속 하기 때문에 arrayList가 아닌 Linked List 사용 O(1) for (CommonNoticeFormatDto notice : scrapResults) { try { if (Collections.binarySearch(savedArticleIds, Integer.valueOf(notice.getArticleId())) < 0) { // 정렬되어있다, 이진탐색으로 O(logN)안에 수행 - DepartmentNotice newDepartmentNotice = convert(notice, departmentNameEnum, important); + DepartmentNotice newDepartmentNotice = convert(notice, departmentNameEnum, important, graduated); newNotices.add(newDepartmentNotice); } } catch (IncorrectResultSizeDataAccessException e) { @@ -118,7 +119,7 @@ private Notice convert(CommonNoticeFormatDto dto, CategoryName CategoryName, boo dto.getFullUrl()); } - private DepartmentNotice convert(CommonNoticeFormatDto dto, DepartmentName departmentNameEnum, boolean important) { + private DepartmentNotice convert(CommonNoticeFormatDto dto, DepartmentName departmentNameEnum, boolean important, boolean graduated) { return DepartmentNotice.builder() .articleId(dto.getArticleId()) .postedDate(dto.getPostedDate()) @@ -128,6 +129,7 @@ private DepartmentNotice convert(CommonNoticeFormatDto dto, DepartmentName depar .important(important) .categoryName(CategoryName.DEPARTMENT) .departmentName(departmentNameEnum) + .graduated(graduated) .build(); } } diff --git a/src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql b/src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql new file mode 100644 index 000000000..ff16ecfca --- /dev/null +++ b/src/main/resources/db/migration/V250905__Add_graduated_to_notice.sql @@ -0,0 +1,2 @@ +ALTER TABLE notice + ADD COLUMN graduated BOOLEAN NULL; \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java index 1de5d06c1..059256724 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java @@ -6,26 +6,11 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; +import java.util.List; + import static com.kustacks.kuring.acceptance.CategoryStep.지원하는_카테고리_조회_요청; -import static com.kustacks.kuring.acceptance.NoticeStep.공지_조회_요청; -import static com.kustacks.kuring.acceptance.NoticeStep.공지_조회_응답_확인; -import static com.kustacks.kuring.acceptance.NoticeStep.공지사항_댓글수_응답_확인; -import static com.kustacks.kuring.acceptance.NoticeStep.공지사항_조회_요청; -import static com.kustacks.kuring.acceptance.NoticeStep.공지사항_조회_요청_실패_응답_확인; -import static com.kustacks.kuring.acceptance.NoticeStep.공지사항_조회_요청_응답_확인; -import static com.kustacks.kuring.acceptance.NoticeStep.공지에_댓글_수정; -import static com.kustacks.kuring.acceptance.NoticeStep.공지에_댓글_추가; -import static com.kustacks.kuring.acceptance.NoticeStep.공지의_댓글_조회; -import static com.kustacks.kuring.acceptance.NoticeStep.댓글_대댓글_확인; -import static com.kustacks.kuring.acceptance.NoticeStep.댓글_삭제; -import static com.kustacks.kuring.acceptance.NoticeStep.댓글_확인; -import static com.kustacks.kuring.acceptance.NoticeStep.로그인_사용자_댓글_조회; -import static com.kustacks.kuring.acceptance.NoticeStep.페이지_번호와_함께_공지사항_조회_요청; -import static com.kustacks.kuring.acceptance.NoticeStep.페이지_번호와_함께_학교_공지사항_조회_요청; -import static com.kustacks.kuring.acceptance.NoticeStep.학교_공지_조회_응답_확인; -import static com.kustacks.kuring.acceptance.UserStep.로그인_요청; -import static com.kustacks.kuring.acceptance.UserStep.사용자_로그인_되어_있음; -import static com.kustacks.kuring.acceptance.UserStep.사용자_회원가입_요청; +import static com.kustacks.kuring.acceptance.NoticeStep.*; +import static com.kustacks.kuring.acceptance.UserStep.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -81,10 +66,13 @@ void look_up_support_university_category() { @Test void look_up_notice_v2() { // when - var 공지사항_조회_요청_응답 = 페이지_번호와_함께_학교_공지사항_조회_요청("stu", "", Boolean.FALSE, 0); + var 공지사항_조회_요청_응답 = 공지사항_조회_요청("stu"); // then 공지사항_조회_요청_응답_확인(공지사항_조회_요청_응답, "student"); + + List graduatedList = 공지사항_조회_요청_응답.jsonPath().getList("data.graduated", Boolean.class); + graduatedList.forEach(value -> assertThat(value).isNull()); } /** @@ -96,10 +84,10 @@ void look_up_notice_v2() { @Test void look_up_department_important_notice_v2() { // when - var 학과_공지_조회_응답 = 페이지_번호와_함께_학교_공지사항_조회_요청("dep", DepartmentName.COMPUTER.getHostPrefix(), Boolean.TRUE, 0); + var 학과_공지_조회_응답 = 페이지_번호와_함께_학과_공지사항_조회_요청("dep", DepartmentName.COMPUTER.getHostPrefix(), Boolean.TRUE, Boolean.FALSE, 0); // then - 학교_공지_조회_응답_확인(학과_공지_조회_응답, Boolean.TRUE); + 학과_공지_조회_응답_확인(학과_공지_조회_응답, Boolean.TRUE, Boolean.FALSE); } /** @@ -111,10 +99,10 @@ void look_up_department_important_notice_v2() { @Test void look_up_department_normal_notice_v2() { // when - var 학과_공지_조회_응답 = 페이지_번호와_함께_학교_공지사항_조회_요청("dep", DepartmentName.COMPUTER.getHostPrefix(), Boolean.FALSE, 0); + var 학과_공지_조회_응답 = 페이지_번호와_함께_학과_공지사항_조회_요청("dep", DepartmentName.COMPUTER.getHostPrefix(), Boolean.FALSE, Boolean.FALSE, 0); // then - 학교_공지_조회_응답_확인(학과_공지_조회_응답, Boolean.FALSE); + 학과_공지_조회_응답_확인(학과_공지_조회_응답, Boolean.FALSE, Boolean.FALSE); } /** diff --git a/src/test/java/com/kustacks/kuring/acceptance/NoticeStep.java b/src/test/java/com/kustacks/kuring/acceptance/NoticeStep.java index 980b9bf2c..b4914d8f2 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/NoticeStep.java +++ b/src/test/java/com/kustacks/kuring/acceptance/NoticeStep.java @@ -41,20 +41,21 @@ public class NoticeStep { .extract(); } - public static ExtractableResponse 페이지_번호와_함께_학교_공지사항_조회_요청(String category, String hostPrefix, Boolean important, int page) { + public static ExtractableResponse 페이지_번호와_함께_학과_공지사항_조회_요청(String category, String hostPrefix, Boolean important, Boolean graduated, int page) { 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}&page={page}&size={size}") + .when().get("/api/v2/notices?type={type}&department={department}&important={important}&graduated={graduated}&page={page}&size={size}") .then().log().all() .extract(); } - public static void 학교_공지_조회_응답_확인(ExtractableResponse response, Boolean important) { + public static void 학과_공지_조회_응답_확인(ExtractableResponse response, Boolean important, Boolean graduated) { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(response.jsonPath().getInt("code")).isEqualTo(200), @@ -64,7 +65,8 @@ public class NoticeStep { () -> assertThat(response.jsonPath().getString("data[0].url")).isNotBlank(), () -> assertThat(response.jsonPath().getString("data[0].subject")).isNotBlank(), () -> assertThat(response.jsonPath().getString("data[0].category")).isEqualTo("department"), - () -> assertThat(response.jsonPath().getBoolean("data[0].important")).isEqualTo(important) + () -> assertThat(response.jsonPath().getBoolean("data[0].important")).isEqualTo(important), + () -> assertThat(response.jsonPath().getBoolean("data[0].graduated")).isEqualTo(graduated) ); } diff --git a/src/test/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeRepositoryTest.java b/src/test/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeRepositoryTest.java index 3d879c8b2..71fac47da 100644 --- a/src/test/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeRepositoryTest.java +++ b/src/test/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeRepositoryTest.java @@ -64,9 +64,9 @@ void lookupAllNoticeByIds() { noticeCommandPort.saveAllCategoryNotices(List.of(notice1, notice2)); DepartmentNotice departmentNotice1 = new DepartmentNotice("3", "2024-01-22 17:27:05", "2023-04-03 17:27:05", - "departmentNotice1", CategoryName.DEPARTMENT, false, "https://www.example.com", DepartmentName.ADMINISTRATION); + "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)); User user = new User("user_token"); diff --git a/src/test/java/com/kustacks/kuring/notice/domain/DepartmentNoticeTest.java b/src/test/java/com/kustacks/kuring/notice/domain/DepartmentNoticeTest.java index c4a0a2a1a..2e4727c96 100644 --- a/src/test/java/com/kustacks/kuring/notice/domain/DepartmentNoticeTest.java +++ b/src/test/java/com/kustacks/kuring/notice/domain/DepartmentNoticeTest.java @@ -20,7 +20,7 @@ class DepartmentNoticeTest { "https://library.konkuk.ac.kr/library-guide/bulletins/notice/7192", "http://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?forum=notice&sort=6&id=5b50736&cat=0000300001", "http://mae.konkuk.ac.kr/noticeView.do?siteId=MAE&boardSeq=988&menuSeq=6823&categorySeq=0&curBoardDispType=LIST&curPage=12&pageNum=1&seq=179896"}) void create_member(String url) { - assertThatCode(() -> new DepartmentNotice("artice_id", "2023-04-03 00:00:12", "2023-04-03 00:01:12", "subject", CategoryName.DEPARTMENT, false, url, DepartmentName.BIOLOGICAL)) + assertThatCode(() -> new DepartmentNotice("artice_id", "2023-04-03 00:00:12", "2023-04-03 00:01:12", "subject", CategoryName.DEPARTMENT, false, url, DepartmentName.BIOLOGICAL, false)) .doesNotThrowAnyException(); } @@ -28,7 +28,7 @@ void create_member(String url) { @ParameterizedTest @ValueSource(strings = {"//www.example.com", "https:/www.example.com", "https://"}) void member_invalid_email_id(String url) { - assertThatThrownBy(() -> new DepartmentNotice("artice_id", "2023-04-03 00:00:12", "2023-04-03 00:01:12", "subject", CategoryName.DEPARTMENT, false, url, DepartmentName.BIOLOGICAL)) + assertThatThrownBy(() -> new DepartmentNotice("artice_id", "2023-04-03 00:00:12", "2023-04-03 00:01:12", "subject", CategoryName.DEPARTMENT, false, url, DepartmentName.BIOLOGICAL, false)) .isInstanceOf(InternalLogicException.class); } @@ -41,7 +41,7 @@ void notice_equals_test() { } private DepartmentNotice createDepartmentNotice(long id, String url) { - DepartmentNotice departmentNotice = new DepartmentNotice("artice_id", "2023-04-03 00:00:12", "2023-04-03 00:01:12", "subject", CategoryName.DEPARTMENT, false, url, DepartmentName.BIOLOGICAL); + DepartmentNotice departmentNotice = new DepartmentNotice("artice_id", "2023-04-03 00:00:12", "2023-04-03 00:01:12", "subject", CategoryName.DEPARTMENT, false, url, DepartmentName.BIOLOGICAL, false); ReflectionTestUtils.setField(departmentNotice, "id", id); return departmentNotice; } diff --git a/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java b/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java index cde0e375d..a8384d220 100644 --- a/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java +++ b/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java @@ -131,7 +131,7 @@ public void loadData() { private void setCharsetAllTable() { for (String tableName : tableNames) { - 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"); } } @@ -200,14 +200,29 @@ private void initFeedback() { } private void initNotice() { + // 카테고리 공지 List noticeList = buildNotices(5, CategoryName.STUDENT); noticePersistenceAdapter.saveAllCategoryNotices(noticeList); - List importantDeptNotices = buildImportantDepartmentNotice(7, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, true); - noticePersistenceAdapter.saveAllDepartmentNotices(importantDeptNotices); - - List normalDeptNotices = buildNormalDepartmentNotice(5, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, false); - noticePersistenceAdapter.saveAllDepartmentNotices(normalDeptNotices); + // 대학원 중요 공지 + List importantGradDeptNotices = + buildImportantDepartmentNotice(7, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, true, true); + noticePersistenceAdapter.saveAllDepartmentNotices(importantGradDeptNotices); + + // 학부 중요 공지 + List importantUnderDeptNotices = + buildImportantDepartmentNotice(7, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, true, false); + noticePersistenceAdapter.saveAllDepartmentNotices(importantUnderDeptNotices); + + // 대학원 일반 공지 + List normalGradDeptNotices = + buildNormalDepartmentNotice(5, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, false, true); + noticePersistenceAdapter.saveAllDepartmentNotices(normalGradDeptNotices); + + // 학부 일반 공지 + List normalUnderDeptNotices = + buildNormalDepartmentNotice(5, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, false, false); + noticePersistenceAdapter.saveAllDepartmentNotices(normalUnderDeptNotices); } private void initStaff() { @@ -236,24 +251,24 @@ private void initWhitelistWords() { } - private List buildImportantDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important) { + private List 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 buildNormalDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important) { + private List 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 static List buildNotices(int cnt, CategoryName categoryName) { return Stream.iterate(0, i -> i + 1) .limit(cnt) - .map(i -> new Notice("article_" + i, "2023-04-03 00:00:1"+i, "2023-04-03 00:00:1"+i, "subject_" + i, categoryName, false, "https://www.example.com")) + .map(i -> new Notice("article_" + i, "2023-04-03 00:00:1" + i, "2023-04-03 00:00:1" + i, "subject_" + i, categoryName, false, "https://www.example.com")) .toList(); } diff --git a/src/test/java/com/kustacks/kuring/worker/scrap/KuisHomepageNoticeScraperTemplateTest.java b/src/test/java/com/kustacks/kuring/worker/scrap/KuisHomepageNoticeScraperTemplateTest.java index 0dcabbb56..c2dded128 100644 --- a/src/test/java/com/kustacks/kuring/worker/scrap/KuisHomepageNoticeScraperTemplateTest.java +++ b/src/test/java/com/kustacks/kuring/worker/scrap/KuisHomepageNoticeScraperTemplateTest.java @@ -69,7 +69,7 @@ void scrapForEmbedding() throws IOException { ); NoticeDto noticeDto = new NoticeDto(1L, "1", "2024-01-01 00:00:00", - "http://example.com", "제목", "category", true, 2L); + "http://example.com", "제목", "category", true, false, 2L); when(normalJsoupClient.get(anyString(), anyInt())).thenReturn(doc); diff --git a/src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java b/src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java new file mode 100644 index 000000000..2cd4f0a3d --- /dev/null +++ b/src/test/java/com/kustacks/kuring/worker/scrap/graduatedeptinfo/GraduateDeptInfoTest.java @@ -0,0 +1,90 @@ +package com.kustacks.kuring.worker.scrap.graduatedeptinfo; + +import com.kustacks.kuring.notice.domain.DepartmentName; +import com.kustacks.kuring.support.IntegrationTestSupport; +import com.kustacks.kuring.support.TestFileLoader; +import com.kustacks.kuring.worker.dto.ComplexNoticeFormatDto; +import com.kustacks.kuring.worker.scrap.DepartmentNoticeScraperTemplate; +import com.kustacks.kuring.worker.scrap.client.NormalJsoupClient; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; +import com.kustacks.kuring.worker.scrap.deptinfo.engineering.ComputerScienceDept; +import com.kustacks.kuring.worker.update.notice.dto.response.CommonNoticeFormatDto; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +class GraduateDeptInfoTest extends IntegrationTestSupport { + + @MockBean + private NormalJsoupClient normalJsoupClient; + + @Autowired + private DepartmentNoticeScraperTemplate scraperTemplate; + + @Autowired + private ComputerScienceDept computerScienceDept; + + @Autowired + private LatestPageNoticeApiClient latestPageNoticeApiClient; + @Autowired + private LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient; + + @Test + @DisplayName("컴퓨터공학부 대학원 공지를 스크래핑한다.") + void computer_scrapLatestPage() throws IOException { + // given + String html = TestFileLoader.loadHtmlFile("src/test/resources/notice/graduate-cse-notice.html"); + Document doc = Jsoup.parse(html); + + when(normalJsoupClient.get(anyString(), anyInt())).thenReturn(doc); + + // when + List results = + scraperTemplate.scrap(computerScienceDept, latestPageGraduateNoticeApiClient::request); + + // then + assertThat(results).isNotEmpty(); + assertThat(computerScienceDept.getDepartmentName()).isEqualTo(DepartmentName.COMPUTER); + + ComplexNoticeFormatDto dto = results.get(0); + + List importantNotices = dto.getImportantNoticeList(); + List normalNotices = dto.getNormalNoticeList(); + + + assertAll( + // important 검증 + () -> assertThat(importantNotices).hasSize(1), + () -> assertThat(importantNotices.get(0).getArticleId()).isEqualTo("1139134"), + () -> assertThat(importantNotices.get(0).getSubject()) + .isEqualTo("[내규] 일반대학원 컴퓨터공학과 내규"), + () -> assertThat(importantNotices.get(0).getPostedDate()).isEqualTo("2024.11.14"), + () -> assertThat(importantNotices.get(0).getFullUrl()) + .isEqualTo("https://cse.konkuk.ac.kr/bbs/cse/411/1139134/artclView.do"), + () -> assertThat(importantNotices.get(0).getImportant()).isTrue(), + + // normal 검증 + () -> assertThat(normalNotices).hasSize(10), + () -> assertThat(normalNotices.get(0).getArticleId()).isEqualTo("1154434"), + () -> assertThat(normalNotices.get(0).getSubject()) + .isEqualTo("2025.2학기 대학원 연구등록금 납부일정 안내"), + () -> assertThat(normalNotices.get(0).getPostedDate()).isEqualTo("2025.07.25"), + () -> assertThat(normalNotices.get(0).getFullUrl()) + .isEqualTo("https://cse.konkuk.ac.kr/bbs/cse/411/1154434/artclView.do"), + () -> assertThat(normalNotices.get(0).getImportant()).isFalse() + ); + } +} diff --git a/src/test/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupportTest.java b/src/test/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupportTest.java index 3398b980c..66e2d9881 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupportTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/notice/NoticeUpdateSupportTest.java @@ -72,7 +72,7 @@ void filteringSoonSaveDepartmentNotices() { // when List results = noticeUpdateSupport - .filteringSoonSaveDepartmentNotices(originNotices, List.of(1, 2, 4), DepartmentName.ECONOMICS, false); + .filteringSoonSaveDepartmentNotices(originNotices, List.of(1, 2, 4), DepartmentName.ECONOMICS, false, false); // then assertThat(results).hasSize(2) diff --git a/src/test/resources/notice/graduate-cse-notice.html b/src/test/resources/notice/graduate-cse-notice.html new file mode 100644 index 000000000..ac6733c23 --- /dev/null +++ b/src/test/resources/notice/graduate-cse-notice.html @@ -0,0 +1,4108 @@ + + + 공지사항 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
본문 바로가기
+
주메뉴 바로가기
+
+ + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + +
+ +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + 건국대학교 + 검색 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + SITEMAP +
+
+ + +
+ +
+
+ +

대학원

+ +
+
+ + +
+
+ +
+
    + +
  • + + +
  • + +
    + 즐겨찾는 메뉴 + +
    + + +
    +
    +
  • + + +
  • + +
    + +
      +
    • +
    • +
    • +
    • +
    • +
    +
    +
  • + +
+
+ + + + +
+
+ +
+ + +
+
+
+ +
+

공지사항

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + +
+ /WEB-INF/jsp/k2web/com/cop/site/layout.jsp
+ cse_JW_MS_K2WT001_S +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From ed1721df11d599f9ffee4981ced47c81caed5856 Mon Sep 17 00:00:00 2001 From: Jiwoo Kim Date: Sat, 13 Sep 2025 12:12:56 +0900 Subject: [PATCH 02/16] Feat/cors setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(AuthConfig): CORS 설정 추가 * feat(AuthConfig): CORS 설정 추가 * feat(Report): reporterId로 필드명 변경 * feat(AdminQueryService): 조회시 Page구현체 사용하도록 변경 --- .../admin/adapter/in/web/AdminQueryApiV2.java | 32 +++++----- .../in/web/dto/AdminAlertListResponse.java | 24 +++++++ .../in/web/dto/AdminFeedbackListResponse.java | 25 ++++++++ .../in/web/dto/AdminReportListResponse.java | 25 ++++++++ .../port/in/AdminQueryUseCase.java | 9 +-- .../port/out/AdminUserFeedbackPort.java | 4 +- .../service/AdminQueryService.java | 62 +++++++++---------- .../com/kustacks/kuring/auth/AuthConfig.java | 39 +++++++++++- .../kuring/config/FeatureFlagConfig.java | 12 ++-- .../adapter/in/web/ReportCommandApiV2.java | 4 +- .../port/in/dto/AdminReportsResult.java | 8 +-- .../kustacks/kuring/report/domain/Report.java | 14 +---- .../persistence/UserPersistenceAdapter.java | 3 +- .../out/persistence/UserQueryRepository.java | 3 +- .../persistence/UserQueryRepositoryImpl.java | 15 +++-- .../acceptance/AdminAcceptanceTest.java | 14 +---- .../kustacks/kuring/acceptance/AdminStep.java | 4 +- .../out/persistence/UserRepositoryTest.java | 6 +- 18 files changed, 204 insertions(+), 99 deletions(-) create mode 100644 src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminAlertListResponse.java create mode 100644 src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminFeedbackListResponse.java create mode 100644 src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminReportListResponse.java diff --git a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminQueryApiV2.java b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminQueryApiV2.java index f345aea44..b1e5d188d 100644 --- a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminQueryApiV2.java +++ b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminQueryApiV2.java @@ -1,14 +1,14 @@ package com.kustacks.kuring.admin.adapter.in.web; -import com.kustacks.kuring.admin.adapter.in.web.dto.AdminAlertResponse; +import com.kustacks.kuring.admin.adapter.in.web.dto.AdminAlertListResponse; +import com.kustacks.kuring.admin.adapter.in.web.dto.AdminFeedbackListResponse; +import com.kustacks.kuring.admin.adapter.in.web.dto.AdminReportListResponse; import com.kustacks.kuring.admin.application.port.in.AdminQueryUseCase; import com.kustacks.kuring.admin.domain.AdminRole; import com.kustacks.kuring.auth.authorization.AuthenticationPrincipal; import com.kustacks.kuring.auth.context.Authentication; import com.kustacks.kuring.auth.secured.Secured; import com.kustacks.kuring.common.dto.BaseResponse; -import com.kustacks.kuring.report.application.port.in.dto.AdminReportsResult; -import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -27,10 +27,7 @@ import java.util.List; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.ALERT_SEARCH_SUCCESS; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.AUTH_AUTHENTICATION_SUCCESS; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.FEEDBACK_SEARCH_SUCCESS; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.REPORT_SEARCH_SUCCESS; +import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.*; @Tag(name = "Admin-Query", description = "관리자가 주체가 되는 정보 조회") @Validated @@ -45,36 +42,39 @@ public class AdminQueryApiV2 { @SecurityRequirement(name = "JWT") @Secured(AdminRole.ROLE_ROOT) @GetMapping("/feedbacks") - public ResponseEntity>> getFeedbacks( + public ResponseEntity> getFeedbacks( @Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page, @Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size ) { - List feedbacks = adminQueryUseCase.lookupFeedbacks(page, size); - return ResponseEntity.ok().body(new BaseResponse<>(FEEDBACK_SEARCH_SUCCESS, feedbacks)); + var pageResult = adminQueryUseCase.lookupFeedbacks(page, size); + AdminFeedbackListResponse response = AdminFeedbackListResponse.from(pageResult); + return ResponseEntity.ok().body(new BaseResponse<>(FEEDBACK_SEARCH_SUCCESS, response)); } @Operation(summary = "예약 알림 조회", description = "어드민이 등록한 모든 예약 알림을 조회한다") @SecurityRequirement(name = "JWT") @Secured(AdminRole.ROLE_ROOT) @GetMapping("/alerts") - public ResponseEntity>> getAlerts( + public ResponseEntity> getAlerts( @Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page, @Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size ) { - List alerts = adminQueryUseCase.lookupAlerts(page, size); - return ResponseEntity.ok().body(new BaseResponse<>(ALERT_SEARCH_SUCCESS, alerts)); + var pageResult = adminQueryUseCase.lookupAlerts(page, size); + AdminAlertListResponse response = AdminAlertListResponse.from(pageResult); + return ResponseEntity.ok().body(new BaseResponse<>(ALERT_SEARCH_SUCCESS, response)); } @Operation(summary = "신고 목록 조회", description = "사용자의 모든 신고 목록을 조회합니다") @SecurityRequirement(name = "JWT") @Secured(AdminRole.ROLE_ROOT) @GetMapping("/reports") - public ResponseEntity>> getReports( + public ResponseEntity> getReports( @Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page, @Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size ) { - List result = adminQueryUseCase.lookupReports(page, size); - return ResponseEntity.ok().body(new BaseResponse<>(REPORT_SEARCH_SUCCESS, result)); + var pageResult = adminQueryUseCase.lookupReports(page, size); + AdminReportListResponse response = AdminReportListResponse.from(pageResult); + return ResponseEntity.ok().body(new BaseResponse<>(REPORT_SEARCH_SUCCESS, response)); } /** diff --git a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminAlertListResponse.java b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminAlertListResponse.java new file mode 100644 index 000000000..e8f211d85 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminAlertListResponse.java @@ -0,0 +1,24 @@ +package com.kustacks.kuring.admin.adapter.in.web.dto; + +import org.springframework.data.domain.Page; + +import java.util.List; + +public record AdminAlertListResponse( + List alerts, + boolean hasNext, + long totalElements, + int totalPages +) { + + public static AdminAlertListResponse from(Page page) { + return new AdminAlertListResponse( + page.getContent(), + page.hasNext(), + page.getTotalElements(), + page.getTotalPages() + ); + } +} + + diff --git a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminFeedbackListResponse.java b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminFeedbackListResponse.java new file mode 100644 index 000000000..aa9990394 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminFeedbackListResponse.java @@ -0,0 +1,25 @@ +package com.kustacks.kuring.admin.adapter.in.web.dto; + +import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record AdminFeedbackListResponse( + List feedbacks, + boolean hasNext, + long totalElements, + int totalPages +) { + + public static AdminFeedbackListResponse from(Page page) { + return new AdminFeedbackListResponse( + page.getContent(), + page.hasNext(), + page.getTotalElements(), + page.getTotalPages() + ); + } +} + + diff --git a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminReportListResponse.java b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminReportListResponse.java new file mode 100644 index 000000000..d3640e380 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminReportListResponse.java @@ -0,0 +1,25 @@ +package com.kustacks.kuring.admin.adapter.in.web.dto; + +import com.kustacks.kuring.report.application.port.in.dto.AdminReportsResult; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record AdminReportListResponse( + List reports, + boolean hasNext, + long totalElements, + int totalPages +) { + + public static AdminReportListResponse from(Page page) { + return new AdminReportListResponse( + page.getContent(), + page.hasNext(), + page.getTotalElements(), + page.getTotalPages() + ); + } +} + + diff --git a/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminQueryUseCase.java b/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminQueryUseCase.java index c24754cad..fcfa7b325 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminQueryUseCase.java +++ b/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminQueryUseCase.java @@ -3,13 +3,14 @@ import com.kustacks.kuring.admin.adapter.in.web.dto.AdminAlertResponse; import com.kustacks.kuring.report.application.port.in.dto.AdminReportsResult; import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult; +import org.springframework.data.domain.Page; -import java.util.List; public interface AdminQueryUseCase { - List lookupFeedbacks(int page, int size); - List lookupAlerts(int page, int size); + Page lookupFeedbacks(int page, int size); - List lookupReports(int page, int size); + Page lookupAlerts(int page, int size); + + Page lookupReports(int page, int size); } diff --git a/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java b/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java index a8da3ef33..5d58cde44 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java +++ b/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java @@ -1,11 +1,13 @@ package com.kustacks.kuring.admin.application.port.out; import com.kustacks.kuring.user.application.port.out.dto.FeedbackDto; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; public interface AdminUserFeedbackPort { List findAllToken(); - List findAllFeedbackByPageRequest(Pageable pageable); + + Page findAllFeedbackByPageRequest(Pageable pageable); } diff --git a/src/main/java/com/kustacks/kuring/admin/application/service/AdminQueryService.java b/src/main/java/com/kustacks/kuring/admin/application/service/AdminQueryService.java index a5e61c9c9..0a5047a22 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/service/AdminQueryService.java +++ b/src/main/java/com/kustacks/kuring/admin/application/service/AdminQueryService.java @@ -10,11 +10,11 @@ import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; -import java.util.List; @Slf4j @UseCase @@ -27,47 +27,41 @@ public class AdminQueryService implements AdminQueryUseCase { @Transactional(readOnly = true) @Override - public List lookupFeedbacks(int page, int size) { + public Page lookupFeedbacks(int page, int size) { PageRequest pageRequest = PageRequest.of(page, size); - return adminUserFeedbackPort.findAllFeedbackByPageRequest(pageRequest) - .stream() - .map(dto -> new AdminFeedbacksResult( - dto.getContents(), - dto.getUserId(), - dto.getCreatedAt() - )) - .toList(); + var pageResult = adminUserFeedbackPort.findAllFeedbackByPageRequest(pageRequest); + return pageResult.map(dto -> new AdminFeedbacksResult( + dto.getContents(), + dto.getUserId(), + dto.getCreatedAt() + )); } @Override - public List lookupAlerts(int page, int size) { + public Page lookupAlerts(int page, int size) { PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Order.desc("createdAt"))); - return adminAlertQueryPort.findAllAlertByPageRequest(pageRequest) - .stream() - .map(alert -> AdminAlertResponse.of( - alert.getId(), - alert.getTitle(), - alert.getContent(), - alert.getStatus(), - alert.getAlertTime() - )) - .toList(); + var pageResult = adminAlertQueryPort.findAllAlertByPageRequest(pageRequest); + return pageResult.map(alert -> AdminAlertResponse.of( + alert.getId(), + alert.getTitle(), + alert.getContent(), + alert.getStatus(), + alert.getAlertTime() + )); } @Override - public List lookupReports(int page, int size) { + public Page lookupReports(int page, int size) { PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Order.desc("createdAt"))); - return adminUserReportPort.findAllReportByPageRequest(pageRequest) - .stream() - .map(report -> AdminReportsResult.of( - report.getId(), - report.getTargetId(), - report.getUserId(), - report.getTargetType(), - report.getContent(), - report.getCreatedAt(), - report.getUpdatedAt() - )) - .toList(); + var pageResult = adminUserReportPort.findAllReportByPageRequest(pageRequest); + return pageResult.map(report -> AdminReportsResult.of( + report.getId(), + report.getTargetId(), + report.getReporterId(), + report.getTargetType(), + report.getContent(), + report.getCreatedAt(), + report.getUpdatedAt() + )); } } diff --git a/src/main/java/com/kustacks/kuring/auth/AuthConfig.java b/src/main/java/com/kustacks/kuring/auth/AuthConfig.java index 9b4b2a22e..a284c7af8 100644 --- a/src/main/java/com/kustacks/kuring/auth/AuthConfig.java +++ b/src/main/java/com/kustacks/kuring/auth/AuthConfig.java @@ -10,14 +10,20 @@ import com.kustacks.kuring.auth.interceptor.FirebaseTokenAuthenticationFilter; import com.kustacks.kuring.auth.interceptor.UserRegisterNonChainingFilter; import com.kustacks.kuring.auth.token.JwtTokenProvider; -import com.kustacks.kuring.message.application.port.in.FirebaseWithUserUseCase; import com.kustacks.kuring.common.properties.ServerProperties; +import com.kustacks.kuring.message.application.port.in.FirebaseWithUserUseCase; import com.kustacks.kuring.user.adapter.out.persistence.UserPersistenceAdapter; import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -34,6 +40,34 @@ public class AuthConfig implements WebMvcConfigurer { private final FirebaseWithUserUseCase firebaseService; private final UserPersistenceAdapter userPersistenceAdapter; + @Bean + public FilterRegistrationBean corsFilterRegistration() { + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowedOriginPatterns(List.of( + "https://www.ku-ring.com", + "https://ku-ring.com", + "http://localhost:[*]", + "http://127.0.0.1:[*]" + )); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); // 모든 요청 헤더 허용 + + // [보안 권장] 모든 응답 헤더를 노출하는 대신 필요한 헤더만 명시적으로 노출합니다. + // 예를 들어, 프론트에서 Authorization 헤더에 담긴 토큰을 읽어야 할 경우 아래와 같이 설정합니다. + config.setExposedHeaders(List.of("Authorization", "Location")); + + config.setAllowCredentials(true); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + FilterRegistrationBean registration = new FilterRegistrationBean<>(new CorsFilter(source)); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); // [수정] 필터 순서를 가장 높게 설정하여 다른 필터보다 먼저 실행되도록 합니다. + return registration; + } + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SecurityContextPersistenceFilter()); @@ -56,7 +90,7 @@ adminDetailsService, passwordEncoder(), objectMapper, } @Override - public void addArgumentResolvers(List argumentResolvers) { + public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); } @@ -84,4 +118,5 @@ AuthenticationSuccessHandler userRegisterSuccessHandler() { AuthenticationFailureHandler userRegisterFailureHandler() { return new UserRegisterFailureHandler(objectMapper); } + } diff --git a/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java b/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java index cf515dcd8..9ea3b844c 100644 --- a/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java +++ b/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java @@ -1,8 +1,7 @@ package com.kustacks.kuring.config; -import com.kustacks.kuring.common.env.RemotePropertyResolver; import com.kustacks.kuring.common.featureflag.FeatureFlagProperties; -import com.kustacks.kuring.common.featureflag.RemoteFeatureFlags; +import com.kustacks.kuring.common.featureflag.FeatureFlags; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; @@ -32,9 +31,14 @@ public AppConfigDataClient appConfigDataClient() { .region(Region.of(properties.region())) .build(); } +// +// @Bean +// public RemoteFeatureFlags remoteFeatureFlags(RemotePropertyResolver remotePropertyResolver) { +// return new RemoteFeatureFlags(remotePropertyResolver); +// } @Bean - public RemoteFeatureFlags remoteFeatureFlags(RemotePropertyResolver remotePropertyResolver) { - return new RemoteFeatureFlags(remotePropertyResolver); + public FeatureFlags featureFlags() { + return new FeatureFlags.AlwaysDisabledFeatureFlags(); } } diff --git a/src/main/java/com/kustacks/kuring/report/adapter/in/web/ReportCommandApiV2.java b/src/main/java/com/kustacks/kuring/report/adapter/in/web/ReportCommandApiV2.java index 4c038f956..8b00bfdb4 100644 --- a/src/main/java/com/kustacks/kuring/report/adapter/in/web/ReportCommandApiV2.java +++ b/src/main/java/com/kustacks/kuring/report/adapter/in/web/ReportCommandApiV2.java @@ -37,6 +37,7 @@ public ResponseEntity report( @RequestBody ReportRequest request ) { ReportTargetType targetType = ReportTargetType.fromString(request.reportType()); + if (targetType.match(COMMENT)) { var command = new ReportCommentCommand( request.targetId(), @@ -48,7 +49,8 @@ public ResponseEntity report( return ResponseEntity.status(REPORT_COMMENT_SUCCESS.getCode()) .body(new BaseResponse<>(REPORT_COMMENT_SUCCESS, null)); } + return ResponseEntity.status(REPORT_INVALID_TARGET_TYPE.getHttpStatus()) .body(new ErrorResponse(REPORT_INVALID_TARGET_TYPE)); } -} \ No newline at end of file +} diff --git a/src/main/java/com/kustacks/kuring/report/application/port/in/dto/AdminReportsResult.java b/src/main/java/com/kustacks/kuring/report/application/port/in/dto/AdminReportsResult.java index effdb9561..cd936564c 100644 --- a/src/main/java/com/kustacks/kuring/report/application/port/in/dto/AdminReportsResult.java +++ b/src/main/java/com/kustacks/kuring/report/application/port/in/dto/AdminReportsResult.java @@ -5,7 +5,7 @@ public record AdminReportsResult( Long id, Long targetId, - Long userId, + Long reporterId, String type, String content, LocalDateTime createdAt, @@ -14,12 +14,12 @@ public record AdminReportsResult( public static AdminReportsResult of( Long id, Long targetId, - Long userId, + Long reporterId, String type, String content, LocalDateTime createdAt, LocalDateTime updatedAt ) { - return new AdminReportsResult(id, targetId, userId, type, content, createdAt, updatedAt); + return new AdminReportsResult(id, targetId, reporterId, type, content, createdAt, updatedAt); } -} \ No newline at end of file +} diff --git a/src/main/java/com/kustacks/kuring/report/domain/Report.java b/src/main/java/com/kustacks/kuring/report/domain/Report.java index 349cc47bc..a4e31485e 100644 --- a/src/main/java/com/kustacks/kuring/report/domain/Report.java +++ b/src/main/java/com/kustacks/kuring/report/domain/Report.java @@ -3,17 +3,7 @@ import com.kustacks.kuring.common.domain.BaseTimeEntity; import com.kustacks.kuring.common.domain.Content; import com.kustacks.kuring.user.domain.User; -import jakarta.persistence.Column; -import jakarta.persistence.Embedded; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -54,7 +44,7 @@ public String getContent() { return content.getValue(); } - public Long getUserId() { + public Long getReporterId() { return reporter.getId(); } diff --git a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java index a8674cede..9b465e06d 100644 --- a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java +++ b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java @@ -10,6 +10,7 @@ import com.kustacks.kuring.user.domain.RootUser; import com.kustacks.kuring.user.domain.User; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; @@ -25,7 +26,7 @@ public class UserPersistenceAdapter implements UserCommandPort, UserQueryPort, A private final RootUserRepository rootUserRepository; @Override - public List findAllFeedbackByPageRequest(Pageable pageable) { + public Page findAllFeedbackByPageRequest(Pageable pageable) { return userRepository.findAllFeedbackByPageRequest(pageable); } diff --git a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java index 4c0324e9e..4dfab8491 100644 --- a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java +++ b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java @@ -2,13 +2,14 @@ import com.kustacks.kuring.user.application.port.out.dto.FeedbackDto; import com.kustacks.kuring.user.domain.User; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; interface UserQueryRepository { - List findAllFeedbackByPageRequest(Pageable pageable); + Page findAllFeedbackByPageRequest(Pageable pageable); List findByPageRequest(Pageable pageable); diff --git a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java index be4cdadac..cf1fa0ad7 100644 --- a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java +++ b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java @@ -5,7 +5,9 @@ import com.kustacks.kuring.user.domain.User; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -20,15 +22,20 @@ class UserQueryRepositoryImpl implements UserQueryRepository { private final JPAQueryFactory queryFactory; @Override - public List findAllFeedbackByPageRequest(Pageable pageable) { - return queryFactory.select(new QFeedbackDto(feedback.content, feedback.user.id, feedback.createdAt)) + public Page 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()) - .fetch(); + .limit(pageable.getPageSize()); + + var countQuery = queryFactory.select(feedback.count()) + .from(feedback); + + return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchOne); } + @Override public List findByPageRequest(Pageable pageable) { return queryFactory diff --git a/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java index 79328b3f3..3a7ab93a7 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java @@ -18,15 +18,7 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import static com.kustacks.kuring.acceptance.AdminStep.금칙어_로드_요청; -import static com.kustacks.kuring.acceptance.AdminStep.사용자_피드백_조회_요청; -import static com.kustacks.kuring.acceptance.AdminStep.신고_목록_조회_요청; -import static com.kustacks.kuring.acceptance.AdminStep.신고_목록_조회_확인; -import static com.kustacks.kuring.acceptance.AdminStep.알림_예약; -import static com.kustacks.kuring.acceptance.AdminStep.예약_알림_삭제; -import static com.kustacks.kuring.acceptance.AdminStep.예약_알림_조회; -import static com.kustacks.kuring.acceptance.AdminStep.피드백_조회_확인; -import static com.kustacks.kuring.acceptance.AdminStep.허용_단어_로드_요청; +import static com.kustacks.kuring.acceptance.AdminStep.*; import static com.kustacks.kuring.acceptance.AuthStep.로그인_되어_있음; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -206,7 +198,7 @@ void delete_alert_test() { // given String accessToken = 로그인_되어_있음(ADMIN_LOGIN_ID, ADMIN_PASSWORD); 알림_예약(accessToken, alertCreateCommand); - int alertId = 예약_알림_조회(accessToken).jsonPath().getInt("data[0].id"); + int alertId = 예약_알림_조회(accessToken).jsonPath().getInt("data.alerts[0].id"); // when var 예약_알림_삭제_응답 = 예약_알림_삭제(accessToken, alertId); @@ -218,7 +210,7 @@ void delete_alert_test() { () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(response.jsonPath().getInt("code")).isEqualTo(200), () -> assertThat(response.jsonPath().getString("message")).isEqualTo("예약 알림 조회에 성공하였습니다"), - () -> assertThat(response.jsonPath().getString("data[0].status")).isEqualTo("CANCELED") + () -> assertThat(response.jsonPath().getString("data.alerts[0].status")).isEqualTo("CANCELED") ); } diff --git a/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java b/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java index 377362669..013515f83 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java +++ b/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java @@ -17,7 +17,7 @@ public class AdminStep { () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(response.jsonPath().getInt("code")).isEqualTo(200), () -> assertThat(response.jsonPath().getString("message")).isEqualTo("피드백 조회에 성공하였습니다"), - () -> assertThat(response.jsonPath().getList("data")).hasSize(5) + () -> assertThat(response.jsonPath().getList("data.feedbacks")).hasSize(5) ); } @@ -26,7 +26,7 @@ public class AdminStep { () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(response.jsonPath().getInt("code")).isEqualTo(200), () -> assertThat(response.jsonPath().getString("message")).isEqualTo("신고 목록 조회에 성공하였습니다"), - () -> assertThat(response.jsonPath().getList("data")).hasSize(3) + () -> assertThat(response.jsonPath().getList("data.reports")).hasSize(3) ); } diff --git a/src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java b/src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java index cdb1cfbc0..7cceabdb5 100644 --- a/src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java +++ b/src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import java.util.List; @@ -39,7 +40,8 @@ void findAllFeedbackByPageRequest() { Long userId = savedUser.getId(); // when - List feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(PageRequest.of(0, 3)); + List feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(PageRequest.of(0, 3)) + .stream().toList(); // then assertThat(feedbackDtos).hasSize(3) @@ -61,7 +63,7 @@ void deleteALl() { userPersistenceAdapter.deleteAll(List.of(savedUser)); // then - List feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(PageRequest.of(0, 10)); + Page feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(PageRequest.of(0, 10)); assertThat(feedbackDtos).hasSize(5); } From 054e092aaaefe5ba56a104a5420c44b329ef2b4e Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Sat, 20 Sep 2025 15:36:46 +0900 Subject: [PATCH 03/16] =?UTF-8?q?Feat:=20=EC=9C=A0=EC=A0=80=20=ED=95=99?= =?UTF-8?q?=EC=82=AC=EC=9D=BC=EC=A0=95=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=B3=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 학사일정 알림 ON/OFF 컬럼 추가 Flyway 쿼리문 작성 * feat: 유저 엔티티에 학사일정 알림 ON/OFF 필드 추가 * feat: 학사일정 알림 FeatureFlag 추가 * fix: 불피요 Getter 삭제 * fix: 학사일정 알림 ON/OFF 컬럼 추가 시 불필요 쿼리문 제거 --- .../com/kustacks/kuring/common/featureflag/KuringFeatures.java | 3 ++- src/main/java/com/kustacks/kuring/user/domain/User.java | 3 +++ .../V250918__Add_academic_event_notification_to_user.sql | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V250918__Add_academic_event_notification_to_user.sql diff --git a/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java b/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java index 16d8219b0..2ff88df3f 100644 --- a/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java +++ b/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java @@ -8,7 +8,8 @@ public enum KuringFeatures { UPDATE_KUIS_NOTICE(new Feature("update_kuis_notice")), UPDATE_USER(new Feature("update_user")), UPDATE_STAFF(new Feature("update_staff")), - UPDATE_ACADEMIC_EVENT(new Feature("update_academic_event")); + UPDATE_ACADEMIC_EVENT(new Feature("update_academic_event")), + NOTIFY_ACADEMIC_EVENT(new Feature("notify_academic_event")); private final Feature feature; diff --git a/src/main/java/com/kustacks/kuring/user/domain/User.java b/src/main/java/com/kustacks/kuring/user/domain/User.java index a65235636..3e0cead79 100644 --- a/src/main/java/com/kustacks/kuring/user/domain/User.java +++ b/src/main/java/com/kustacks/kuring/user/domain/User.java @@ -61,6 +61,9 @@ public class User implements Serializable { @Column(nullable = true) private Long loginUserId; + @Column(name = "academic_event_notification_enabled", nullable = false) + private Boolean academicEventNotificationEnabled = Boolean.TRUE; + //Fcm Token User public User(String token) { this.fcmToken = token; diff --git a/src/main/resources/db/migration/V250918__Add_academic_event_notification_to_user.sql b/src/main/resources/db/migration/V250918__Add_academic_event_notification_to_user.sql new file mode 100644 index 000000000..368944d88 --- /dev/null +++ b/src/main/resources/db/migration/V250918__Add_academic_event_notification_to_user.sql @@ -0,0 +1,3 @@ +-- 사용자 테이블에 학사일정 알림 설정 컬럼 추가 +ALTER TABLE user + ADD COLUMN academic_event_notification_enabled BOOLEAN NOT NULL DEFAULT TRUE; \ No newline at end of file From 675e8a1ffbf0e44f56980646c102b4ea7b95441b Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:54:27 +0900 Subject: [PATCH 04/16] =?UTF-8?q?Feat:=20=ED=95=99=EC=82=AC=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 임포트 수정 --- .../common/dto/ResponseCodeAndMessages.java | 1 + .../kuring/config/FeatureFlagConfig.java | 12 +++------ .../user/adapter/in/web/UserCommandApiV2.java | 16 +++++++++++- .../UserAcademicEventNotificationRequest.java | 8 ++++++ .../port/in/UserCommandUseCase.java | 3 +++ .../UserAcademicEventNotificationCommand.java | 7 ++++++ .../service/UserCommandService.java | 7 ++++++ .../com/kustacks/kuring/user/domain/User.java | 5 ++++ .../kuring/acceptance/UserAcceptanceTest.java | 21 ++++++++++++++++ .../kustacks/kuring/acceptance/UserStep.java | 21 ++++++++++++++++ .../kustacks/kuring/user/domain/UserTest.java | 25 ++++++++++++++++++- 11 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserAcademicEventNotificationRequest.java create mode 100644 src/main/java/com/kustacks/kuring/user/application/port/in/dto/UserAcademicEventNotificationCommand.java diff --git a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java index b1019faef..168eaeea2 100644 --- a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java +++ b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java @@ -58,6 +58,7 @@ public enum ResponseCodeAndMessages { /* Academic Events */ ACADEMIC_EVENT_SEARCH_SUCCESS(HttpStatus.OK.value(), "학사일정 조회에 성공했습니다."), + ACADEMIC_EVENT_NOTIFICATION_UPDATE_SUCCESS(HttpStatus.OK.value(), "학사일정 알림 설정이 변경되었습니다."), /** * ErrorCodes about auth diff --git a/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java b/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java index 9ea3b844c..cf515dcd8 100644 --- a/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java +++ b/src/main/java/com/kustacks/kuring/config/FeatureFlagConfig.java @@ -1,7 +1,8 @@ package com.kustacks.kuring.config; +import com.kustacks.kuring.common.env.RemotePropertyResolver; import com.kustacks.kuring.common.featureflag.FeatureFlagProperties; -import com.kustacks.kuring.common.featureflag.FeatureFlags; +import com.kustacks.kuring.common.featureflag.RemoteFeatureFlags; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; @@ -31,14 +32,9 @@ public AppConfigDataClient appConfigDataClient() { .region(Region.of(properties.region())) .build(); } -// -// @Bean -// public RemoteFeatureFlags remoteFeatureFlags(RemotePropertyResolver remotePropertyResolver) { -// return new RemoteFeatureFlags(remotePropertyResolver); -// } @Bean - public FeatureFlags featureFlags() { - return new FeatureFlags.AlwaysDisabledFeatureFlags(); + public RemoteFeatureFlags remoteFeatureFlags(RemotePropertyResolver remotePropertyResolver) { + return new RemoteFeatureFlags(remotePropertyResolver); } } diff --git a/src/main/java/com/kustacks/kuring/user/adapter/in/web/UserCommandApiV2.java b/src/main/java/com/kustacks/kuring/user/adapter/in/web/UserCommandApiV2.java index 906fbda8f..81213ef7e 100644 --- a/src/main/java/com/kustacks/kuring/user/adapter/in/web/UserCommandApiV2.java +++ b/src/main/java/com/kustacks/kuring/user/adapter/in/web/UserCommandApiV2.java @@ -7,6 +7,7 @@ import com.kustacks.kuring.common.dto.BaseResponse; import com.kustacks.kuring.common.exception.InvalidStateException; import com.kustacks.kuring.common.exception.code.ErrorCode; +import com.kustacks.kuring.user.adapter.in.web.dto.UserAcademicEventNotificationRequest; import com.kustacks.kuring.user.adapter.in.web.dto.UserBookmarkRequest; import com.kustacks.kuring.user.adapter.in.web.dto.UserCategoriesSubscribeRequest; import com.kustacks.kuring.user.adapter.in.web.dto.UserDepartmentsSubscribeRequest; @@ -15,8 +16,8 @@ import com.kustacks.kuring.user.adapter.in.web.dto.UserLoginResponse; import com.kustacks.kuring.user.adapter.in.web.dto.UserPasswordModifyRequest; import com.kustacks.kuring.user.adapter.in.web.dto.UserSignupRequest; -import com.kustacks.kuring.user.application.port.in.dto.UserWithdrawCommand; import com.kustacks.kuring.user.application.port.in.UserCommandUseCase; +import com.kustacks.kuring.user.application.port.in.dto.UserAcademicEventNotificationCommand; import com.kustacks.kuring.user.application.port.in.dto.UserBookmarkCommand; import com.kustacks.kuring.user.application.port.in.dto.UserCategoriesSubscribeCommand; import com.kustacks.kuring.user.application.port.in.dto.UserDepartmentsSubscribeCommand; @@ -26,6 +27,7 @@ import com.kustacks.kuring.user.application.port.in.dto.UserLogoutCommand; import com.kustacks.kuring.user.application.port.in.dto.UserPasswordModifyCommand; import com.kustacks.kuring.user.application.port.in.dto.UserSignupCommand; +import com.kustacks.kuring.user.application.port.in.dto.UserWithdrawCommand; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -42,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import static com.kustacks.kuring.auth.authentication.AuthorizationExtractor.extractAuthorizationValue; +import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.ACADEMIC_EVENT_NOTIFICATION_UPDATE_SUCCESS; import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.BOOKMARK_SAVE_SUCCESS; import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CATEGORY_SUBSCRIBE_SUCCESS; import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.DEPARTMENTS_SUBSCRIBE_SUCCESS; @@ -87,6 +90,17 @@ public ResponseEntity> editUserSubscribeDepartments( return ResponseEntity.ok().body(new BaseResponse<>(DEPARTMENTS_SUBSCRIBE_SUCCESS, null)); } + @Operation(summary = "사용자 학사일정 알림 설정 수정", description = "사용자의 학사일정 알림 설정 상태를 수정합니다.") + @SecurityRequirement(name = FCM_TOKEN_HEADER_KEY) + @PatchMapping(value = "/notifications/academic-events", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> updateAcademicEventNotification( + @Valid @RequestBody UserAcademicEventNotificationRequest request, + @RequestHeader(FCM_TOKEN_HEADER_KEY) String id + ) { + userCommandUseCase.updateAcademicEventNotification(new UserAcademicEventNotificationCommand(id, request.enabled())); + return ResponseEntity.ok().body(new BaseResponse<>(ACADEMIC_EVENT_NOTIFICATION_UPDATE_SUCCESS, null)); + } + @Operation(summary = "사용자 피드백 작성", description = "사용자가 피드백을 작성하여 저장합니다") @SecurityRequirement(name = FCM_TOKEN_HEADER_KEY) @PostMapping("/feedbacks") diff --git a/src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserAcademicEventNotificationRequest.java b/src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserAcademicEventNotificationRequest.java new file mode 100644 index 000000000..ce5bdd829 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/user/adapter/in/web/dto/UserAcademicEventNotificationRequest.java @@ -0,0 +1,8 @@ +package com.kustacks.kuring.user.adapter.in.web.dto; + +import jakarta.validation.constraints.NotNull; + +public record UserAcademicEventNotificationRequest( + @NotNull Boolean enabled +) { +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/user/application/port/in/UserCommandUseCase.java b/src/main/java/com/kustacks/kuring/user/application/port/in/UserCommandUseCase.java index c2dc1c611..cd06828f7 100644 --- a/src/main/java/com/kustacks/kuring/user/application/port/in/UserCommandUseCase.java +++ b/src/main/java/com/kustacks/kuring/user/application/port/in/UserCommandUseCase.java @@ -1,5 +1,6 @@ package com.kustacks.kuring.user.application.port.in; +import com.kustacks.kuring.user.application.port.in.dto.UserAcademicEventNotificationCommand; import com.kustacks.kuring.user.application.port.in.dto.UserBookmarkCommand; import com.kustacks.kuring.user.application.port.in.dto.UserCategoriesSubscribeCommand; import com.kustacks.kuring.user.application.port.in.dto.UserDecreaseQuestionCountCommand; @@ -15,6 +16,8 @@ public interface UserCommandUseCase { void editSubscribeCategories(UserCategoriesSubscribeCommand command); void editSubscribeDepartments(UserDepartmentsSubscribeCommand command); + + void updateAcademicEventNotification(UserAcademicEventNotificationCommand command); void saveFeedback(UserFeedbackCommand command); void saveBookmark(UserBookmarkCommand command); void decreaseQuestionCount(UserDecreaseQuestionCountCommand command); diff --git a/src/main/java/com/kustacks/kuring/user/application/port/in/dto/UserAcademicEventNotificationCommand.java b/src/main/java/com/kustacks/kuring/user/application/port/in/dto/UserAcademicEventNotificationCommand.java new file mode 100644 index 000000000..4906c15b5 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/user/application/port/in/dto/UserAcademicEventNotificationCommand.java @@ -0,0 +1,7 @@ +package com.kustacks.kuring.user.application.port.in.dto; + +public record UserAcademicEventNotificationCommand( + String userToken, + Boolean enabled +) { +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java b/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java index 9d1514700..247072db2 100644 --- a/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java +++ b/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java @@ -12,6 +12,7 @@ import com.kustacks.kuring.notice.domain.CategoryName; import com.kustacks.kuring.notice.domain.DepartmentName; import com.kustacks.kuring.user.application.port.in.UserCommandUseCase; +import com.kustacks.kuring.user.application.port.in.dto.UserAcademicEventNotificationCommand; import com.kustacks.kuring.user.application.port.in.dto.UserBookmarkCommand; import com.kustacks.kuring.user.application.port.in.dto.UserCategoriesSubscribeCommand; import com.kustacks.kuring.user.application.port.in.dto.UserDecreaseQuestionCountCommand; @@ -82,6 +83,12 @@ public void editSubscribeDepartments(UserDepartmentsSubscribeCommand command) { ); } + @Override + public void updateAcademicEventNotification(UserAcademicEventNotificationCommand command) { + User user = findUserByToken(command.userToken()); + user.updateAcademicNotificationEnabled(command.enabled()); + } + @Override public void saveFeedback(UserFeedbackCommand command) { User findUser = findUserByToken(command.userToken()); diff --git a/src/main/java/com/kustacks/kuring/user/domain/User.java b/src/main/java/com/kustacks/kuring/user/domain/User.java index 3e0cead79..ee3826296 100644 --- a/src/main/java/com/kustacks/kuring/user/domain/User.java +++ b/src/main/java/com/kustacks/kuring/user/domain/User.java @@ -61,6 +61,7 @@ public class User implements Serializable { @Column(nullable = true) private Long loginUserId; + @Getter(AccessLevel.PUBLIC) @Column(name = "academic_event_notification_enabled", nullable = false) private Boolean academicEventNotificationEnabled = Boolean.TRUE; @@ -177,6 +178,10 @@ public boolean isLoggedIn() { return this.loginUserId != null; } + public void updateAcademicNotificationEnabled(Boolean enabled) { + this.academicEventNotificationEnabled = enabled; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java index ecbe7695e..4a4fde583 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java @@ -42,6 +42,8 @@ import static com.kustacks.kuring.acceptance.UserStep.피드백_요청_응답_확인_v2; import static com.kustacks.kuring.acceptance.UserStep.학과_구독_요청; import static com.kustacks.kuring.acceptance.UserStep.학과_구독_응답_확인; +import static com.kustacks.kuring.acceptance.UserStep.학사일정_알림_토글_요청; +import static com.kustacks.kuring.acceptance.UserStep.학사일정_알림_토글_응답_확인; import static com.kustacks.kuring.acceptance.UserStep.회원_가입_요청; import static com.kustacks.kuring.acceptance.UserStep.회원_탈퇴_요청; import static com.kustacks.kuring.acceptance.UserStep.회원_탈퇴_응답_확인; @@ -502,4 +504,23 @@ void modify_password_with_wrong_email() { // then 실패_응답_확인(비밀번호_초기화_응답, HttpStatus.NOT_FOUND); } + + @DisplayName("[v2] 사용자는 학사일정 알림을 토글할 수 있다") + @Test + void toggle_academic_event_notification() { + // given + doNothing().when(firebaseSubscribeService).validationToken(anyString()); + + // when - 첫 번째 토글 (true → false) + var 첫번째_토글_응답 = 학사일정_알림_토글_요청(USER_FCM_TOKEN, false); + + // then + 학사일정_알림_토글_응답_확인(첫번째_토글_응답, false); + + // when - 두 번째 토글 (false → true) + var 두번째_토글_응답 = 학사일정_알림_토글_요청(USER_FCM_TOKEN, true); + + // then + 학사일정_알림_토글_응답_확인(두번째_토글_응답, true); + } } \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/acceptance/UserStep.java b/src/test/java/com/kustacks/kuring/acceptance/UserStep.java index 17e8a4074..ae324c2e2 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/UserStep.java +++ b/src/test/java/com/kustacks/kuring/acceptance/UserStep.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.acceptance; import com.kustacks.kuring.auth.dto.UserRegisterRequest; +import com.kustacks.kuring.user.adapter.in.web.dto.UserAcademicEventNotificationRequest; import com.kustacks.kuring.user.adapter.in.web.dto.UserBookmarkRequest; import com.kustacks.kuring.user.adapter.in.web.dto.UserCategoriesSubscribeRequest; import com.kustacks.kuring.user.adapter.in.web.dto.UserDepartmentsSubscribeRequest; @@ -325,4 +326,24 @@ public class UserStep { return response.jsonPath().getString("data.accessToken"); } + public static ExtractableResponse 학사일정_알림_토글_요청(String fcmToken, boolean enabled) { + return RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .header("User-Token", fcmToken) + .body(new UserAcademicEventNotificationRequest(enabled)) + .when().patch("/api/v2/users/notifications/academic-events") + .then().log().all() + .extract(); + } + + public static void 학사일정_알림_토글_응답_확인(ExtractableResponse response, Boolean expectedEnabled) { + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getInt("code")).isEqualTo(200), + () -> assertThat(response.jsonPath().getString("message")).isEqualTo("학사일정 알림 설정이 변경되었습니다."), + () -> assertThat(response.jsonPath().getList("data")).isNull() + ); + } + } diff --git a/src/test/java/com/kustacks/kuring/user/domain/UserTest.java b/src/test/java/com/kustacks/kuring/user/domain/UserTest.java index 88763bd5c..c4a74b726 100644 --- a/src/test/java/com/kustacks/kuring/user/domain/UserTest.java +++ b/src/test/java/com/kustacks/kuring/user/domain/UserTest.java @@ -9,7 +9,9 @@ import java.util.List; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("도메인 : User") class UserTest { @@ -184,6 +186,27 @@ void logout_user() { .isNull(); } + @DisplayName("학사일정 알림 상태를 변경할 수 있다") + @Test + void update_academic_event_notification() { + // given - true + User user = createUser(1L, "token"); + + // when - false + user.updateAcademicNotificationEnabled(false); + + // then - false + assertThat(user.getAcademicEventNotificationEnabled()) + .isFalse(); + + // when - true + user.updateAcademicNotificationEnabled(true); + + // then - true + assertThat(user.getAcademicEventNotificationEnabled()) + .isTrue(); + } + private RootUser createRootUser(Long id, String email, String password, String nickname) { RootUser rootUser = new RootUser(email, password, nickname); ReflectionTestUtils.setField(rootUser, "id", id); From 656d52b0764518bb28091812f53ef852db14f6eb Mon Sep 17 00:00:00 2001 From: Han Ju Kim Date: Sun, 21 Sep 2025 18:28:12 +0900 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20=EA=B5=90=EC=A7=81=EC=9B=90=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=9E=A9=20DTO=EB=B3=80=ED=99=98=EA=B0=84=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20Key=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuring/worker/update/staff/StaffUpdater.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java index caa6c3c48..48a1c343c 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java @@ -27,8 +27,9 @@ public class StaffUpdater { private final StaffScraper staffScraper; private final List deptInfos; private final FeatureFlags featureFlags; - + @Scheduled(fixedRate = 30, timeUnit = TimeUnit.DAYS) +// @Scheduled(cron = "0 0/5 * * * *") public void update() { if (featureFlags.isEnabled(KuringFeatures.UPDATE_STAFF.getFeature())) { log.info("========== 교직원 업데이트 시작 =========="); @@ -53,6 +54,7 @@ private void scrapSingleDepartmentsStaffs(Map kuStaffDtoMap, D try { Map staffScrapResultMap = scrapStaffByDepartment(deptInfo); mergeForMultipleDepartmentsStaff(kuStaffDtoMap, staffScrapResultMap); + log.info("스크랩 완료 {} ", deptInfo.getDeptName()); } catch (InternalLogicException e) { log.error("[StaffScraperException] {}학과 교직원 스크래핑 문제 발생.", deptInfo.getDeptName()); } @@ -65,14 +67,18 @@ private Map scrapStaffByDepartment(DeptInfo deptInfo) { private Map convertStaffDtoMap(List scrapedStaffDtos) { return scrapedStaffDtos.stream() - .collect(Collectors.toMap(StaffDto::identifier, staffDto -> staffDto)); + .collect(Collectors.toMap( + StaffDto::identifier, + staffDto -> staffDto, + (existing, replacement) -> existing)); } private void mergeForMultipleDepartmentsStaff( Map kuStaffDTOMap, Map staffDtoMap ) { - staffDtoMap.forEach((key, value) -> kuStaffDTOMap.merge(key, value, (v1, v2) -> { + staffDtoMap.forEach((key, value) -> kuStaffDTOMap.merge(key, value, + (v1, v2) -> { v1.setDeptName(v1.getDeptName() + ", " + v2.getDeptName()); return v1; } From 61244aa25434c6f8efc456745f4f62901c5cb440 Mon Sep 17 00:00:00 2001 From: Han Ju Kim Date: Sun, 21 Sep 2025 18:34:40 +0900 Subject: [PATCH 06/16] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kustacks/kuring/worker/update/staff/StaffUpdater.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java index 48a1c343c..92836834e 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java @@ -54,7 +54,6 @@ private void scrapSingleDepartmentsStaffs(Map kuStaffDtoMap, D try { Map staffScrapResultMap = scrapStaffByDepartment(deptInfo); mergeForMultipleDepartmentsStaff(kuStaffDtoMap, staffScrapResultMap); - log.info("스크랩 완료 {} ", deptInfo.getDeptName()); } catch (InternalLogicException e) { log.error("[StaffScraperException] {}학과 교직원 스크래핑 문제 발생.", deptInfo.getDeptName()); } From ebae03057e5b0cb1344f6b80e1051e3bf1fd6c98 Mon Sep 17 00:00:00 2001 From: Han Ju Kim Date: Sun, 21 Sep 2025 18:35:15 +0900 Subject: [PATCH 07/16] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kustacks/kuring/worker/update/staff/StaffUpdater.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java index 92836834e..29172f4f0 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/staff/StaffUpdater.java @@ -29,7 +29,6 @@ public class StaffUpdater { private final FeatureFlags featureFlags; @Scheduled(fixedRate = 30, timeUnit = TimeUnit.DAYS) -// @Scheduled(cron = "0 0/5 * * * *") public void update() { if (featureFlags.isEnabled(KuringFeatures.UPDATE_STAFF.getFeature())) { log.info("========== 교직원 업데이트 시작 =========="); From afe4b043f3025849cebed7d9763deaeaa2281b54 Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:10:07 +0900 Subject: [PATCH 08/16] =?UTF-8?q?Feat:=20=20=ED=95=99=EC=82=AC=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=95=8C=EB=A6=BC=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=A7=81=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20(#305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 --- .../UserRegisterNonChainingFilter.java | 23 ++- .../AcademicEventPersistenceAdapter.java | 5 + .../AcademicEventQueryRepository.java | 2 + .../AcademicEventQueryRepositoryImpl.java | 20 ++ .../in/AcademicEventNotificationUseCase.java | 6 + .../port/out/AcademicEventQueryPort.java | 8 + .../port/out/dto/AcademicEventReadModel.java | 17 ++ .../AcademicEventNotificationService.java | 102 ++++++++++ .../service/FirebaseSubscribeService.java | 1 + .../service/UserCommandService.java | 28 ++- .../application/service/UserQueryService.java | 8 +- .../com/kustacks/kuring/user/domain/User.java | 4 + .../AcademicEventNotificationScheduler.java | 32 ++++ .../update/calendar/AcademicEventUpdater.java | 3 +- .../AcademicEventNotificationServiceTest.java | 102 ++++++++++ .../calendar/domain/AcademicEventTest.java | 1 - .../kuring/support/FeatureFlagsSupport.java | 41 ++++ .../support/IntegrationTestSupport.java | 4 + .../kustacks/kuring/user/domain/UserTest.java | 22 +++ ...cademicEventNotificationSchedulerTest.java | 179 ++++++++++++++++++ .../calendar/AcademicEventUpdaterTest.java | 6 - .../worker/update/user/UserUpdaterTest.java | 14 +- 22 files changed, 598 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/kustacks/kuring/calendar/application/port/in/AcademicEventNotificationUseCase.java create mode 100644 src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java create mode 100644 src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java create mode 100644 src/test/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationServiceTest.java create mode 100644 src/test/java/com/kustacks/kuring/support/FeatureFlagsSupport.java create mode 100644 src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java diff --git a/src/main/java/com/kustacks/kuring/auth/interceptor/UserRegisterNonChainingFilter.java b/src/main/java/com/kustacks/kuring/auth/interceptor/UserRegisterNonChainingFilter.java index f8434dfd5..c227c48fc 100644 --- a/src/main/java/com/kustacks/kuring/auth/interceptor/UserRegisterNonChainingFilter.java +++ b/src/main/java/com/kustacks/kuring/auth/interceptor/UserRegisterNonChainingFilter.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.stream.Collectors; +import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC; import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ALL_DEVICE_SUBSCRIBED_TOPIC; @Slf4j @@ -59,14 +60,26 @@ private void register(String userFcmToken) { log.warn("User already exists: {}", userFcmToken, e); } - UserSubscribeCommand command = - new UserSubscribeCommand( - userFcmToken, - serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC) - ); + subscribeDefaultTopics(userFcmToken); + } + + private void subscribeDefaultTopics(String userFcmToken) { + subscribeTopic(userFcmToken, ALL_DEVICE_SUBSCRIBED_TOPIC); + subscribeTopic(userFcmToken, ACADEMIC_EVENT_TOPIC); + } + + private void subscribeTopic(String userFcmToken, String topic) { + UserSubscribeCommand command = makeSubscribeCommand(userFcmToken, topic); firebaseService.subscribe(command); } + private UserSubscribeCommand makeSubscribeCommand(String userFcmToken, String topic) { + return new UserSubscribeCommand( + userFcmToken, + serverProperties.ifDevThenAddSuffix(topic) + ); + } + public String convert(HttpServletRequest request) throws IOException { String content = request.getReader().lines() .collect(Collectors.joining(System.lineSeparator())); diff --git a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventPersistenceAdapter.java b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventPersistenceAdapter.java index 092e05400..3bfd96035 100644 --- a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventPersistenceAdapter.java +++ b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventPersistenceAdapter.java @@ -66,4 +66,9 @@ public List findEventsAfter(LocalDate startDate) { public List findEventsBefore(LocalDate endDate) { return findEventsBetweenDate(null, endDate); } + + @Override + public List findTodayEvents(LocalDate date) { + return academicEventRepository.findTodayEvents(date); + } } diff --git a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepository.java b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepository.java index 521ddb10a..1dfa6ca3e 100644 --- a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepository.java +++ b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepository.java @@ -13,4 +13,6 @@ interface AcademicEventQueryRepository { List findAllEventReadModels(); List findEventsByDate(LocalDate startDate, LocalDate endDate); + + List findTodayEvents(LocalDate date); } \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java index 68fa0a612..141fe9412 100644 --- a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java +++ b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java @@ -38,6 +38,7 @@ public List findAllEventReadModels() { return queryFactorySelectingReadModel() .fetch(); } + @Override @Transactional(readOnly = true) public List findEventsByDate(LocalDate startDate, LocalDate endDate) { @@ -73,4 +74,23 @@ private BooleanExpression isEndTimeGoe(LocalDate startDate) { private BooleanExpression isStartTimeLt(LocalDate endDate) { return endDate != null ? academicEvent.startTime.lt(endDate.plusDays(1L).atStartOfDay()) : null; } + + @Override + @Transactional(readOnly = true) + public List findTodayEvents(LocalDate date) { + return queryFactorySelectingReadModel() + .where( + academicEvent.notifyEnabled.isTrue(), + academicEvent.startTime.between( + date.atStartOfDay(), + date.atTime(23, 59, 59, 999_999_999) + ).or( + academicEvent.endTime.between( + date.atStartOfDay(), + date.atTime(23, 59, 59, 999_999_999) + ) + ) + ) + .fetch(); + } } \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/calendar/application/port/in/AcademicEventNotificationUseCase.java b/src/main/java/com/kustacks/kuring/calendar/application/port/in/AcademicEventNotificationUseCase.java new file mode 100644 index 000000000..7cd457538 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/calendar/application/port/in/AcademicEventNotificationUseCase.java @@ -0,0 +1,6 @@ +package com.kustacks.kuring.calendar.application.port.in; + +public interface AcademicEventNotificationUseCase { + + void sendTodayAcademicEventNotifications(); +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/calendar/application/port/out/AcademicEventQueryPort.java b/src/main/java/com/kustacks/kuring/calendar/application/port/out/AcademicEventQueryPort.java index 9ad37df01..e9526fdfe 100644 --- a/src/main/java/com/kustacks/kuring/calendar/application/port/out/AcademicEventQueryPort.java +++ b/src/main/java/com/kustacks/kuring/calendar/application/port/out/AcademicEventQueryPort.java @@ -22,4 +22,12 @@ public interface AcademicEventQueryPort { List findEventsAfter(LocalDate startDate); List findEventsBefore(LocalDate endDate); + + /** + * 지정된 날짜에 시작하거나 종료하는 알림 가능한 학사일정들을 조회합니다. + * + * @param date 조회할 날짜 + * @return notifyEnabled가 true이고 해당 날짜에 시작하거나 종료하는 일정들 + */ + List findTodayEvents(LocalDate date); } diff --git a/src/main/java/com/kustacks/kuring/calendar/application/port/out/dto/AcademicEventReadModel.java b/src/main/java/com/kustacks/kuring/calendar/application/port/out/dto/AcademicEventReadModel.java index c78ff1233..c86335c92 100644 --- a/src/main/java/com/kustacks/kuring/calendar/application/port/out/dto/AcademicEventReadModel.java +++ b/src/main/java/com/kustacks/kuring/calendar/application/port/out/dto/AcademicEventReadModel.java @@ -3,6 +3,7 @@ import com.kustacks.kuring.calendar.domain.Transparent; import com.querydsl.core.annotations.QueryProjection; +import java.time.LocalDate; import java.time.LocalDateTime; public record AcademicEventReadModel( @@ -31,4 +32,20 @@ public AcademicEventReadModel(Long id, String eventUid, String summary, String d this.startTime = startTime; this.endTime = endTime; } + + public boolean isStartingToday(LocalDate today) { + return startTime.toLocalDate().equals(today); + } + + public boolean isEndingToday(LocalDate today) { + return endTime.toLocalDate().equals(today); + } + + public boolean isInProgressToday(LocalDate today) { + return isStartingToday(today) && isEndingToday(today); + } + + public boolean isRelatedToDate(LocalDate date) { + return isStartingToday(date) || isEndingToday(date); + } } diff --git a/src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java b/src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java new file mode 100644 index 000000000..4d3cc800f --- /dev/null +++ b/src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java @@ -0,0 +1,102 @@ +package com.kustacks.kuring.calendar.application.service; + +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import com.kustacks.kuring.calendar.application.port.in.AcademicEventNotificationUseCase; +import com.kustacks.kuring.calendar.application.port.out.AcademicEventQueryPort; +import com.kustacks.kuring.calendar.application.port.out.dto.AcademicEventReadModel; +import com.kustacks.kuring.common.annotation.UseCase; +import com.kustacks.kuring.common.properties.ServerProperties; +import com.kustacks.kuring.message.application.port.out.FirebaseMessagingPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDate; +import java.util.List; + +import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC; + +@Slf4j +@UseCase +@RequiredArgsConstructor +public class AcademicEventNotificationService implements AcademicEventNotificationUseCase { + + private static final String MESSAGE_DEFAULT = "오늘은 %s 일정이 있어요"; + + private final AcademicEventQueryPort academicEventQueryPort; + private final FirebaseMessagingPort firebaseMessagingPort; + private final ServerProperties serverProperties; + + @Override + public void sendTodayAcademicEventNotifications() { + LocalDate today = LocalDate.now(); + log.info("******** 학사일정 알림 발송 시작 ({}일자) ********", today); + + // 1. 오늘의 학사일정 조회 + List todayEvents = academicEventQueryPort.findTodayEvents(today); + + if (todayEvents.isEmpty()) { + log.info("오늘 알림 발송할 학사일정이 없습니다."); + return; + } + + // 2. 각 일정별 알림 발송 (토픽 기반) + int successCount = sendNotificationForEvents(todayEvents); + + log.info("******** 학사일정 알림 발송 완료 (총 {}개, 성공 {}개, 실패 {}개) ********", todayEvents.size(), successCount, todayEvents.size() - successCount); + } + + private int sendNotificationForEvents(List todayEvents) { + int successCount = 0; + for (AcademicEventReadModel todayEvent : todayEvents) { + boolean success = sendNotificationForEvent(todayEvent); + successCount += success ? 1 : 0; + } + return successCount; + } + + private boolean sendNotificationForEvent(AcademicEventReadModel event) { + try { + String title = createTitle(event); + String body = createBody(event); + sendNotificationMessage(title, body); + + log.info("학사일정(ID = {}) 알림 전송에 성공했습니다.", event.id()); + return true; + } catch (FirebaseMessagingException e) { + log.error("학사일정(ID = {}) FCM 전송에 실패했습니다.", event.id()); + return false; + } catch (Exception e) { + log.error("학사일정(ID = {})을 FCM에 보내는 중 알 수 없는 오류가 발생했습니다.", event.id()); + return false; + } + } + + private String createTitle(AcademicEventReadModel event) { + return new StringBuilder() + .append("[") + .append(event.summary()) + .append("]") + .toString(); + } + + private String createBody(AcademicEventReadModel event) { + return String.format(MESSAGE_DEFAULT, event.summary()); + } + + private void sendNotificationMessage(String title, String body) throws FirebaseMessagingException { + Message message = makeMessage(title, body); + firebaseMessagingPort.send(message); + } + + private Message makeMessage(String title, String body) { + return Message.builder() + .setNotification(Notification.builder() + .setTitle(title) + .setBody(body) + .build()) + .setTopic(serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/message/application/service/FirebaseSubscribeService.java b/src/main/java/com/kustacks/kuring/message/application/service/FirebaseSubscribeService.java index a9a12f33b..8355847eb 100644 --- a/src/main/java/com/kustacks/kuring/message/application/service/FirebaseSubscribeService.java +++ b/src/main/java/com/kustacks/kuring/message/application/service/FirebaseSubscribeService.java @@ -23,6 +23,7 @@ public class FirebaseSubscribeService implements FirebaseWithUserUseCase { public static final String ALL_DEVICE_SUBSCRIBED_TOPIC = "allDevice"; + public static final String ACADEMIC_EVENT_TOPIC = "academicEvent"; private final FirebaseSubscribePort firebaseSubscribePort; private final FirebaseMessagingPort firebaseMessagingPort; diff --git a/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java b/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java index 247072db2..b30fab9de 100644 --- a/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java +++ b/src/main/java/com/kustacks/kuring/user/application/service/UserCommandService.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.Optional; +import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC; import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ALL_DEVICE_SUBSCRIBED_TOPIC; import static com.kustacks.kuring.user.domain.RootUser.ROOT_USER_EXTRA_QUESTION_COUNT; @@ -86,7 +87,16 @@ public void editSubscribeDepartments(UserDepartmentsSubscribeCommand command) { @Override public void updateAcademicEventNotification(UserAcademicEventNotificationCommand command) { User user = findUserByToken(command.userToken()); - user.updateAcademicNotificationEnabled(command.enabled()); + Boolean wasEnabled = user.getAcademicEventNotificationEnabled(); + + // 설정이 변경된 경우에만 토픽 구독/해제 처리 + if (wasEnabled.equals(command.enabled())) { + editAcademicEventNotificationEnabled( + command.userToken(), + command.enabled() + ); + user.updateAcademicNotificationEnabled(command.enabled()); + } } @Override @@ -365,11 +375,25 @@ private void unsubscribeDepartment(String userToken, DepartmentName removeDepart user.unsubscribeDepartment(removeDepartmentName); } + private void editAcademicEventNotificationEnabled(String userToken, boolean enabled) { + if (enabled) { + // 알림 활성화 -> 토픽 구독 + userEventPort.subscribeEvent(userToken, + serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC)); + } else { + // 알림 비활성화 -> 토픽 구독 해제 + userEventPort.unsubscribeEvent(userToken, + serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC)); + } + } + private User findUserByToken(String token) { Optional optionalUser = userQueryPort.findByToken(token); if (optionalUser.isEmpty()) { - optionalUser = Optional.of(userCommandPort.save(new User(token))); + User newUser = new User(token); + optionalUser = Optional.of(userCommandPort.save(newUser)); userEventPort.subscribeEvent(token, serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC)); + userEventPort.subscribeEvent(token, serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC)); } return optionalUser.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); diff --git a/src/main/java/com/kustacks/kuring/user/application/service/UserQueryService.java b/src/main/java/com/kustacks/kuring/user/application/service/UserQueryService.java index df2874214..fbc677892 100644 --- a/src/main/java/com/kustacks/kuring/user/application/service/UserQueryService.java +++ b/src/main/java/com/kustacks/kuring/user/application/service/UserQueryService.java @@ -9,7 +9,11 @@ import com.kustacks.kuring.notice.domain.CategoryName; import com.kustacks.kuring.notice.domain.DepartmentName; import com.kustacks.kuring.user.application.port.in.UserQueryUseCase; -import com.kustacks.kuring.user.application.port.in.dto.*; +import com.kustacks.kuring.user.application.port.in.dto.UserAIAskCountResult; +import com.kustacks.kuring.user.application.port.in.dto.UserBookmarkResult; +import com.kustacks.kuring.user.application.port.in.dto.UserCategoryNameResult; +import com.kustacks.kuring.user.application.port.in.dto.UserDepartmentNameResult; +import com.kustacks.kuring.user.application.port.in.dto.UserInfoResult; import com.kustacks.kuring.user.application.port.out.RootUserQueryPort; import com.kustacks.kuring.user.application.port.out.UserCommandPort; import com.kustacks.kuring.user.application.port.out.UserEventPort; @@ -23,6 +27,7 @@ import java.util.List; import java.util.Optional; +import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC; import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ALL_DEVICE_SUBSCRIBED_TOPIC; @Slf4j @@ -103,6 +108,7 @@ private User findUserByToken(String token) { if (optionalUser.isEmpty()) { optionalUser = Optional.of(userCommandPort.save(new User(token))); userEventPort.subscribeEvent(token, serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC)); + userEventPort.subscribeEvent(token, serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC)); } return optionalUser.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); diff --git a/src/main/java/com/kustacks/kuring/user/domain/User.java b/src/main/java/com/kustacks/kuring/user/domain/User.java index ee3826296..7446d8c6a 100644 --- a/src/main/java/com/kustacks/kuring/user/domain/User.java +++ b/src/main/java/com/kustacks/kuring/user/domain/User.java @@ -182,6 +182,10 @@ public void updateAcademicNotificationEnabled(Boolean enabled) { this.academicEventNotificationEnabled = enabled; } + public boolean isAcademicEventNotificationEnabled() { + return Boolean.TRUE.equals(this.academicEventNotificationEnabled); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java b/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java new file mode 100644 index 000000000..26ebca21c --- /dev/null +++ b/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java @@ -0,0 +1,32 @@ +package com.kustacks.kuring.worker.notification; + +import com.kustacks.kuring.calendar.application.port.in.AcademicEventNotificationUseCase; +import com.kustacks.kuring.common.featureflag.FeatureFlags; +import com.kustacks.kuring.common.featureflag.KuringFeatures; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AcademicEventNotificationScheduler { + + private final AcademicEventNotificationUseCase academicEventNotificationUseCase; + private final FeatureFlags featureFlags; + + // 매일 오전 9시 학사일정 알림 전송 + @Scheduled(cron = "0 0 9 * * *", zone = "Asia/Seoul") + public void sendDailyAcademicEventNotifications() { + if (featureFlags.isEnabled(KuringFeatures.NOTIFY_ACADEMIC_EVENT.getFeature())) { + log.info("******** 일일 학사일정 알림 발송 시작 ********"); + try { + academicEventNotificationUseCase.sendTodayAcademicEventNotifications(); + log.info("******** 일일 학사일정 알림 발송 완료 ********"); + } catch (Exception e) { + log.error("학사일정 알림 발송 간 오류가 발생했습니다.", e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java index 312c5b847..028807313 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java @@ -25,8 +25,7 @@ public class AcademicEventUpdater { private final FeatureFlags featureFlags; //매월 1일 00시 00분 업데이트 진행 - //TODO: 현재는 매일 새벽 5시 업데이트 적용(테스트), 원복 필요 - @Scheduled(cron = "0 0 5 * * *", zone = "Asia/Seoul") + @Scheduled(cron = "0 0 0 1 * *", zone = "Asia/Seoul") public void update() { if (featureFlags.isEnabled(KuringFeatures.UPDATE_ACADEMIC_EVENT.getFeature())) { log.info("******** 학사일정 업데이트 시작 ********"); diff --git a/src/test/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationServiceTest.java b/src/test/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationServiceTest.java new file mode 100644 index 000000000..28c2fa9c1 --- /dev/null +++ b/src/test/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationServiceTest.java @@ -0,0 +1,102 @@ +package com.kustacks.kuring.calendar.application.service; + +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.kustacks.kuring.calendar.application.port.out.AcademicEventQueryPort; +import com.kustacks.kuring.calendar.application.port.out.dto.AcademicEventReadModel; +import com.kustacks.kuring.calendar.domain.Transparent; +import com.kustacks.kuring.common.properties.ServerProperties; +import com.kustacks.kuring.message.application.port.out.FirebaseMessagingPort; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("서비스 : AcademicEventNotificationService") +class AcademicEventNotificationServiceTest { + + @Mock + private AcademicEventQueryPort academicEventQueryPort; + + @Mock + private FirebaseMessagingPort firebaseMessagingPort; + + @Mock + private ServerProperties serverProperties; + + @InjectMocks + private AcademicEventNotificationService service; + + @DisplayName("오늘 일정이 없으면 알림을 발송하지 않는다") + @Test + void should_not_send_notification_when_no_events() throws FirebaseMessagingException { + // given + when(academicEventQueryPort.findTodayEvents(any(LocalDate.class))).thenReturn(List.of()); + + // when + service.sendTodayAcademicEventNotifications(); + + // then + verify(firebaseMessagingPort, never()).send(any(Message.class)); + } + + @DisplayName("일정이 있으면 토픽으로 알림을 발송한다") + @Test + void should_send_notification_to_topic_when_events_exist() throws FirebaseMessagingException { + // given + AcademicEventReadModel event = createTestEventReadModel(1L, "개강", LocalDateTime.now(), LocalDateTime.now()); + when(academicEventQueryPort.findTodayEvents(any(LocalDate.class))).thenReturn(List.of(event)); + when(serverProperties.ifDevThenAddSuffix("academicEvent")).thenReturn("academicEvent"); + + // when + service.sendTodayAcademicEventNotifications(); + + // then + verify(firebaseMessagingPort, times(1)).send(any(Message.class)); + } + + @DisplayName("여러 일정이 있으면 각 일정마다 토픽으로 알림을 발송한다") + @Test + void should_send_notifications_for_multiple_events() throws FirebaseMessagingException { + // given + AcademicEventReadModel event1 = createTestEventReadModel(1L, "개강", LocalDateTime.now(), LocalDateTime.now()); + AcademicEventReadModel event2 = createTestEventReadModel(2L, "종강", LocalDateTime.now(), LocalDateTime.now()); + when(academicEventQueryPort.findTodayEvents(any(LocalDate.class))).thenReturn(List.of(event1, event2)); + when(serverProperties.ifDevThenAddSuffix("academicEvent")).thenReturn("academicEvent"); + + // when + service.sendTodayAcademicEventNotifications(); + + // then + verify(firebaseMessagingPort, times(2)).send(any(Message.class)); + } + + private AcademicEventReadModel createTestEventReadModel(Long id, String summary, LocalDateTime startTime, LocalDateTime endTime) { + return new AcademicEventReadModel( + id, + UUID.randomUUID().toString(), + summary, + "테스트 이벤트", + "TEST", + Transparent.TRANSPARENT, + 1, + true, + startTime, + endTime + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java b/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java index 357ab6837..f27419264 100644 --- a/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java +++ b/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java @@ -98,7 +98,6 @@ void domain_update_method_test() { assertThat(existingEvent.getEndTime()).isEqualTo(endTime.plusHours(1)); } - private AcademicEvent createAcademicEvent() { return AcademicEvent.builder() .eventUid(uid) diff --git a/src/test/java/com/kustacks/kuring/support/FeatureFlagsSupport.java b/src/test/java/com/kustacks/kuring/support/FeatureFlagsSupport.java new file mode 100644 index 000000000..705e1d8cf --- /dev/null +++ b/src/test/java/com/kustacks/kuring/support/FeatureFlagsSupport.java @@ -0,0 +1,41 @@ +package com.kustacks.kuring.support; + +import com.kustacks.kuring.common.env.MockKuringPropertyRestClient; +import com.kustacks.kuring.common.env.RemotePropertyResolver; +import com.kustacks.kuring.common.featureflag.RemoteFeatureFlags; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class FeatureFlagsSupport { + + private final RemotePropertyResolver remotePropertyResolver; + private final RemoteFeatureFlags remoteFeatureFlags; + + public FeatureFlagsSupport(RemotePropertyResolver mockRemotePropertyResolver, + RemoteFeatureFlags remoteFeatureFlags) { + this.remotePropertyResolver = mockRemotePropertyResolver; + this.remoteFeatureFlags = remoteFeatureFlags; + } + + //refresh를 꼭 해주어야 한다... + public void setMapProperty(String key, boolean value) { + setProperty(key, Map.of("enabled", value)); + } + + public void setProperty(String key, Object value) { + if (remotePropertyResolver instanceof MockKuringPropertyRestClient mockClient) { + mockClient.setProperty(key, value); + remoteFeatureFlags.refresh(); + + } + } + + public void resetProperties() { + if (remotePropertyResolver instanceof MockKuringPropertyRestClient mockClient) { + mockClient.resetProperties(); + remoteFeatureFlags.refresh(); + } + } +} diff --git a/src/test/java/com/kustacks/kuring/support/IntegrationTestSupport.java b/src/test/java/com/kustacks/kuring/support/IntegrationTestSupport.java index 2166131f7..0a55ed3e0 100644 --- a/src/test/java/com/kustacks/kuring/support/IntegrationTestSupport.java +++ b/src/test/java/com/kustacks/kuring/support/IntegrationTestSupport.java @@ -41,6 +41,9 @@ public class IntegrationTestSupport { @LocalServerPort protected int port; + @Autowired + protected FeatureFlagsSupport featureFlagsSupport; + @Autowired private DatabaseConfigurator databaseConfigurator; @@ -53,5 +56,6 @@ public void setUp() { databaseConfigurator.clear(); databaseConfigurator.loadData(); badWordValidator.wordsInit(); + featureFlagsSupport.resetProperties(); } } diff --git a/src/test/java/com/kustacks/kuring/user/domain/UserTest.java b/src/test/java/com/kustacks/kuring/user/domain/UserTest.java index c4a74b726..4393a73a2 100644 --- a/src/test/java/com/kustacks/kuring/user/domain/UserTest.java +++ b/src/test/java/com/kustacks/kuring/user/domain/UserTest.java @@ -207,6 +207,28 @@ void update_academic_event_notification() { .isTrue(); } + @DisplayName("학사일정 알림이 활성화되어 있는지 확인한다") + @Test + void is_academic_event_notification_enabled() { + // given + User user = createUser(1L, "token"); + + // when & then - 기본값은 true + assertThat(user.isAcademicEventNotificationEnabled()).isTrue(); + + // when - false로 설정 + user.updateAcademicNotificationEnabled(false); + + // then + assertThat(user.isAcademicEventNotificationEnabled()).isFalse(); + + // when - true로 다시 설정 + user.updateAcademicNotificationEnabled(true); + + // then + assertThat(user.isAcademicEventNotificationEnabled()).isTrue(); + } + private RootUser createRootUser(Long id, String email, String password, String nickname) { RootUser rootUser = new RootUser(email, password, nickname); ReflectionTestUtils.setField(rootUser, "id", id); diff --git a/src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java b/src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java new file mode 100644 index 000000000..e55bc996a --- /dev/null +++ b/src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java @@ -0,0 +1,179 @@ +package com.kustacks.kuring.worker.notification; + +import com.google.firebase.messaging.FirebaseMessagingException; +import com.kustacks.kuring.calendar.adapter.out.persistence.AcademicEventRepository; +import com.kustacks.kuring.calendar.application.port.in.AcademicEventNotificationUseCase; +import com.kustacks.kuring.calendar.domain.AcademicEvent; +import com.kustacks.kuring.calendar.domain.Transparent; +import com.kustacks.kuring.common.featureflag.KuringFeatures; +import com.kustacks.kuring.message.adapter.out.firebase.FakeFirebaseAdapter; +import com.kustacks.kuring.support.IntegrationTestSupport; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Transactional +@DisplayName("통합 테스트 : AcademicEventNotificationScheduler") +class AcademicEventNotificationSchedulerTest extends IntegrationTestSupport { + + @Autowired + private AcademicEventNotificationScheduler scheduler; + + @Autowired + private AcademicEventRepository academicEventRepository; + + @SpyBean + private FakeFirebaseAdapter fakeFirebaseAdapter; + + @SpyBean + private AcademicEventNotificationUseCase academicEventNotificationUseCase; + + @Autowired + private EntityManager entityManager; + + @DisplayName("Feature Flag가 OFF이면 알림을 발송하지 않는다") + @Test + void should_not_send_notification_when_feature_flag_disabled() { + // given + featureFlagsSupport.setMapProperty(KuringFeatures.NOTIFY_ACADEMIC_EVENT.getFeature().value(), false); + + // when & then + assertThatCode(() -> scheduler.sendDailyAcademicEventNotifications()) + .doesNotThrowAnyException(); + + // then + verify(academicEventNotificationUseCase, never()).sendTodayAcademicEventNotifications(); + } + + @DisplayName("오늘 일정 없음 = 알림 발송 안함") + @Test + void should_not_send_notification_when_no_events_today() throws FirebaseMessagingException { + // given + // 오늘이 아닌 다른 날의 일정만 생성 + LocalDateTime tomorrow = LocalDateTime.now().plusDays(1); + createTestAcademicEvent("내일 일정", tomorrow, tomorrow.plusHours(2)); + + // when & then + assertThatCode(() -> scheduler.sendDailyAcademicEventNotifications()) + .doesNotThrowAnyException(); + + // then + verify(fakeFirebaseAdapter, never()).send(any()); + } + + @DisplayName("오늘 일정 있음 = 토픽으로 알림 발송") + @Test + void should_send_notification_to_topic_when_events_exist() throws FirebaseMessagingException { + // given + // 오늘 일정 생성 + LocalDateTime today = LocalDateTime.now(); + createTestAcademicEvent("오늘 일정", today, today.plusHours(2)); + + // when & then + assertThatCode(() -> scheduler.sendDailyAcademicEventNotifications()) + .doesNotThrowAnyException(); + + // then - 토픽으로 1번 발송(유저 1명) + verify(fakeFirebaseAdapter, times(1)).send(any()); + } + + @DisplayName("오늘 시작하는 일정 알림 발송") + @Test + void should_send_notification_for_today_starting_event() throws FirebaseMessagingException { + // given + // 오늘 시작하는 일정 생성 + LocalDateTime today = LocalDateTime.now(); + createTestAcademicEvent("개강", today, today.plusDays(1)); + + // when & then + assertThatCode(() -> scheduler.sendDailyAcademicEventNotifications()) + .doesNotThrowAnyException(); + + // then - 토픽으로 1번 발송(유저 1명) + verify(fakeFirebaseAdapter, times(1)).send(any()); + } + + @DisplayName("오늘 종료하는 일정 알림 발송") + @Test + void should_send_notification_for_today_ending_event() throws FirebaseMessagingException { + // given + // 오늘 종료하는 일정 생성 + LocalDateTime today = LocalDateTime.now(); + createTestAcademicEvent("종강", today.minusDays(1), today); + + // when & then + assertThatCode(() -> scheduler.sendDailyAcademicEventNotifications()) + .doesNotThrowAnyException(); + + // then - 토픽으로 1번 발송 + verify(fakeFirebaseAdapter, times(1)).send(any()); + } + + @DisplayName("오늘 진행중인 일정 알림 발송") + @Test + void should_send_notification_for_today_in_progress_event() throws FirebaseMessagingException { + // given + // 오늘 진행중인 일정 생성 (시작일과 종료일이 모두 오늘) + LocalDateTime today = LocalDateTime.now(); + createTestAcademicEvent("졸업식", today, today.plusHours(4)); + + // when & then + assertThatCode(() -> scheduler.sendDailyAcademicEventNotifications()) + .doesNotThrowAnyException(); + + // then - 토픽으로 1번 발송 + verify(fakeFirebaseAdapter, times(1)).send(any()); + } + + @DisplayName("여러 일정이 있는 경우 각 일정마다 알림 발송") + @Test + void should_send_notifications_for_multiple_events() throws FirebaseMessagingException { + // given + // 오늘 관련 일정 2개 생성 + LocalDateTime today = LocalDateTime.now(); + createTestAcademicEvent("개강", today, today.plusDays(1)); // 오늘 시작 + createTestAcademicEvent("종강", today.minusDays(1), today); // 오늘 종료 + + // when & then + assertThatCode(() -> scheduler.sendDailyAcademicEventNotifications()) + .doesNotThrowAnyException(); + + // then + // 2개 일정 = 2번 발송 + verify(fakeFirebaseAdapter, times(2)).send(any()); + } + + private AcademicEvent createTestAcademicEvent(String summary, LocalDateTime startTime, LocalDateTime endTime) { + AcademicEvent event = AcademicEvent.builder() + .eventUid(UUID.randomUUID().toString()) + .summary(summary) + .description("테스트 이벤트: " + summary) + .category("TEST") + .transparent(Transparent.TRANSPARENT) + .sequence(1) + .notifyEnabled(true) + .startTime(startTime) + .endTime(endTime) + .build(); + + event = academicEventRepository.save(event); + + entityManager.flush(); + entityManager.clear(); + + return event; + } + +} \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java index f57a8226c..c04e69670 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java @@ -3,7 +3,6 @@ import com.kustacks.kuring.calendar.application.port.out.AcademicEventQueryPort; import com.kustacks.kuring.calendar.domain.AcademicEvent; import com.kustacks.kuring.calendar.domain.Transparent; -import com.kustacks.kuring.common.featureflag.FeatureFlags; import com.kustacks.kuring.support.IntegrationTestSupport; import com.kustacks.kuring.worker.scrap.calendar.IcsScraper; import net.fortuna.ical4j.data.CalendarBuilder; @@ -23,7 +22,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -39,9 +37,6 @@ class AcademicEventUpdaterTest extends IntegrationTestSupport { @MockBean private IcsScraper icsScraper; - @MockBean - private FeatureFlags featureFlags; - private final String ORIGINAL_CALENDAR_FILE = "src/test/resources/calendar/academic-calendar-origin.ics"; private final String UPDATED_CALENDAR_FILE = "src/test/resources/calendar/academic-calendar-updated.ics"; @@ -60,7 +55,6 @@ public void setUp() { } catch (IOException | ParserException e) { throw new RuntimeException(e); } - when(featureFlags.isEnabled(any())).thenReturn(true); } @Test diff --git a/src/test/java/com/kustacks/kuring/worker/update/user/UserUpdaterTest.java b/src/test/java/com/kustacks/kuring/worker/update/user/UserUpdaterTest.java index f7121e667..b940987a1 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/user/UserUpdaterTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/user/UserUpdaterTest.java @@ -1,6 +1,5 @@ package com.kustacks.kuring.worker.update.user; -import com.kustacks.kuring.common.featureflag.FeatureFlags; import com.kustacks.kuring.common.featureflag.KuringFeatures; import com.kustacks.kuring.support.IntegrationTestSupport; import com.kustacks.kuring.user.adapter.out.persistence.UserPersistenceAdapter; @@ -10,13 +9,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; class UserUpdaterTest extends IntegrationTestSupport { @@ -29,16 +24,11 @@ class UserUpdaterTest extends IntegrationTestSupport { @Autowired UserUpdater userUpdater; - @MockBean - private FeatureFlags featureFlags; - @Transactional // em.flush(), clear()를 위해 설정 @DisplayName("사용자 질문 카운트가 감소해도 매월 초에 초기값으로 다시 설정된다") @Test void questionCountReset() { // given - when(featureFlags.isEnabled(any())).thenReturn(true); - User savedUser = userPersistenceAdapter.findByToken(USER_FCM_TOKEN).get(); savedUser.decreaseQuestionCount(); // 2 -> 1 savedUser.decreaseQuestionCount(); // 1 -> 0 @@ -59,8 +49,6 @@ void questionCountReset() { @Test void rootUserQuestionCountReset() { // given - when(featureFlags.isEnabled(any())).thenReturn(true); - RootUser savedRootUser = userPersistenceAdapter.findRootUserByEmail(USER_EMAIL).get(); savedRootUser.updateQuestionCount(0); em.flush(); @@ -79,7 +67,7 @@ void rootUserQuestionCountReset() { @Test void rootUserQuestionCountCantReset() { // given - when(featureFlags.isEnabled(eq(KuringFeatures.UPDATE_USER.getFeature()))).thenReturn(false); + featureFlagsSupport.setProperty(KuringFeatures.UPDATE_USER.getFeature().value(), false); RootUser savedRootUser = userPersistenceAdapter.findRootUserByEmail(USER_EMAIL).get(); savedRootUser.updateQuestionCount(0); em.flush(); From bbfd2e6a77748084382ec9811f30af9bbc2d3197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=A7=80=EC=9C=A4?= <155087951+jiyun921@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:23:51 +0900 Subject: [PATCH 09/16] =?UTF-8?q?Fix:=20=ED=95=99=EA=B3=BC=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95,=20=ED=95=99=EA=B3=BC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20api=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#310)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat]: 학과별(학사) 전체 공지 업데이트 테스트 구현 * [refactor]: 변수 이름 graduated로 수정 * [fix]: latestPageGraduateNoticeApiClient 주입 누락 수정 * [feat]: 학과 목록 조회 - graduateSupported 추가 * [refactor]: 학과 목록 조회 - DeptInfo 주입 방식으로 수정 * [feat]: 매체연기학과 추가 구현 * [refactor]: import 수정 * [fix]: 학과 개수 변경 반영 --- .../web/dto/NoticeDepartmentNameResponse.java | 6 ++-- .../in/dto/NoticeDepartmentNameResult.java | 9 ++++-- .../service/NoticeQueryService.java | 16 ++++++---- .../kuring/notice/domain/DepartmentName.java | 3 +- .../architecture/ArchitectureDept.java | 5 ++- .../art_design/IndustrialDesignDept.java | 5 ++- .../deptinfo/art_design/LivingDesignDept.java | 5 ++- .../deptinfo/art_design/MediaActingDept.java | 32 +++++++++++++++++++ .../art_design/MovingImageFilmDept.java | 5 ++- .../education/EducationalTechnologyDept.java | 5 ++- .../education/EnglishEducationDept.java | 5 ++- .../education/PhysicalEducationDept.java | 5 ++- .../deptinfo/engineering/BiologicalDept.java | 5 ++- .../engineering/ChemicalDivisionDept.java | 5 ++- .../engineering/CivilEnvironmentDept.java | 5 ++- .../engineering/ComputerScienceDept.java | 5 ++- .../ElectricalElectronicsDept.java | 5 ++- .../deptinfo/engineering/IndustrialDept.java | 5 ++- .../BioMedicalScienceDept.java | 5 ++- .../ku_integrated_science/CosmeticsDept.java | 5 ++- .../ku_integrated_science/EnergyDept.java | 5 ++- .../deptinfo/liberal_art/ChineseDept.java | 5 ++- .../liberal_art/CultureContentDept.java | 5 ++- .../deptinfo/liberal_art/EnglishDept.java | 5 ++- .../deptinfo/liberal_art/GeologyDept.java | 5 ++- .../deptinfo/liberal_art/HistoryDept.java | 5 ++- .../deptinfo/liberal_art/KoreanDept.java | 5 ++- .../liberal_art/MediaCommunicationDept.java | 5 ++- .../deptinfo/liberal_art/PhilosophyDept.java | 5 ++- .../deptinfo/real_estate/RealEstateDept.java | 5 ++- .../AnimalScienceTechnologyDept.java | 5 ++- .../BiologicalSciencesDept.java | 5 ++- .../FoodMarketingSafetyDept.java | 5 ++- .../deptinfo/science/MathematicsDept.java | 5 ++- .../scrap/deptinfo/science/PhysicsDept.java | 5 ++- .../InternationalTradeDept.java | 5 ++- .../social_science/PoliticalScienceDept.java | 5 ++- .../PublicAdministrationDept.java | 5 ++- .../DepartmentGraduationNoticeUpdater.java | 8 ++--- .../acceptance/CategoryAcceptanceTest.java | 11 +++++-- .../notice/DepartmentNoticeUpdaterTest.java | 20 ++++++++++-- 41 files changed, 216 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MediaActingDept.java diff --git a/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeDepartmentNameResponse.java b/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeDepartmentNameResponse.java index 05cd4e6d1..d4390bdcc 100644 --- a/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeDepartmentNameResponse.java +++ b/src/main/java/com/kustacks/kuring/notice/adapter/in/web/dto/NoticeDepartmentNameResponse.java @@ -5,13 +5,15 @@ public record NoticeDepartmentNameResponse( String name, String hostPrefix, - String korName + String korName, + boolean graduateSupported ) { public static NoticeDepartmentNameResponse from(NoticeDepartmentNameResult result) { return new NoticeDepartmentNameResponse( result.name(), result.hostPrefix(), - result.korName() + result.korName(), + result.graduateSupported() ); } } diff --git a/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeDepartmentNameResult.java b/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeDepartmentNameResult.java index 9dbc7bcda..5a4fe421a 100644 --- a/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeDepartmentNameResult.java +++ b/src/main/java/com/kustacks/kuring/notice/application/port/in/dto/NoticeDepartmentNameResult.java @@ -1,13 +1,16 @@ package com.kustacks.kuring.notice.application.port.in.dto; import com.kustacks.kuring.notice.domain.DepartmentName; +import com.kustacks.kuring.worker.scrap.deptinfo.DeptInfo; public record NoticeDepartmentNameResult( String name, String hostPrefix, - String korName + String korName, + boolean graduateSupported ) { - public static NoticeDepartmentNameResult from(DepartmentName name) { - return new NoticeDepartmentNameResult(name.getName(), name.getHostPrefix(), name.getKorName()); + public static NoticeDepartmentNameResult from(DeptInfo deptInfo) { + DepartmentName name = deptInfo.getDepartmentName(); + return new NoticeDepartmentNameResult(name.getName(), name.getHostPrefix(), name.getKorName(), deptInfo.isSupportGraduateScrap()); } } diff --git a/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java b/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java index 542147719..d93d4ed3a 100644 --- a/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java +++ b/src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java @@ -22,6 +22,7 @@ import com.kustacks.kuring.notice.domain.DepartmentName; import com.kustacks.kuring.user.application.port.out.RootUserQueryPort; import com.kustacks.kuring.user.domain.RootUser; +import com.kustacks.kuring.worker.scrap.deptinfo.DeptInfo; import org.springframework.data.domain.PageRequest; import org.springframework.transaction.annotation.Transactional; @@ -48,18 +49,19 @@ public class NoticeQueryService implements NoticeQueryUseCase, NoticeCommentRead private final CommentQueryPort commentQueryPort; private final RootUserQueryPort rootUserQueryPort; private final List supportedCategoryNameList; - private final List supportedDepartmentNameList; + private final List deptInfoList; public NoticeQueryService( NoticeQueryPort noticeQueryPort, CommentQueryPort commentQueryPort, - RootUserQueryPort rootUserQueryPort + RootUserQueryPort rootUserQueryPort, + List deptInfoList ) { this.noticeQueryPort = noticeQueryPort; this.commentQueryPort = commentQueryPort; this.rootUserQueryPort = rootUserQueryPort; this.supportedCategoryNameList = Arrays.asList(CategoryName.values()); - this.supportedDepartmentNameList = Arrays.asList(DepartmentName.values()); + this.deptInfoList = deptInfoList; } @Override @@ -87,7 +89,7 @@ public List lookupSupportedCategories() { @Override public List lookupSupportedDepartments() { - return convertDepartmentNameDtos(supportedDepartmentNameList); + return convertDepartmentNameDtos(deptInfoList); } @Override @@ -191,9 +193,9 @@ private List getDepartmentNoticeRangeLookup(NoticeRange .toList(); } - private List convertDepartmentNameDtos(List departmentNames) { - return departmentNames.stream() - .filter(dn -> !dn.equals(DepartmentName.COMM_DESIGN)) + private List convertDepartmentNameDtos(List deptInfos) { + return deptInfos.stream() + .filter(dept -> !dept.getDepartmentName().equals(DepartmentName.COMM_DESIGN)) .map(NoticeDepartmentNameResult::from) .toList(); } diff --git a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java index 60321a65b..ebfa8311f 100644 --- a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java +++ b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java @@ -77,6 +77,7 @@ public enum DepartmentName { LIVING_DESIGN("living_design", "livingdesign", "리빙디자인학과"), CONT_ART("contemporary_art", "contemporaryart", "현대미술학과"), MOV_IMAGE("moving_image_film", "movingimages", "영상학과"), + MEDIA_ACTING("media_acting", "mediaacting", "매체연기학과"), JAPANESE_EDU("japanese_education", "japan", "일어교육과"), MATH_EDU("mathematics_education", "mathedu", "수학교육과"), @@ -84,7 +85,7 @@ public enum DepartmentName { MUSIC_EDU("music_education", "music", "음악교육과"), EDU_TECH("education_technology", "edutech", "교육공학과"), ENGLISH_EDU("english_education", "englishedu", "영어교육과"), - EDUCATION("education", "edu", "교육학과"), + EDUCATION("education", "edu", "교직과"), ELE_EDU_CENTER("elective_education_center", "sgedu", "교양교육센터"), VOLUNTEER("volunteer_center", "kuvolunteer", "사회봉사센터"), diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java index 6bceb5695..ea331e766 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/architecture/ArchitectureDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.architecture; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class ArchitectureDept extends ArchitectureCollege { public ArchitectureDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public ArchitectureDept( this.noticeScrapInfo = new NoticeScrapInfo(ARCHITECTURE.getHostPrefix(), 397); this.departmentName = ARCHITECTURE; this.noticeGraduationInfo = new NoticeScrapInfo(ARCHITECTURE.getHostPrefix(), 748); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java index ad08ef9bb..c654df03b 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/IndustrialDesignDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.art_design; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class IndustrialDesignDept extends ArtDesignCollege { public IndustrialDesignDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public IndustrialDesignDept( this.noticeScrapInfo = new NoticeScrapInfo(IND_DESIGN.getHostPrefix(), 4017); this.departmentName = IND_DESIGN; this.noticeGraduationInfo = new NoticeScrapInfo(IND_DESIGN.getHostPrefix(), 5683); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java index 0d74bacfd..ce8933ebe 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/LivingDesignDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.art_design; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -18,7 +19,8 @@ public class LivingDesignDept extends ArtDesignCollege { public LivingDesignDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -30,5 +32,6 @@ public LivingDesignDept( this.noticeScrapInfo = new NoticeScrapInfo(LIVING_DESIGN.getHostPrefix(), 962); this.departmentName = LIVING_DESIGN; this.noticeGraduationInfo = new NoticeScrapInfo(LIVING_DESIGN.getHostPrefix(), 487); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MediaActingDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MediaActingDept.java new file mode 100644 index 000000000..d8fc3e18a --- /dev/null +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MediaActingDept.java @@ -0,0 +1,32 @@ +package com.kustacks.kuring.worker.scrap.deptinfo.art_design; + +import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; +import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; +import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; +import com.kustacks.kuring.worker.scrap.deptinfo.RegisterDepartmentMap; +import com.kustacks.kuring.worker.scrap.deptinfo.StaffScrapInfo; + +import java.util.List; + +import static com.kustacks.kuring.notice.domain.DepartmentName.MEDIA_ACTING; + +@RegisterDepartmentMap(key = MEDIA_ACTING) +public class MediaActingDept extends ArtDesignCollege { + + public MediaActingDept( + LatestPageNoticeApiClient latestPageNoticeApiClient, + LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, + LatestPageNoticeProperties latestPageNoticeProperties + ) { + super(); + this.noticeApiClient = latestPageNoticeApiClient; + this.htmlParser = latestPageNoticeHtmlParser; + this.latestPageNoticeProperties = latestPageNoticeProperties; + + List siteIds = List.of(11299); + this.staffScrapInfo = new StaffScrapInfo(MEDIA_ACTING.getHostPrefix(), siteIds); + this.noticeScrapInfo = new NoticeScrapInfo(MEDIA_ACTING.getHostPrefix(), 493); + this.departmentName = MEDIA_ACTING; + } +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java index 80681242c..29617ac34 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/art_design/MovingImageFilmDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.art_design; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class MovingImageFilmDept extends ArtDesignCollege { public MovingImageFilmDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public MovingImageFilmDept( this.noticeScrapInfo = new NoticeScrapInfo(MOV_IMAGE.getHostPrefix(), 491); this.departmentName = MOV_IMAGE; this.noticeGraduationInfo = new NoticeScrapInfo(MOV_IMAGE.getHostPrefix(), 5710); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java index 2ea220eb9..b81166739 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EducationalTechnologyDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.education; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class EducationalTechnologyDept extends EducationCollege { public EducationalTechnologyDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public EducationalTechnologyDept( this.noticeScrapInfo = new NoticeScrapInfo(EDU_TECH.getHostPrefix(), 4020); this.departmentName = EDU_TECH; this.noticeGraduationInfo = new NoticeScrapInfo(EDU_TECH.getHostPrefix(), 4092); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java index 8ca1a7811..df6d1f28f 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/EnglishEducationDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.education; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class EnglishEducationDept extends EducationCollege { public EnglishEducationDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public EnglishEducationDept( this.noticeScrapInfo = new NoticeScrapInfo(ENGLISH_EDU.getHostPrefix(), 505); this.departmentName = ENGLISH_EDU; this.noticeGraduationInfo = new NoticeScrapInfo(ENGLISH_EDU.getHostPrefix(), 990); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java index 7f07403d3..de7bf0e90 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/education/PhysicalEducationDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.education; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class PhysicalEducationDept extends EducationCollege { public PhysicalEducationDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public PhysicalEducationDept( this.noticeScrapInfo = new NoticeScrapInfo(PHY_EDU.getHostPrefix(), 501); this.departmentName = PHY_EDU; this.noticeGraduationInfo = new NoticeScrapInfo(PHY_EDU.getHostPrefix(), 979); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java index e9dbf5ed3..c001f5aef 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/BiologicalDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.engineering; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class BiologicalDept extends EngineeringCollege { public BiologicalDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public BiologicalDept( this.noticeScrapInfo = new NoticeScrapInfo(BIOLOGICAL.getHostPrefix(), 790); this.departmentName = BIOLOGICAL; this.noticeGraduationInfo = new NoticeScrapInfo(BIOLOGICAL.getHostPrefix(), 417); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java index 62b0c3d42..3c30c9407 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ChemicalDivisionDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.engineering; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class ChemicalDivisionDept extends EngineeringCollege { public ChemicalDivisionDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public ChemicalDivisionDept( this.noticeScrapInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), 409); this.departmentName = CHEMI_DIV; this.noticeGraduationInfo = new NoticeScrapInfo(CHEMI_DIV.getHostPrefix(), 769); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java index 6f1fc6588..47c4d4d9f 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/CivilEnvironmentDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.engineering; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class CivilEnvironmentDept extends EngineeringCollege { public CivilEnvironmentDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public CivilEnvironmentDept( this.noticeScrapInfo = new NoticeScrapInfo(CIVIL_ENV.getHostPrefix(), 401); this.departmentName = CIVIL_ENV; this.noticeGraduationInfo = new NoticeScrapInfo(CIVIL_ENV.getHostPrefix(), 756); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java index 800087074..9a66b9ac3 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ComputerScienceDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.engineering; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class ComputerScienceDept extends EngineeringCollege { public ComputerScienceDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public ComputerScienceDept( this.noticeScrapInfo = new NoticeScrapInfo(COMPUTER.getHostPrefix(), 775); this.departmentName = COMPUTER; this.noticeGraduationInfo = new NoticeScrapInfo(COMPUTER.getHostPrefix(), 411); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java index d741aa8bf..1bb230b61 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/ElectricalElectronicsDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.engineering; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class ElectricalElectronicsDept extends EngineeringCollege { public ElectricalElectronicsDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public ElectricalElectronicsDept( this.noticeScrapInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), 407); this.departmentName = ELEC_ELEC; this.noticeGraduationInfo = new NoticeScrapInfo(ELEC_ELEC.getHostPrefix(), 767); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java index 7be82c118..6205b1cda 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/engineering/IndustrialDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.engineering; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class IndustrialDept extends EngineeringCollege { public IndustrialDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public IndustrialDept( this.noticeScrapInfo = new NoticeScrapInfo(INDUSTRIAL.getHostPrefix(), 413); this.departmentName = INDUSTRIAL; this.noticeGraduationInfo = new NoticeScrapInfo(INDUSTRIAL.getHostPrefix(), 778); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java index dd1a8ec79..f909efd27 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/BioMedicalScienceDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.ku_integrated_science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class BioMedicalScienceDept extends KuIntegratedScienceCollege { public BioMedicalScienceDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public BioMedicalScienceDept( this.noticeScrapInfo = new NoticeScrapInfo(BIO_MEDICAL.getHostPrefix(), 880); this.departmentName = BIO_MEDICAL; this.noticeGraduationInfo = new NoticeScrapInfo(BIO_MEDICAL.getHostPrefix(), 883); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java index 554d41997..42c13baf5 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/CosmeticsDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.ku_integrated_science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class CosmeticsDept extends KuIntegratedScienceCollege { public CosmeticsDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public CosmeticsDept( this.noticeScrapInfo = new NoticeScrapInfo(COSMETICS.getHostPrefix(), 457); this.departmentName = COSMETICS; this.noticeGraduationInfo = new NoticeScrapInfo(COSMETICS.getHostPrefix(), 873); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java index 14dd1b1d6..e46be64ec 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/ku_integrated_science/EnergyDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.ku_integrated_science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class EnergyDept extends KuIntegratedScienceCollege { public EnergyDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public EnergyDept( this.noticeScrapInfo = new NoticeScrapInfo(ENERGY.getHostPrefix(), 451); this.departmentName = ENERGY; this.noticeGraduationInfo = new NoticeScrapInfo(ENERGY.getHostPrefix(), 848); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java index 4e5cce466..e7c365928 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/ChineseDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class ChineseDept extends LiberalArtCollege { public ChineseDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public ChineseDept( this.noticeScrapInfo = new NoticeScrapInfo(CHINESE.getHostPrefix(), 353); this.departmentName = CHINESE; this.noticeGraduationInfo = new NoticeScrapInfo(CHINESE.getHostPrefix(), 709); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java index 92d574bd5..4fe03cd6b 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/CultureContentDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class CultureContentDept extends LiberalArtCollege { public CultureContentDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public CultureContentDept( this.noticeScrapInfo = new NoticeScrapInfo(CULTURE_CONT.getHostPrefix(), 661); this.departmentName = CULTURE_CONT; this.noticeGraduationInfo = new NoticeScrapInfo(CULTURE_CONT.getHostPrefix(), 379); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java index f98c364fa..418a0c4ca 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/EnglishDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class EnglishDept extends LiberalArtCollege { public EnglishDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public EnglishDept( this.noticeScrapInfo = new NoticeScrapInfo(ENGLISH.getHostPrefix(), 347); this.departmentName = ENGLISH; this.noticeGraduationInfo = new NoticeScrapInfo(ENGLISH.getHostPrefix(), 350); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java index a0d1b1436..923b365c3 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/GeologyDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class GeologyDept extends LiberalArtCollege { public GeologyDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public GeologyDept( this.noticeScrapInfo = new NoticeScrapInfo(GEOLOGY.getHostPrefix(), 373); this.departmentName = GEOLOGY; this.noticeGraduationInfo = new NoticeScrapInfo(GEOLOGY.getHostPrefix(), 716); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java index c235971c6..9dcce0886 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/HistoryDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class HistoryDept extends LiberalArtCollege { public HistoryDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public HistoryDept( this.noticeScrapInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), 361); this.departmentName = HISTORY; this.noticeGraduationInfo = new NoticeScrapInfo(HISTORY.getHostPrefix(), 363); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java index 0ddd61b31..2e99b1032 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/KoreanDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class KoreanDept extends LiberalArtCollege { public KoreanDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public KoreanDept( this.noticeScrapInfo = new NoticeScrapInfo(KOREAN.getHostPrefix(), 334); this.departmentName = KOREAN; this.noticeGraduationInfo = new NoticeScrapInfo(KOREAN.getHostPrefix(), 332); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java index b7605c3a5..1bbc9fe5f 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/MediaCommunicationDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class MediaCommunicationDept extends LiberalArtCollege { public MediaCommunicationDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public MediaCommunicationDept( this.noticeScrapInfo = new NoticeScrapInfo(MEDIA_COMM.getHostPrefix(), 375); this.departmentName = MEDIA_COMM; this.noticeGraduationInfo = new NoticeScrapInfo(MEDIA_COMM.getHostPrefix(), 602); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java index b87f6d873..7b879a193 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/liberal_art/PhilosophyDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.liberal_art; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class PhilosophyDept extends LiberalArtCollege { public PhilosophyDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public PhilosophyDept( this.noticeScrapInfo = new NoticeScrapInfo(PHILOSOPHY.getHostPrefix(), 356); this.departmentName = PHILOSOPHY; this.noticeGraduationInfo = new NoticeScrapInfo(PHILOSOPHY.getHostPrefix(), 360); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java index 6a4bfcb14..3669edcf6 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/real_estate/RealEstateDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.real_estate; import com.kustacks.kuring.worker.parser.notice.RealEstateNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.RealEstateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -16,7 +17,8 @@ public class RealEstateDept extends RealEstateCollege { public RealEstateDept(RealEstateNoticeApiClient realEstateNoticeApiClient, RealEstateNoticeHtmlParser realEstateNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties) { + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient) { super(); this.noticeApiClient = realEstateNoticeApiClient; this.htmlParser = realEstateNoticeHtmlParser; @@ -27,5 +29,6 @@ public RealEstateDept(RealEstateNoticeApiClient realEstateNoticeApiClient, this.noticeScrapInfo = new NoticeScrapInfo(REAL_ESTATE.getHostPrefix(), 1563); this.departmentName = REAL_ESTATE; this.noticeGraduationInfo = new NoticeScrapInfo(REAL_ESTATE.getHostPrefix(), 1565); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java index e7b108ab9..9bf0f6299 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/AnimalScienceTechnologyDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.sanghuo_biology; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class AnimalScienceTechnologyDept extends SanghuoBiologyCollege { public AnimalScienceTechnologyDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public AnimalScienceTechnologyDept( this.noticeScrapInfo = new NoticeScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), 914); this.departmentName = ANIMAL_SCIENCE; this.noticeGraduationInfo = new NoticeScrapInfo(ANIMAL_SCIENCE.getHostPrefix(), 469); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java index 5c5219fed..d4df68cbd 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/BiologicalSciencesDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.sanghuo_biology; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -18,7 +19,8 @@ public class BiologicalSciencesDept extends SanghuoBiologyCollege { public BiologicalSciencesDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -30,5 +32,6 @@ public BiologicalSciencesDept( this.noticeScrapInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), 909); this.departmentName = BIO_SCIENCE; this.noticeGraduationInfo = new NoticeScrapInfo(BIO_SCIENCE.getHostPrefix(), 905); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java index 4dbdc41fd..90b08b408 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/sanghuo_biology/FoodMarketingSafetyDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.sanghuo_biology; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class FoodMarketingSafetyDept extends SanghuoBiologyCollege { public FoodMarketingSafetyDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public FoodMarketingSafetyDept( this.noticeScrapInfo = new NoticeScrapInfo(FOOD_MARKETING.getHostPrefix(), 929); this.departmentName = FOOD_MARKETING; this.noticeGraduationInfo = new NoticeScrapInfo(FOOD_MARKETING.getHostPrefix(), 475); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java index 65d1158a8..0e2ab383e 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/MathematicsDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class MathematicsDept extends ScienceCollege { public MathematicsDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public MathematicsDept( this.noticeScrapInfo = new NoticeScrapInfo(MATH.getHostPrefix(), 727); this.departmentName = MATH; this.noticeGraduationInfo = new NoticeScrapInfo(MATH.getHostPrefix(), 391); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java index 2ff07d0d4..3c41218fa 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/science/PhysicsDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -16,7 +17,8 @@ public class PhysicsDept extends ScienceCollege { public PhysicsDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -28,5 +30,6 @@ public PhysicsDept( this.noticeScrapInfo = new NoticeScrapInfo(PHYSICS.getHostPrefix(), 393); this.departmentName = PHYSICS; this.noticeGraduationInfo = new NoticeScrapInfo(PHYSICS.getHostPrefix(), 735); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java index 220cecedb..814e704b1 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/InternationalTradeDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.social_science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class InternationalTradeDept extends SocialSciencesCollege { public InternationalTradeDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public InternationalTradeDept( this.noticeScrapInfo = new NoticeScrapInfo(INT_TRADE.getHostPrefix(), 429); this.departmentName = INT_TRADE; this.noticeGraduationInfo = new NoticeScrapInfo(INT_TRADE.getHostPrefix(), 815); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java index 36be5fa70..c776d696c 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PoliticalScienceDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.social_science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class PoliticalScienceDept extends SocialSciencesCollege { public PoliticalScienceDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public PoliticalScienceDept( this.noticeScrapInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), 803); this.departmentName = POLITICS; this.noticeGraduationInfo = new NoticeScrapInfo(POLITICS.getHostPrefix(), 421); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java index 83a3187d6..f5da91654 100644 --- a/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java +++ b/src/main/java/com/kustacks/kuring/worker/scrap/deptinfo/social_science/PublicAdministrationDept.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.scrap.deptinfo.social_science; import com.kustacks.kuring.worker.parser.notice.LatestPageNoticeHtmlParser; +import com.kustacks.kuring.worker.scrap.client.notice.LatestPageGraduateNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.LatestPageNoticeApiClient; import com.kustacks.kuring.worker.scrap.client.notice.property.LatestPageNoticeProperties; import com.kustacks.kuring.worker.scrap.deptinfo.NoticeScrapInfo; @@ -17,7 +18,8 @@ public class PublicAdministrationDept extends SocialSciencesCollege { public PublicAdministrationDept( LatestPageNoticeApiClient latestPageNoticeApiClient, LatestPageNoticeHtmlParser latestPageNoticeHtmlParser, - LatestPageNoticeProperties latestPageNoticeProperties + LatestPageNoticeProperties latestPageNoticeProperties, + LatestPageGraduateNoticeApiClient latestPageGraduateNoticeApiClient ) { super(); this.noticeApiClient = latestPageNoticeApiClient; @@ -29,5 +31,6 @@ public PublicAdministrationDept( this.noticeScrapInfo = new NoticeScrapInfo(ADMINISTRATION.getHostPrefix(), 855); this.departmentName = ADMINISTRATION; this.noticeGraduationInfo = new NoticeScrapInfo(ADMINISTRATION.getHostPrefix(), 427); + this.latestPageGraduateNoticeApiClient = latestPageGraduateNoticeApiClient; } } diff --git a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java index be7bda345..4b034aaf8 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java @@ -112,8 +112,8 @@ private List compareLatestAndUpdateDB(List saveNewNotices(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduate) { - List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduate); + private List saveNewNotices(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduated) { + List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduated); noticeCommandPort.saveAllDepartmentNotices(newNotices); return newNotices; } @@ -141,8 +141,8 @@ private void compareAllAndUpdateDB(List scrapResults, St } } - private void synchronizationWithDb(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduate) { - List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduate); + private void synchronizationWithDb(List scrapResults, List savedArticleIds, DepartmentName departmentNameEnum, boolean important, boolean graduated) { + List newNotices = noticeUpdateSupport.filteringSoonSaveDepartmentNotices(scrapResults, savedArticleIds, departmentNameEnum, important, graduated); List latestNoticeIds = noticeUpdateSupport.extractDepartmentNoticeIds(scrapResults); diff --git a/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java index 3a0719090..60627b49a 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java @@ -12,7 +12,14 @@ import java.util.List; -import static com.kustacks.kuring.acceptance.CategoryStep.*; +import static com.kustacks.kuring.acceptance.CategoryStep.사용자가_구독한_카테고리_목록_조회_요청; +import static com.kustacks.kuring.acceptance.CategoryStep.지원하는_카테고리_조회_요청; +import static com.kustacks.kuring.acceptance.CategoryStep.카테고리_구독_요청; +import static com.kustacks.kuring.acceptance.CategoryStep.카테고리_구독_요청_응답_확인; +import static com.kustacks.kuring.acceptance.CategoryStep.카테고리_수정_요청; +import static com.kustacks.kuring.acceptance.CategoryStep.카테고리_조회_요청_응답_확인; +import static com.kustacks.kuring.acceptance.CategoryStep.학과_조회_요청; +import static com.kustacks.kuring.acceptance.CategoryStep.학과_조회_응답_확인; import static com.kustacks.kuring.acceptance.CommonStep.실패_응답_확인; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -53,7 +60,7 @@ void look_up_department_list() { var 학과_조회_요청_응답 = 학과_조회_요청(); // then - 학과_조회_응답_확인(학과_조회_요청_응답, 62); + 학과_조회_응답_확인(학과_조회_요청_응답, 63); } /** diff --git a/src/test/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdaterTest.java b/src/test/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdaterTest.java index 4f9a82c8f..35b581135 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdaterTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdaterTest.java @@ -64,7 +64,7 @@ private static List createDepartmentNoticesFixture() { List importantNoticeList = new ArrayList<>(); List normalNoticeList = new ArrayList<>(); - for(int i = 0; i < 30; i++) { + for (int i = 0; i < 30; i++) { CommonNoticeFormatDto importantFormatDto = CommonNoticeFormatDto.builder().articleId(String.valueOf(i)).updatedDate("2021-01-01").subject("important" + i) .postedDate("2021-01-01").fullUrl("https://library.konkuk.ac.kr/library-guide/bulletins/important/71921") .important(true).build(); @@ -79,4 +79,20 @@ private static List createDepartmentNoticesFixture() { result.add(new ComplexNoticeFormatDto(importantNoticeList, normalNoticeList)); return result; } -} + + @DisplayName("학과별(학사) 전체 공지 업데이트 테스트") + @Test + void updateAll_undergraduate_test() throws InterruptedException { + // given + doReturn(createDepartmentNoticesFixture()).when(scrapperTemplate).scrap(any(), any()); + doNothing().when(firebaseService).sendNotifications(anyList()); + + // when + departmentNoticeUpdater.updateAll(); + noticeUpdaterThreadTaskExecutor.getThreadPoolExecutor().awaitTermination(2, TimeUnit.SECONDS); + + // then + Long count = noticeQueryPort.count(); + assertThat(count).isEqualTo(3720); + } +} \ No newline at end of file From f910b061c60f627981a3b4d73545b6ee0e7c37bd Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:43:02 +0900 Subject: [PATCH 10/16] =?UTF-8?q?Feat:=20OCI=20=ED=99=98=EA=B2=BD=20CI/CD?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80=20(#311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: OCI CI/CD 워크플로우 * fix: plain 파일 제외 하도록 수정 --- .github/workflows/dev.yml | 88 ++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 509ea2ba9..ab49efcf8 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,54 +1,64 @@ -# This is a basic workflow to help you get started with Actions +name: Deploy to OCI (Develop) -name: Deploy to develop - -# Controls when the workflow will run on: - # Triggers the workflow on push or pull request events but only for the master branch push: branches: [ develop ] -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on + deploy: + name: Build & Deploy to OCI (Dev) runs-on: ubuntu-latest - environment: Test-Server - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # (1) 코드 체크아웃 + - name: Checkout repository + uses: actions/checkout@v3 - - name: Decrypt Secrets - env: - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - run: | - sh .github/workflows/decrypt.sh + # (2) JDK 17 설치 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' - - name: Ready to deploy + # (3) Gradle 빌드 + - name: Build project run: | - rm .gitignore - git config user.email "todory2002@gmail.com" - git config user.name "ngwoon" - git add . - git commit -m "Build Ready" + ./gradlew clean build -x test - - name: Install Heroku CLI - run: | - curl https://cli-assets.heroku.com/install.sh | sh + # (4) 빌드 결과 확인 + - name: Verify JAR + run: ls -al build/libs - - name: Deploy to Heroku - # You may pin to the exact commit or the version. - # uses: AkhileshNS/heroku-deploy@79ef2ae4ff9b897010907016b268fd0f88561820 - uses: AkhileshNS/heroku-deploy@v3.12.12 + # (5) OCI 서버로 JAR 파일 전송 + - name: Upload to OCI + uses: appleboy/scp-action@v0.1.7 with: - # This will be used for authentication. You can find it in your heroku homepage account settings - heroku_api_key: ${{ secrets.HEROKU_API_KEY }} - # Email that you use with heroku - heroku_email: ${{ secrets.HEROKU_EMAIL }} - # The appname to use for deploying/updating - heroku_app_name: ${{ secrets.HEROKU_APP_NAME }} - env: - HD_DEPLOY_ENV: "dev" + host: ${{ secrets.OCI_DEV_HOST }} + username: ubuntu + key: ${{ secrets.OCI_DEV_SSH_KEY }} + source: "build/libs/*.jar" + target: "/home/ubuntu/dev-app" + + # (6) SSH 접속 → 기존 프로세스 중지 → 새 버전 실행 + - name: Restart Dev Application + uses: appleboy/ssh-action@v1.1.0 + with: + host: ${{ secrets.OCI_DEV_HOST }} + username: ubuntu + key: ${{ secrets.OCI_DEV_SSH_KEY }} + script: | + echo "[1] 현재 실행중인 개발 서버 종료 중..." + PID=$(ps -ef | grep java | grep 'dev-app' | awk '{print $2}') + if [ -n "$PID" ]; then + echo "기존 프로세스 종료: $PID" + kill -9 $PID + else + echo "기존 Java 프로세스 없음" + fi + + echo "[2] 새 JAR 실행" + cd /home/ubuntu/dev-app + JAR_FILE=$(ls -t build/libs/*.jar | grep -m1 -v 'plain') + nohup java -jar $JAR_FILE --spring.profiles.active=dev > dev-app.log 2>&1 & + echo "✅ 개발 서버 재시작 완료!" \ No newline at end of file From 9bb94a977a22f0d528fa64dc60a88bc52430ed5c Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:02:38 +0900 Subject: [PATCH 11/16] =?UTF-8?q?fix:=20dev=20=ED=99=98=EA=B2=BD=20ci/cd?= =?UTF-8?q?=20workflow=20=EC=88=98=EC=A0=95(decrpt=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20sh=20=EB=AA=85=EB=A0=B9=EC=96=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95)=20(#312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index ab49efcf8..07ba30200 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -21,16 +21,21 @@ jobs: distribution: 'temurin' java-version: '17' - # (3) Gradle 빌드 + # (3) firebase secret decrypt + - name: Decrypt Secrets + run: | + sh .github/workflows/decrypt.sh + + # (4) Gradle 빌드 - name: Build project run: | ./gradlew clean build -x test - # (4) 빌드 결과 확인 + # (5) 빌드 결과 확인 - name: Verify JAR run: ls -al build/libs - # (5) OCI 서버로 JAR 파일 전송 + # (6) OCI 서버로 JAR 파일 전송 - name: Upload to OCI uses: appleboy/scp-action@v0.1.7 with: @@ -40,7 +45,7 @@ jobs: source: "build/libs/*.jar" target: "/home/ubuntu/dev-app" - # (6) SSH 접속 → 기존 프로세스 중지 → 새 버전 실행 + # (7) SSH 접속 → 기존 프로세스 중지 → 새 버전 실행 - name: Restart Dev Application uses: appleboy/ssh-action@v1.1.0 with: @@ -49,7 +54,7 @@ jobs: key: ${{ secrets.OCI_DEV_SSH_KEY }} script: | echo "[1] 현재 실행중인 개발 서버 종료 중..." - PID=$(ps -ef | grep java | grep 'dev-app' | awk '{print $2}') + PID=$(ps -ef | grep 'kuring-.*\.jar' | grep -v grep | awk '{print $2}') if [ -n "$PID" ]; then echo "기존 프로세스 종료: $PID" kill -9 $PID @@ -60,5 +65,5 @@ jobs: echo "[2] 새 JAR 실행" cd /home/ubuntu/dev-app JAR_FILE=$(ls -t build/libs/*.jar | grep -m1 -v 'plain') - nohup java -jar $JAR_FILE --spring.profiles.active=dev > dev-app.log 2>&1 & + nohup java -jar "$JAR_FILE" --spring.profiles.active=dev > /home/ubuntu/dev-app.log 2>&1 & echo "✅ 개발 서버 재시작 완료!" \ No newline at end of file From 9e9e8a285e0ed1d119348424f1a4daff9572438b Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:16:17 +0900 Subject: [PATCH 12/16] =?UTF-8?q?feat:=20decrpt=20secrets=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=8B=9C=20env=20=EC=B6=94=EA=B0=80=20(#313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 07ba30200..1193c0528 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -23,6 +23,8 @@ jobs: # (3) firebase secret decrypt - name: Decrypt Secrets + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | sh .github/workflows/decrypt.sh From 485d77a25b8926d4d7d638a1ff3287a1d4279c5c Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:34:09 +0900 Subject: [PATCH 13/16] =?UTF-8?q?Feat:=20=ED=95=99=EC=82=AC=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A0=84=EC=86=A1=20=EB=B6=84=EB=A5=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 생성자 추가 --- .../AcademicEventJdbcRepository.java | 4 +- .../AcademicEventQueryRepositoryImpl.java | 2 +- .../AcademicEventNotificationService.java | 2 + .../kuring/calendar/domain/AcademicEvent.java | 10 +- .../domain/AcademicEventCategory.java | 19 ++ .../AcademicEventNotificationScheduler.java | 4 +- .../calendar/AcademicEventCategorizer.java | 61 ++++++ .../calendar/AcademicEventConverter.java | 55 +++-- .../calendar/AcademicEventDbSynchronizer.java | 2 +- .../AcademicEventNotificationClassifier.java | 73 +++++++ .../AcademicEventSummaryNormalizer.java | 97 +++++++++ .../calendar/domain/AcademicEventTest.java | 8 +- .../kuring/support/DatabaseConfigurator.java | 13 +- ...cademicEventNotificationSchedulerTest.java | 3 +- .../AcademicEventCategorizerTest.java | 83 ++++++++ .../calendar/AcademicEventConverterTest.java | 129 +++++++++++- .../AcademicEventDbSynchronizerTest.java | 5 +- ...ademicEventNotificationClassifierTest.java | 193 ++++++++++++++++++ .../AcademicEventSummaryNormalizerTest.java | 94 +++++++++ .../calendar/AcademicEventUpdaterTest.java | 12 +- 20 files changed, 820 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/kustacks/kuring/calendar/domain/AcademicEventCategory.java create mode 100644 src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizer.java create mode 100644 src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifier.java create mode 100644 src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizer.java create mode 100644 src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizerTest.java create mode 100644 src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifierTest.java create mode 100644 src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizerTest.java diff --git a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventJdbcRepository.java b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventJdbcRepository.java index b9c75c8ab..cda40eb45 100644 --- a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventJdbcRepository.java +++ b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventJdbcRepository.java @@ -35,7 +35,7 @@ public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, academicEvent.getEventUid()); ps.setString(2, academicEvent.getSummary()); ps.setString(3, academicEvent.getDescription()); - ps.setString(4, academicEvent.getCategory()); + ps.setString(4, academicEvent.getCategory().name()); ps.setString(5, academicEvent.getTransparent().toString()); ps.setInt(6, academicEvent.getSequence()); ps.setBoolean(7, academicEvent.getNotifyEnabled()); @@ -68,7 +68,7 @@ public void setValues(PreparedStatement ps, int i) throws SQLException { AcademicEvent academicEvent = events.get(i); ps.setString(1, academicEvent.getSummary()); ps.setString(2, academicEvent.getDescription()); - ps.setString(3, academicEvent.getCategory()); + ps.setString(3, academicEvent.getCategory().name()); ps.setString(4, academicEvent.getTransparent().toString()); ps.setInt(5, academicEvent.getSequence()); ps.setBoolean(6, academicEvent.getNotifyEnabled()); diff --git a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java index 141fe9412..d98d8f735 100644 --- a/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java +++ b/src/main/java/com/kustacks/kuring/calendar/adapter/out/persistence/AcademicEventQueryRepositoryImpl.java @@ -57,7 +57,7 @@ private JPAQuery queryFactorySelectingReadModel() { academicEvent.eventUid, academicEvent.summary, academicEvent.description, - academicEvent.category, + academicEvent.category.stringValue(), academicEvent.transparent, academicEvent.sequence, academicEvent.notifyEnabled, diff --git a/src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java b/src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java index 4d3cc800f..454c164eb 100644 --- a/src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java +++ b/src/main/java/com/kustacks/kuring/calendar/application/service/AcademicEventNotificationService.java @@ -97,6 +97,8 @@ private Message makeMessage(String title, String body) { .setBody(body) .build()) .setTopic(serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC)) + .putData("title", title) + .putData("body", body) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/calendar/domain/AcademicEvent.java b/src/main/java/com/kustacks/kuring/calendar/domain/AcademicEvent.java index 7a5149a44..fed5f96b2 100644 --- a/src/main/java/com/kustacks/kuring/calendar/domain/AcademicEvent.java +++ b/src/main/java/com/kustacks/kuring/calendar/domain/AcademicEvent.java @@ -36,9 +36,9 @@ public class AcademicEvent extends BaseTimeEntity { @Column(name = "description", columnDefinition = "TEXT") private String description; // 이벤트 설명 - //TODO: 카테고리 확정 시 ENUM화 필요 @김한주 25.08.24 - @Column(name = "category", length = 20) - private String category; // 일정 분류 (예: 학사, 시험, 휴강 등) + @Enumerated(EnumType.STRING) + @Column(name = "category", length = 30) + private AcademicEventCategory category; // 일정 분류 @Enumerated(EnumType.STRING) @Column(name = "transparent", nullable = false, length = 20) @@ -57,7 +57,7 @@ public class AcademicEvent extends BaseTimeEntity { private LocalDateTime endTime; // 종료일시 @Builder - private AcademicEvent(String eventUid, String summary, String description, String category, + private AcademicEvent(String eventUid, String summary, String description, AcademicEventCategory category, Transparent transparent, Integer sequence, Boolean notifyEnabled, LocalDateTime startTime, LocalDateTime endTime) { this.eventUid = eventUid; @@ -71,7 +71,7 @@ private AcademicEvent(String eventUid, String summary, String description, Strin this.endTime = endTime; } - public static AcademicEvent from(String uid, String summary, String description, String category, + public static AcademicEvent from(String uid, String summary, String description, AcademicEventCategory category, Transparent transparent, Integer sequence, Boolean notifyEnabled, LocalDateTime startTime, LocalDateTime endTime) { Assert.notNull(uid, "UID must not be null"); diff --git a/src/main/java/com/kustacks/kuring/calendar/domain/AcademicEventCategory.java b/src/main/java/com/kustacks/kuring/calendar/domain/AcademicEventCategory.java new file mode 100644 index 000000000..65a2245fe --- /dev/null +++ b/src/main/java/com/kustacks/kuring/calendar/domain/AcademicEventCategory.java @@ -0,0 +1,19 @@ +package com.kustacks.kuring.calendar.domain; + +public enum AcademicEventCategory { + + ACADEMIC_DEGREE("학적/학위"), + REGISTRATION_COURSE_GRADE("등록/수강/성적"), + ACADEMIC_OPERATION_EVENT("학사운영/행사"), + ETC("기타"); + + private final String displayName; + + AcademicEventCategory(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java b/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java index 26ebca21c..69bd66abc 100644 --- a/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java +++ b/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java @@ -16,8 +16,8 @@ public class AcademicEventNotificationScheduler { private final AcademicEventNotificationUseCase academicEventNotificationUseCase; private final FeatureFlags featureFlags; - // 매일 오전 9시 학사일정 알림 전송 - @Scheduled(cron = "0 0 9 * * *", zone = "Asia/Seoul") + // 매주 월요일 새벽 5시 학사일정 알림 전송 + @Scheduled(cron = "0 0 5 * * 1", zone = "Asia/Seoul") public void sendDailyAcademicEventNotifications() { if (featureFlags.isEnabled(KuringFeatures.NOTIFY_ACADEMIC_EVENT.getFeature())) { log.info("******** 일일 학사일정 알림 발송 시작 ********"); diff --git a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizer.java b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizer.java new file mode 100644 index 000000000..1fef8b886 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizer.java @@ -0,0 +1,61 @@ +package com.kustacks.kuring.worker.update.calendar; + +import com.kustacks.kuring.calendar.domain.AcademicEventCategory; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +import static com.kustacks.kuring.calendar.domain.AcademicEventCategory.ACADEMIC_DEGREE; +import static com.kustacks.kuring.calendar.domain.AcademicEventCategory.ACADEMIC_OPERATION_EVENT; +import static com.kustacks.kuring.calendar.domain.AcademicEventCategory.REGISTRATION_COURSE_GRADE; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class AcademicEventCategorizer { + + private static final Map> CATEGORY_KEYWORDS = Map.of( + ACADEMIC_DEGREE, List.of( + "휴학", "복학", "재입학", "졸업유예", "조기졸업", "졸업논문", "논문입력", "예비사정", + "전과", "다부전공", "교직다전공", "교직이수", "교직무시험", "검정원서", "교직이수예정자" + ), + REGISTRATION_COURSE_GRADE, List.of( + "수강정정", "초과과목", "수강신청", "바구니", "자동신청", "시간표조회", "수강신청확인서", + "등록", "등록개시", "등록금", "납입", "반환", "취득학점포기", "성적확인", "성적이의신청", + "최종성적", "성적등재", "성적조회", "성적표", "장학", "학비감면", "서류제출", + "폐강교과목", "폐강대상자", "환불", "강의시간표" + ), + ACADEMIC_OPERATION_EVENT, List.of( + "제적대상자", "제적처리", "재적생변동", "중간고사", "기말고사", "성적입력오픈", + "개강", "방학", "하계", "동계", "계절학기", "종강", "공휴일", "추석", "한글날", "성탄절", "신정", "삼일절", + "학위수여식", "졸업대상자선정", "졸업사정회", "졸업확정처리", "워크숍", + "예술제", "축전", "일감호", "학원창립", "개교기념일", "기념일" + ) + ); + + public static AcademicEventCategory categorize(String summary) { + if (summary == null || summary.isBlank()) { + return AcademicEventCategory.ETC; + } + + String normalizedSummary = summary.trim(); + + if (containsKeywords(normalizedSummary, CATEGORY_KEYWORDS.get(ACADEMIC_DEGREE))) { + return ACADEMIC_DEGREE; + } + + if (containsKeywords(normalizedSummary, CATEGORY_KEYWORDS.get(REGISTRATION_COURSE_GRADE))) { + return REGISTRATION_COURSE_GRADE; + } + + if (containsKeywords(normalizedSummary, CATEGORY_KEYWORDS.get(ACADEMIC_OPERATION_EVENT))) { + return ACADEMIC_OPERATION_EVENT; + } + + return AcademicEventCategory.ETC; + } + + private static boolean containsKeywords(String text, List keywords) { + return keywords.stream() + .anyMatch(text::contains); + } +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverter.java b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverter.java index 8ebbdb74b..7677ac4a0 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverter.java +++ b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverter.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.worker.update.calendar; import com.kustacks.kuring.calendar.domain.AcademicEvent; +import com.kustacks.kuring.calendar.domain.AcademicEventCategory; import com.kustacks.kuring.calendar.domain.Transparent; import com.kustacks.kuring.common.utils.converter.StringToDateTimeConverter; import com.kustacks.kuring.worker.parser.calendar.dto.IcsEvent; @@ -10,6 +11,8 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; import static com.kustacks.kuring.calendar.domain.Transparent.OPAQUE; @@ -17,34 +20,46 @@ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class AcademicEventConverter { + // 공휴일 관련 키워드 패턴 (ReDoS 방지) + private static final Pattern HOLIDAY_PATTERN = Pattern.compile("[^\\r\\n]*공휴일[^\\r\\n]*"); + public static List convertToAcademicEvents(List icsEvents) { return icsEvents.stream() .map(AcademicEventConverter::convertToAcademicEvent) - .filter(Objects::nonNull) + .filter(Optional::isPresent) + .map(Optional::get) .toList(); } - public static AcademicEvent convertToAcademicEvent(IcsEvent icsEvent) { + public static Optional convertToAcademicEvent(IcsEvent icsEvent) { String uid = parseString(icsEvent.uid()); - String summary = parseString(icsEvent.summary()); + String rawSummary = parseString(icsEvent.summary()); String description = parseString(icsEvent.description()); + // 1. 학사일정 변환 가능 여부 확인 (공휴일 제외) + if (!shouldConvertToAcademicEvent(rawSummary)) { + return Optional.empty(); + } + + // 2. summary 전처리 (괄호 안 날짜/시간 제거 등) + String summary = AcademicEventSummaryNormalizer.normalize(rawSummary); LocalDateTime startTime = StringToDateTimeConverter.convert(icsEvent.dtstart()); LocalDateTime endTime = StringToDateTimeConverter.convert(icsEvent.dtend()); - //초기 기타로 통일, 별도 분류 필요 25.08.26 김한주 - String category = "ETC"; + AcademicEventCategory category = AcademicEventCategorizer.categorize(summary); Transparent transparent = convertToTransparent(icsEvent.transp()); Integer sequence = convertToSequence(icsEvent.sequence()); - boolean notifyEnabled = determineNotifyEnabled(transparent); + boolean notifyEnabled = AcademicEventNotificationClassifier.proceed(transparent, summary); try { - return AcademicEvent.from(uid, summary, description, - category, transparent, sequence, notifyEnabled, startTime, endTime); + return Optional.of( + AcademicEvent.from(uid, summary, description, category, + transparent, sequence, notifyEnabled, startTime, endTime) + ); } catch (Exception e) { - log.warn("ICS event 변좐에 실패했습니다.(uid={}, summary={}): {}", uid, summary, e.toString()); - return null; + log.warn("ICS event 변환에 실패했습니다.(uid={}, summary={}): {}", uid, summary, e.toString()); + return Optional.empty(); } } @@ -55,6 +70,7 @@ private static String parseString(String string) { return string.trim(); } + private static Transparent convertToTransparent(String transp) { if (Objects.isNull(transp)) { return OPAQUE; // 기본값: 바쁨 @@ -74,15 +90,20 @@ private static Integer convertToSequence(String sequence) { } } - private static boolean determineNotifyEnabled(Transparent transparent) { - if (Objects.isNull(transparent)) { + /** + * summary가 학사일정으로 변환 가능한지 필터링 + */ + private static boolean shouldConvertToAcademicEvent(String summary) { + if (summary == null || summary.isBlank()) { return false; } - return switch (transparent) { - case OPAQUE -> true; // 중요한 일정은 알림 활성화 - case TRANSPARENT -> false; // 덜 중요한 일정은 알림 비활성화 - default -> false; - }; + // 공휴일 관련 이벤트는 제외 + return !isHolidayEvent(summary); + } + + private static boolean isHolidayEvent(String summary) { + return HOLIDAY_PATTERN.matcher(summary).matches(); } + } \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizer.java b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizer.java index 40e3e3d7c..629281c16 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizer.java +++ b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizer.java @@ -40,7 +40,7 @@ public void compareAndUpdateDb(IcsCalendarResult result) { // 4. 저장, 업데이트 saveAndUpdate(eventUpdateResult); - log.debug("신규 이벤트 {} 개, 업데이트 이벤트 {} 개", eventUpdateResult.newEvents().size(), eventUpdateResult.updatedEvents().size()); + log.info("신규 이벤트 {} 개, 업데이트 이벤트 {} 개", eventUpdateResult.newEvents().size(), eventUpdateResult.updatedEvents().size()); } private void saveAndUpdate(EventUpdateResult eventUpdateResult) { diff --git a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifier.java b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifier.java new file mode 100644 index 000000000..a5893b57f --- /dev/null +++ b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifier.java @@ -0,0 +1,73 @@ +package com.kustacks.kuring.worker.update.calendar; + +import com.kustacks.kuring.calendar.domain.Transparent; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Objects; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class AcademicEventNotificationClassifier { + + // 학생이 행정적 액션을 해야하는 일정들 (알림 필수 키워드) + private static final List ACTION_REQUIRED_KEYWORDS = List.of( + // 수강 관련 + "수강정정", "초과과목신청", "수강신청", "바구니", "자동신청", "시간표조회", "수강신청확인서", + + // 등록 관련 + "등록", "등록개시", "등록마감", "등록금", "납입", "반환", "환불마감", + + // 학적 관련 + "휴학신청", "복학신청", "재입학접수", "졸업유예접수", "조기졸업", "졸업논문입력", "예비사정", + "전과신청", "다부전공신청", "다부전공포기", "교직다전공", "교직이수", "교직무시험", "검정원서접수", + + // 성적 관련 + "성적확인", "성적이의신청", "최종성적이의신청", "성적등재", "성적조회", "성적증명서", "성적표", "취득학점포기", + + // 장학 관련 + "장학신청", "학비감면", "서류제출", + + // 폐강 관련 + "폐강교과목공지", "폐강대상자", + + // 일반 액션 키워드 + "신청", "접수", "제출", "마감", "포기", "확인", "조회", "출력" + ); + + public static boolean proceed(Transparent transparent, String summary) { + return isNotifiableByTransparent(transparent) || isNotifiableBySummary(summary); + } + + /** + * Transparent 속성으로 알림 필요 여부 판단 + * OPAQUE = 중요일정 → 알림 발송 + * TRANSPARENT = 미중요일정 → 알림 미발송 + */ + private static boolean isNotifiableByTransparent(Transparent transparent) { + if (Objects.isNull(transparent)) { + return false; + } + + return switch (transparent) { + case OPAQUE -> true; + case TRANSPARENT -> false; + }; + } + + /** + * summary 내용으로 알림 필요 여부 판단 + */ + private static boolean isNotifiableBySummary(String summary) { + if (summary == null || summary.isBlank()) { + return false; + } + + String normalizedSummary = summary.trim(); + return containsActionRequiredKeywords(normalizedSummary); + } + + private static boolean containsActionRequiredKeywords(String text) { + return ACTION_REQUIRED_KEYWORDS.stream() + .anyMatch(text::contains); + } +} \ No newline at end of file diff --git a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizer.java b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizer.java new file mode 100644 index 000000000..378fed7c4 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizer.java @@ -0,0 +1,97 @@ +package com.kustacks.kuring.worker.update.calendar; + +import lombok.NoArgsConstructor; + +import java.util.regex.Pattern; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class AcademicEventSummaryNormalizer { + + // 괄호 안에 시간/날짜 정보가 있는 패턴 + // 예: (2. 25. 화 9:30 ~ 2. 26 수 17:00), (9:30 ~) + // 1단계: 모든 괄호 찾기 + private static final Pattern BRACKET_PATTERN = Pattern.compile("\\([^)]+\\)"); + // 2단계: 괄호 내용에서 시간/날짜 확인 + private static final Pattern TIME_IN_BRACKET = Pattern.compile("\\d{1,4}[:.]|\\d{1,4}\\s{0,3}[월화수목금토일]"); + + // 괄호 없는 날짜/시간 패턴 제거 (ReDoS 방지) + // 예: 14.(월) 10:30 ~ 15.(화) 16:30, ~ 5.(월) 16:30 + private static final Pattern DATE_TIME_PATTERN = Pattern.compile("\\s{0,5}~?\\s{0,5}\\d{1,2}\\.\\s{0,5}\\([월화수목금토일]\\)\\s{0,5}\\d{1,2}[:.]\\d{1,2}.{0,200}$"); + + // 단순 시간 패턴 제거 (HH:MM 형식) + // 예: 17:00 + private static final Pattern SIMPLE_TIME_PATTERN = Pattern.compile("\\s+\\d{1,2}:\\d{2}\\s*$"); + + // ※ 기호 이후의 모든 텍스트 제거 패턴 (줄바꿈 전까지) + private static final Pattern REFERENCE_MARK_PATTERN = Pattern.compile("※[^\\r\\n]*$"); + + public static String normalize(String summary) { + if (summary == null || summary.isBlank()) { + return null; + } + + String processed = summary.trim(); + + // 1. 괄호 안 날짜/시간 정보 제거 + processed = removeDateTimeBrackets(processed); + + // 2. 괄호 없는 직접 날짜/시간 정보 제거 + processed = removeDateTime(processed); + + // 3. 단순 시간 정보 제거 (HH:MM) + processed = removeSimpleTime(processed); + + // 4. ※ 기호 이후 모든 텍스트 제거 + processed = removeTextAfterSymbol(processed); + + // 최종 trim + processed = processed.trim(); + + return processed.isEmpty() ? null : processed; + } + + /** + * 괄호 안의 날짜/시간 정보 제거 (차수 정보는 유지) + * 예: "전체학년 수강신청 (2. 25. 화 9:30 ~ 2. 26 수 17:00)" → "전체학년 수강신청" + * 예: "강의시간표조회 (9:30 ~)" → "강의시간표조회" + * 예: "취득학점포기(3차)" → "취득학점포기(3차)" (차수 정보 유지) + */ + private static String removeDateTimeBrackets(String text) { + return BRACKET_PATTERN.matcher(text).replaceAll(matchResult -> { + String bracketContent = matchResult.group(); + String innerContent = bracketContent.substring(1, bracketContent.length() - 1); + + // 괄호 안에 시간/날짜 정보가 있으면 전체 괄호를 제거 + if (TIME_IN_BRACKET.matcher(innerContent).find()) { + return ""; // 괄호와 내용 모두 제거 + } + + return bracketContent; // 시간/날짜 정보가 없으면 괄호 유지 (차수 정보 등) + }).trim(); + } + + /** + * 괄호 없는 직접 날짜/시간 정보 제거 (~ 기호 포함) + * 예: "취득학점포기(3차) 14.(월) 10:30 ~ 15.(화) 16:30" → "취득학점포기(3차)" + * 예: "최정성적이의신청 ~ 5.(월) 16:30" → "최정성적이의신청" + */ + private static String removeDateTime(String text) { + return DATE_TIME_PATTERN.matcher(text).replaceAll("").trim(); + } + + /** + * 단순 시간 정보 제거 (HH:MM 형식) + * 예: "복학신청 마감 17:00" → "복학신청 마감" + */ + private static String removeSimpleTime(String text) { + return SIMPLE_TIME_PATTERN.matcher(text).replaceAll("").trim(); + } + + /** + * ※ 기호 이후의 모든 텍스트 제거 + * 예: "취득학점포기(3차)※주말(토,일)미포함" → "취득학점포기(3차)" + */ + private static String removeTextAfterSymbol(String text) { + return REFERENCE_MARK_PATTERN.matcher(text).replaceAll("").trim(); + } +} \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java b/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java index f27419264..0fd813486 100644 --- a/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java +++ b/src/test/java/com/kustacks/kuring/calendar/domain/AcademicEventTest.java @@ -17,7 +17,7 @@ class AcademicEventTest { private String uid = UUID.randomUUID().toString(); private String summary = "개강"; private String description = "2025년 1학기 개강"; - private String category = "ETC"; + private AcademicEventCategory category = AcademicEventCategory.ETC; private Transparent transparent = Transparent.TRANSPARENT; private Integer sequence = 1; private Boolean notifyEnabled = true; @@ -64,7 +64,7 @@ void domain_update_method_test() { .eventUid("test-uid") .summary("기존 제목") .description("기존 설명") - .category("기존 카테고리") + .category(AcademicEventCategory.ETC) .transparent(Transparent.OPAQUE) .sequence(0) .notifyEnabled(true) @@ -76,7 +76,7 @@ void domain_update_method_test() { .eventUid("test-uid") .summary("새로운 제목") .description("새로운 설명") - .category("새로운 카테고리") + .category(AcademicEventCategory.ACADEMIC_DEGREE) .transparent(Transparent.TRANSPARENT) .sequence(1) .notifyEnabled(false) @@ -90,7 +90,7 @@ void domain_update_method_test() { // then assertThat(existingEvent.getSummary()).isEqualTo("새로운 제목"); assertThat(existingEvent.getDescription()).isEqualTo("새로운 설명"); - assertThat(existingEvent.getCategory()).isEqualTo("새로운 카테고리"); + assertThat(existingEvent.getCategory()).isEqualTo(AcademicEventCategory.ACADEMIC_DEGREE); assertThat(existingEvent.getTransparent()).isEqualTo(Transparent.TRANSPARENT); assertThat(existingEvent.getSequence()).isEqualTo(1); assertThat(existingEvent.getNotifyEnabled()).isFalse(); diff --git a/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java b/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java index a8384d220..9872aa84d 100644 --- a/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java +++ b/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java @@ -5,6 +5,7 @@ import com.kustacks.kuring.admin.domain.AdminRole; import com.kustacks.kuring.calendar.adapter.out.persistence.AcademicEventRepository; import com.kustacks.kuring.calendar.domain.AcademicEvent; +import com.kustacks.kuring.calendar.domain.AcademicEventCategory; import com.kustacks.kuring.calendar.domain.Transparent; import com.kustacks.kuring.email.adapter.out.persistence.VerificationCodeRepository; import com.kustacks.kuring.email.domain.VerificationCode; @@ -294,11 +295,11 @@ private void initAcademicEvents() { private List buildAcademicEvents() { String[][] eventData = { - {"2월 말 행사", "2월 28일 종료되는 행사", "ACADEMIC", "TRANSPARENT", "2025-02-28T10:00", "2025-02-28T11:00"}, - {"3월 1일 개강", "3월 1일 정확히 시작하는 개강", "ACADEMIC", "TRANSPARENT", "2025-03-01T09:00", "2025-03-01T10:00"}, - {"3월 15일 중간 행사", "3월 중순 행사", "EVENT", "OPAQUE", "2025-03-15T14:00", "2025-03-15T15:00"}, - {"3월 31일 마감", "3월 마지막 날 정확히 끝나는 행사", "DEADLINE", "OPAQUE", "2025-03-31T23:00", "2025-03-31T23:59"}, - {"4월 1일 마지막 행사", "4월 1일 마지막", "ACADEMIC", "TRANSPARENT", "2025-04-01T00:00", "2025-04-01T01:00"} + {"2월 말 행사", "2월 28일 종료되는 행사", "ACADEMIC_DEGREE", "TRANSPARENT", "2025-02-28T10:00", "2025-02-28T11:00"}, + {"3월 1일 개강", "3월 1일 정확히 시작하는 개강", "ACADEMIC_DEGREE", "TRANSPARENT", "2025-03-01T09:00", "2025-03-01T10:00"}, + {"3월 15일 중간 행사", "3월 중순 행사", "REGISTRATION_COURSE_GRADE", "OPAQUE", "2025-03-15T14:00", "2025-03-15T15:00"}, + {"3월 31일 마감", "3월 마지막 날 정확히 끝나는 행사", "ACADEMIC_OPERATION_EVENT", "OPAQUE", "2025-03-31T23:00", "2025-03-31T23:59"}, + {"4월 1일 마지막 행사", "4월 1일 마지막", "ETC", "TRANSPARENT", "2025-04-01T00:00", "2025-04-01T01:00"} }; return Stream.iterate(0, i -> i + 1) @@ -309,7 +310,7 @@ private List buildAcademicEvents() { .eventUid(UUID.randomUUID().toString()) .summary(data[0]) .description(data[1]) - .category(data[2]) + .category(AcademicEventCategory.valueOf(data[2])) .transparent(Transparent.valueOf(data[3])) .sequence(1) .notifyEnabled(true) diff --git a/src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java b/src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java index e55bc996a..d0ee89d52 100644 --- a/src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java +++ b/src/test/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationSchedulerTest.java @@ -4,6 +4,7 @@ import com.kustacks.kuring.calendar.adapter.out.persistence.AcademicEventRepository; import com.kustacks.kuring.calendar.application.port.in.AcademicEventNotificationUseCase; import com.kustacks.kuring.calendar.domain.AcademicEvent; +import com.kustacks.kuring.calendar.domain.AcademicEventCategory; import com.kustacks.kuring.calendar.domain.Transparent; import com.kustacks.kuring.common.featureflag.KuringFeatures; import com.kustacks.kuring.message.adapter.out.firebase.FakeFirebaseAdapter; @@ -160,7 +161,7 @@ private AcademicEvent createTestAcademicEvent(String summary, LocalDateTime star .eventUid(UUID.randomUUID().toString()) .summary(summary) .description("테스트 이벤트: " + summary) - .category("TEST") + .category(AcademicEventCategory.ETC) .transparent(Transparent.TRANSPARENT) .sequence(1) .notifyEnabled(true) diff --git a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizerTest.java b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizerTest.java new file mode 100644 index 000000000..53a9b2c8d --- /dev/null +++ b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventCategorizerTest.java @@ -0,0 +1,83 @@ +package com.kustacks.kuring.worker.update.calendar; + +import com.kustacks.kuring.calendar.domain.AcademicEventCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("AcademicEventCategorizer 단위 테스트") +class AcademicEventCategorizerTest { + + @DisplayName("학적/학위 카테고리 분류") + @ParameterizedTest + @ValueSource(strings = { + "휴학신청", "복학신청", "재입학접수", "졸업유예접수", "조기졸업", "졸업논문입력", + "전과신청", "다부전공신청", "교직다전공", "교직이수예정자", "교직무시험검정원서" + }) + void categorize_academic_degree_keywords(String summary) { + // when + AcademicEventCategory result = AcademicEventCategorizer.categorize(summary); + + // then + assertThat(result).isEqualTo(AcademicEventCategory.ACADEMIC_DEGREE); + } + + @DisplayName("등록/수강/성적 카테고리 분류") + @ParameterizedTest + @ValueSource(strings = { + "수강정정", "수강신청", "등록금납입", "취득학점포기", "성적이의신청", + "장학금신청", "폐강교과목공지", "환불마감", "강의시간표조회" + }) + void categorize_registration_course_grade_keywords(String summary) { + // when + AcademicEventCategory result = AcademicEventCategorizer.categorize(summary); + + // then + assertThat(result).isEqualTo(AcademicEventCategory.REGISTRATION_COURSE_GRADE); + } + + @DisplayName("학사 운영/행사 카테고리 분류") + @ParameterizedTest + @ValueSource(strings = { + "중간고사", "기말고사", "개강", "하계방학", "동계방학", "학위수여식", + "졸업대상자선정", "예술제", "일감호축전", "개교기념일", "제적대상자알림" + }) + void categorize_academic_operation_event_keywords(String summary) { + // when + AcademicEventCategory result = AcademicEventCategorizer.categorize(summary); + + // then + assertThat(result).isEqualTo(AcademicEventCategory.ACADEMIC_OPERATION_EVENT); + } + + @DisplayName("기타 카테고리 분류") + @Test + void categorize_etc_for_unknown_keywords() { + // given + String summary = "알 수 없는 내용"; + + // when + AcademicEventCategory result = AcademicEventCategorizer.categorize(summary); + + // then + assertThat(result).isEqualTo(AcademicEventCategory.ETC); + } + + @DisplayName("null 또는 빈 문자열은 기타로 분류") + @Test + void categorize_null_or_empty_as_etc() { + // given + String[] testCases = {null, "", " "}; + + for (String summary : testCases) { + // when + AcademicEventCategory result = AcademicEventCategorizer.categorize(summary); + + // then + assertThat(result).isEqualTo(AcademicEventCategory.ETC); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverterTest.java b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverterTest.java index c06cd04f8..a4c935815 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverterTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventConverterTest.java @@ -5,6 +5,8 @@ import com.kustacks.kuring.worker.parser.calendar.dto.IcsEvent; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.time.LocalDateTime; import java.util.List; @@ -53,7 +55,7 @@ void convert_ics_event_to_academic_event() { IcsEvent icsEvent = testIcsEvents.get(0); //when - AcademicEvent academicEvent = AcademicEventConverter.convertToAcademicEvent(icsEvent); + AcademicEvent academicEvent = AcademicEventConverter.convertToAcademicEvent(icsEvent).orElse(null); //then assertEventFields(academicEvent, "하계방학", 0, Transparent.TRANSPARENT, false, @@ -72,11 +74,134 @@ void convert_multi_ics_event_to_academic_event() { LocalDateTime.of(2024, 6, 22, 0, 0), LocalDateTime.of(2024, 9, 2, 0, 0)); - assertEventFields(academicEvents.get(1), "폐강교과목 공지(1차)(9:00~)", 0, Transparent.TRANSPARENT, false, + assertEventFields(academicEvents.get(1), "폐강교과목 공지(1차)", 0, Transparent.TRANSPARENT, false, LocalDateTime.of(2024, 9, 2, 0, 0), LocalDateTime.of(2024, 9, 3, 0, 0)); } + @DisplayName("공휴일 이벤트는 변환에서 제외") + @ParameterizedTest + @ValueSource(strings = { + "신정공휴일", + "설날연휴공휴일", + "추석연휴공휴일", + "어린이날공휴일", + "광복절공휴일", + "크리스마스공휴일", + "공휴일입니다" + }) + void convert_exclude_holiday_events(String holidaySummary) { + // given + IcsEvent holidayEvent = new IcsEvent( + "test-uid", + holidaySummary, + "설명", + "20240101", + "20240102", + "PUBLIC", + "0", + "20250826T143707Z", + "TRANSPARENT", + "CONFIRMED", + "0", + "" + ); + + // when + AcademicEvent academicEvent = AcademicEventConverter.convertToAcademicEvent(holidayEvent).orElse(null); + + // then + assertThat(academicEvent).isNull(); + } + + @DisplayName("일반 학사일정 이벤트는 변환 대상") + @ParameterizedTest + @ValueSource(strings = { + "수강신청", + "중간고사", + "기말고사", + "휴학신청", + "개강", + "방학" + }) + void convert_include_normal_events(String normalSummary) { + // given + IcsEvent normalEvent = new IcsEvent( + "test-uid", + normalSummary, + "설명", + "20240101", + "20240102", + "PUBLIC", + "0", + "20250826T143707Z", + "TRANSPARENT", + "CONFIRMED", + "0", + "" + ); + + // when + AcademicEvent academicEvent = AcademicEventConverter.convertToAcademicEvent(normalEvent).orElse(null); + + // then + assertThat(academicEvent).isNotNull(); + assertThat(academicEvent.getSummary()).isEqualTo(normalSummary); + } + + @DisplayName("null이나 빈 summary는 변환에서 제외") + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void convert_exclude_empty_summary(String emptySummary) { + // given + IcsEvent emptyEvent = new IcsEvent( + "test-uid", + emptySummary, + "설명", + "20240101", + "20240102", + "PUBLIC", + "0", + "20250826T143707Z", + "TRANSPARENT", + "CONFIRMED", + "0", + "" + ); + + // when + AcademicEvent academicEvent = AcademicEventConverter.convertToAcademicEvent(emptyEvent).orElse(null); + + // then + assertThat(academicEvent).isNull(); + } + + @DisplayName("null summary는 변환에서 제외") + @Test + void convert_exclude_null_summary() { + // given + IcsEvent nullEvent = new IcsEvent( + "test-uid", + null, + "설명", + "20240101", + "20240102", + "PUBLIC", + "0", + "20250826T143707Z", + "TRANSPARENT", + "CONFIRMED", + "0", + "" + ); + + // when + AcademicEvent academicEvent = AcademicEventConverter.convertToAcademicEvent(nullEvent).orElse(null); + + // then + assertThat(academicEvent).isNull(); + } + private void assertEventFields(AcademicEvent academicEvent, String summary, Integer sequence, Transparent transparent, Boolean notifyEnabled, LocalDateTime startDate, LocalDateTime endDate) { assertAll( diff --git a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizerTest.java b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizerTest.java index f844efb67..eac742db2 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizerTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventDbSynchronizerTest.java @@ -3,6 +3,7 @@ import com.kustacks.kuring.calendar.application.port.out.AcademicEventCommandPort; import com.kustacks.kuring.calendar.application.port.out.AcademicEventQueryPort; import com.kustacks.kuring.calendar.domain.AcademicEvent; +import com.kustacks.kuring.calendar.domain.AcademicEventCategory; import com.kustacks.kuring.calendar.domain.Transparent; import com.kustacks.kuring.worker.parser.calendar.dto.IcsCalendarProperties; import com.kustacks.kuring.worker.parser.calendar.dto.IcsCalendarResult; @@ -84,7 +85,7 @@ private void setupTestData() { .summary("기존 테스트 이벤트 2") .startTime(LocalDateTime.of(2024, 3, 2, 9, 0)) .endTime(LocalDateTime.of(2024, 3, 2, 17, 0)) - .category("기본") + .category(AcademicEventCategory.ETC) .transparent(Transparent.TRANSPARENT) .sequence(0) // 새 이벤트보다 sequence가 낮음 .notifyEnabled(true) @@ -149,7 +150,7 @@ void should_not_update_when_sequence_is_same_or_lower() { .summary("높은 sequence 이벤트") .startTime(LocalDateTime.of(2024, 3, 2, 9, 0)) .endTime(LocalDateTime.of(2024, 3, 2, 17, 0)) - .category("기본") + .category(AcademicEventCategory.ETC) .transparent(Transparent.TRANSPARENT) .sequence(2) // 새 이벤트보다 sequence가 높음 .notifyEnabled(true) diff --git a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifierTest.java b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifierTest.java new file mode 100644 index 000000000..83f11a9b1 --- /dev/null +++ b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventNotificationClassifierTest.java @@ -0,0 +1,193 @@ +package com.kustacks.kuring.worker.update.calendar; + +import com.kustacks.kuring.calendar.domain.Transparent; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("AcademicEventNotificationClassifier 단위 테스트") +class AcademicEventNotificationClassifierTest { + + @DisplayName("OPAQUE 타입은 알림 활성화") + @Test + void proceed_opaque_transparent_returns_true() { + // given + Transparent transparent = Transparent.OPAQUE; + String summary = "일반 학사 일정"; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("TRANSPARENT 타입에서 액션 필요 키워드가 없으면 알림 비활성화") + @Test + void proceed_transparent_without_action_keywords_returns_false() { + // given + Transparent transparent = Transparent.TRANSPARENT; + String summary = "일반 개강일"; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("TRANSPARENT 타입에서 액션 필요 키워드가 있으면 알림 활성화") + @ParameterizedTest + @ValueSource(strings = { + "수강신청", + "수강정정", + "등록금납입", + "환불마감", + "휴학신청", + "복학신청", + "재입학접수", + "졸업논문입력", + "전과신청", + "다부전공신청", + "교직무시험검정원서", + "성적이의신청", + "취득학점포기", + "장학금신청", + "폐강교과목공지" + }) + void proceed_transparent_with_action_keywords_returns_true(String summary) { + // given + Transparent transparent = Transparent.TRANSPARENT; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("null 또는 빈 summary 처리") + @ParameterizedTest + @CsvSource({ + ",", + "'',", + "' '," + }) + void proceed_null_or_empty_summary_with_transparent(String summary) { + // given + Transparent transparent = Transparent.TRANSPARENT; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("액션 필요 키워드 포함 여부 - 대소문자 무관") + @ParameterizedTest + @CsvSource({ + "수강신청, true", + "수강정정 안내, true", + "등록금 납입, true", + "환불마감 기한, true", + "취득학점포기, true", + "성적이의신청, true", + "교직무시험검정원서, true", + "장학금신청기간, true", + "폐강교과목공지, true", + "개강, false", + "방학, false", + "중간고사, false", + "기말고사, false", + "학위수여식, false", + "예술제, false" + }) + void proceed_action_keyword_detection_case_insensitive(String summary, boolean expected) { + // given + Transparent transparent = Transparent.TRANSPARENT; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isEqualTo(expected); + } + + @DisplayName("복합 키워드 테스트") + @ParameterizedTest + @ValueSource(strings = { + "전체학년 수강신청", + "2024년도 등록금 납입 안내", + "동계 계절학기 중간고사 및 환불 마감", + "수강정정 및 초과과목 신청", + "취득학점포기(3차) 신청", + "성적이의신청 및 최종성적이의신청", + "장학금 및 학비감면 서류제출", + "교직무시험검정원서 접수", + "폐강교과목 공지(1차) 및 수강정정" + }) + void proceed_complex_summary_with_action_keywords(String summary) { + // given + Transparent transparent = Transparent.TRANSPARENT; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("null Transparent와 액션 키워드가 있는 summary") + @Test + void proceed_null_transparent_with_action_keyword() { + // given + Transparent transparent = null; + String summary = "수강신청"; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("null Transparent와 액션 키워드가 없는 summary") + @Test + void proceed_null_transparent_without_action_keyword() { + // given + Transparent transparent = null; + String summary = "개강"; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("OPAQUE이면 summary 내용과 관계없이 항상 알림") + @ParameterizedTest + @ValueSource(strings = { + "개강", + "방학", + "축제", + "중간고사", + "기말고사", + "학술제" + }) + void proceed_opaque_always_notify_regardless_of_summary(String summary) { + // given + Transparent transparent = Transparent.OPAQUE; + + // when + boolean result = AcademicEventNotificationClassifier.proceed(transparent, summary); + + // then + assertThat(result).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizerTest.java b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizerTest.java new file mode 100644 index 000000000..fb4de6fd7 --- /dev/null +++ b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventSummaryNormalizerTest.java @@ -0,0 +1,94 @@ +package com.kustacks.kuring.worker.update.calendar; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("AcademicEventSummaryNormalizer 단위 테스트") +class AcademicEventSummaryNormalizerTest { + + + @DisplayName("※ 기호 이후 텍스트 제거") + @ParameterizedTest + @CsvSource({ + "'취득학점포기(3차)※주말(토,일)미포함', '취득학점포기(3차)'", + "'수강신청※자세한 내용은 홈페이지 참조', '수강신청'", + "'등록금납입※기한 엄수', '등록금납입'", + "'중간고사※시험 일정표 확인', '중간고사'" + }) + void normalize_removeTextAfterSymbol(String input, String expected) { + // when + String result = AcademicEventSummaryNormalizer.normalize(input); + + // then + assertThat(result).isEqualTo(expected); + } + + @DisplayName("괄호 안 날짜/시간 정보 제거 (차수 정보는 유지)") + @ParameterizedTest + @CsvSource({ + "전체학년 수강신청 (2. 25. 화 9:30 ~ 2. 26 수 17:00), 전체학년 수강신청", + "강의시간표조회 (9:30 ~), 강의시간표조회", + "성적입력 (10:00~18:00), 성적입력", + "취득학점포기(3차), 취득학점포기(3차)", + "재수강신청(2차), 재수강신청(2차)", + "장학금신청(1차), 장학금신청(1차)" + }) + void normalize_removeDateTimeBrackets_preserveSequence(String input, String expected) { + // when + String result = AcademicEventSummaryNormalizer.normalize(input); + + // then + assertThat(result).isEqualTo(expected); + } + + @DisplayName("괄호 없는 직접 날짜/시간 정보 제거") + @ParameterizedTest + @CsvSource({ + "취득학점포기(3차) 14.(월) 10:30 ~ 15.(화) 16:30, 취득학점포기(3차)", + "'최정성적이의신청 ~ 5.(월) 16:30', '최정성적이의신청'", + "수강신청 25.(화) 09:00 ~ 26.(수) 17:00, 수강신청", + "등록금납입 1.(금) 08:00 ~ 31.(일) 23:59, 등록금납입" + }) + void normalize_removeDateTime(String input, String expected) { + // when + String result = AcademicEventSummaryNormalizer.normalize(input); + + // then + assertThat(result).isEqualTo(expected); + } + + @DisplayName("단순 시간 정보 제거 (HH:MM)") + @ParameterizedTest + @CsvSource({ + "'복학신청 마감 17:00', '복학신청 마감'", + "'수강신청 09:30', '수강신청'", + "'등록금납입 23:59', '등록금납입'", + "'장학금신청 14:30', '장학금신청'", + "'성적이의신청 8:00', '성적이의신청'" + }) + void normalize_removeSimpleTime(String input, String expected) { + // when + String result = AcademicEventSummaryNormalizer.normalize(input); + + // then + assertThat(result).isEqualTo(expected); + } + + @DisplayName("시간이 아닌 콜론 내용은 보존") + @ParameterizedTest + @CsvSource({ + "'전공:컴퓨터공학', '전공:컴퓨터공학'", + "'학과:전자공학', '학과:전자공학'", + "'카테고리:A급', '카테고리:A급'" + }) + void normalize_preserveNonTimeColonContent(String input, String expected) { + // when + String result = AcademicEventSummaryNormalizer.normalize(input); + + // then + assertThat(result).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java index c04e69670..93d7b8e17 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdaterTest.java @@ -66,9 +66,9 @@ void should_save_initial_calendar_data_to_db() throws IOException, ParserExcepti // when - 업데이트 실행 assertDoesNotThrow(() -> academicEventUpdater.update()); - // then - DB에 저장된 데이터 검증(기존 5건 + 신규 198건) + // then - DB에 저장된 데이터 검증(기존 5건 + 신규 198건 - 공휴일 19건 = 184) List allEvents = academicEventQueryPort.findAll(); - assertThat(allEvents).hasSize(203); + assertThat(allEvents).hasSize(184); AcademicEvent event1 = allEvents.stream() .filter(event -> event.getEventUid().equals(testEventUids.get(0))) @@ -86,7 +86,7 @@ void should_save_initial_calendar_data_to_db() throws IOException, ParserExcepti LocalDateTime.of(2024, 9, 2, 0, 0)); // 폐강교과목 공지 이벤트 검증 - assertEventFields(event2, "폐강교과목 공지(1차)(9:00~)", 0, Transparent.TRANSPARENT, false, + assertEventFields(event2, "폐강교과목 공지(1차)", 0, Transparent.TRANSPARENT, false, LocalDateTime.of(2024, 9, 2, 0, 0), LocalDateTime.of(2024, 9, 3, 0, 0)); } @@ -103,9 +103,9 @@ void should_update_existing_and_add_new_events() throws IOException, ParserExcep mockingScrapCalendar(updatedCalendar); assertDoesNotThrow(() -> academicEventUpdater.update()); - // then - DB에 저장된 데이터 검증(기존 5건 + 신규 199건) + // then - DB에 저장된 데이터 검증(기존 5건 + 신규 199건 - 19건 = 185건) List allEvents = academicEventQueryPort.findAll(); - assertThat(allEvents).hasSize(204); + assertThat(allEvents).hasSize(185); AcademicEvent event1 = allEvents.stream() .filter(event -> event.getEventUid().equals(testEventUids.get(0))) @@ -128,7 +128,7 @@ void should_update_existing_and_add_new_events() throws IOException, ParserExcep LocalDateTime.of(2024, 9, 2, 0, 0)); // 폐강교과목 공지 이벤트 검증 - assertEventFields(event2, "폐강교과목 공지(1차)(9:00~)", 1, Transparent.OPAQUE, true, + assertEventFields(event2, "폐강교과목 공지(1차)", 1, Transparent.OPAQUE, true, LocalDateTime.of(2024, 9, 3, 0, 0), LocalDateTime.of(2024, 9, 4, 0, 0)); From 2a5e03045ac4518b36549c511f160d8632889dec Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:13:42 +0900 Subject: [PATCH 14/16] =?UTF-8?q?Feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5-=20=EC=A0=84=EC=B2=B4=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=86=A0=ED=94=BD=20=EA=B5=AC=EB=8F=85=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: User.getFcmToken getter 추가 * feat: 사용자 모든 구독 정보 가져오는 Repository Method 추가 * remove: 불필요 메서드 삭제 * remove: 모든 사용자 토픽 구독 처리 서비스 로직 추가 * remove: 모든 사용자 토픽 구독 처리 API추가 * remove: 모든 사용자 토픽 구독 처리 인수테스트 추가 * fix: 재구독 시 트랜잭션 어노테이션 제거 * fix: 재구독 성공/실패 개수 로깅 * feat: 학사일정 카테고리 컬럼 사이즈 30으로 변경 --- .../adapter/in/web/AdminCommandApiV2.java | 13 ++-- .../port/in/AdminCommandUseCase.java | 4 +- .../port/out/AdminUserFeedbackPort.java | 4 - .../service/AdminCommandService.java | 76 +++++++++++++++---- .../common/dto/ResponseCodeAndMessages.java | 1 + .../persistence/UserPersistenceAdapter.java | 10 +-- .../out/persistence/UserQueryRepository.java | 2 + .../persistence/UserQueryRepositoryImpl.java | 10 +++ .../application/port/out/UserQueryPort.java | 2 + .../com/kustacks/kuring/user/domain/User.java | 1 + ...er_academic_event_category_column_size.sql | 2 + .../acceptance/AdminAcceptanceTest.java | 25 +++++- .../kustacks/kuring/acceptance/AdminStep.java | 19 +++++ 13 files changed, 135 insertions(+), 34 deletions(-) create mode 100644 src/main/resources/db/migration/V251008__Alter_academic_event_category_column_size.sql diff --git a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java index f886551c4..d4518413e 100644 --- a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java +++ b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java @@ -17,7 +17,6 @@ import com.kustacks.kuring.common.utils.converter.StringToDateTimeConverter; import com.kustacks.kuring.common.utils.validator.BadWordInitProcessor; import com.kustacks.kuring.common.utils.validator.WhitelistWordInitProcessor; -import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -28,7 +27,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -136,11 +134,12 @@ public ResponseEntity> refreshWhitelistWords() { return ResponseEntity.ok().body(new BaseResponse<>(ResponseCodeAndMessages.ADMIN_LOAD_WHITELIST_WORDS, null)); } - @Hidden + @Operation(summary = "사용자 토픽 재구독", description = "모든 사용자의 구독 정보를 기반으로 Firebase 토픽을 재구독합니다") + @SecurityRequirement(name = "JWT") @Secured(AdminRole.ROLE_ROOT) - @GetMapping("/subscribe/all") - public ResponseEntity subscribe() { - adminCommandUseCase.subscribeAllUserSameTopic(); - return ResponseEntity.ok().build(); + @PostMapping("/users/subscriptions/all") + public ResponseEntity> resubscribeAllUsersToTopics() { + adminCommandUseCase.resubscribeAllUsersToTopics(); + return ResponseEntity.ok().body(new BaseResponse<>(ResponseCodeAndMessages.ADMIN_USER_SUBSCRIPTION_UPDATE_SUCCESS, null)); } } diff --git a/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java b/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java index 1c227d9bd..005e3f051 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java +++ b/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java @@ -11,11 +11,11 @@ public interface AdminCommandUseCase { void createRealNoticeForAllUser(RealNotificationCommand command); - void subscribeAllUserSameTopic(); - void addAlertSchedule(AlertCreateCommand command); void cancelAlertSchedule(Long id); void embeddingCustomData(DataEmbeddingCommand command); + + void resubscribeAllUsersToTopics(); } diff --git a/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java b/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java index 5d58cde44..166bb3990 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java +++ b/src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java @@ -4,10 +4,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.util.List; - public interface AdminUserFeedbackPort { - List findAllToken(); - Page findAllFeedbackByPageRequest(Pageable pageable); } diff --git a/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java b/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java index e4783257a..1e6b78b91 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java +++ b/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java @@ -1,12 +1,10 @@ package com.kustacks.kuring.admin.application.service; -import com.google.firebase.messaging.FirebaseMessaging; import com.kustacks.kuring.admin.application.port.in.AdminCommandUseCase; import com.kustacks.kuring.admin.application.port.in.dto.RealNotificationCommand; import com.kustacks.kuring.admin.application.port.in.dto.TestNotificationCommand; import com.kustacks.kuring.admin.application.port.out.AdminAlertEventPort; import com.kustacks.kuring.admin.application.port.out.AdminEventPort; -import com.kustacks.kuring.admin.application.port.out.AdminUserFeedbackPort; import com.kustacks.kuring.admin.application.port.out.AiEventPort; import com.kustacks.kuring.admin.domain.Admin; import com.kustacks.kuring.alert.application.port.in.dto.AlertCreateCommand; @@ -14,7 +12,10 @@ import com.kustacks.kuring.auth.userdetails.UserDetailsServicePort; import com.kustacks.kuring.common.annotation.UseCase; import com.kustacks.kuring.common.properties.ServerProperties; +import com.kustacks.kuring.message.application.port.out.FirebaseSubscribePort; import com.kustacks.kuring.notice.domain.CategoryName; +import com.kustacks.kuring.user.application.port.out.UserQueryPort; +import com.kustacks.kuring.user.domain.User; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.InputStreamResource; @@ -26,8 +27,12 @@ import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC; import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ALL_DEVICE_SUBSCRIBED_TOPIC; @Slf4j @@ -36,10 +41,11 @@ public class AdminCommandService implements AdminCommandUseCase { private final UserDetailsServicePort userDetailsServicePort; - private final AdminUserFeedbackPort adminUserFeedbackPort; private final AdminAlertEventPort adminAlertEventPort; private final AdminEventPort adminEventPort; private final AiEventPort aiEventPort; + private final UserQueryPort userQueryPort; + private final FirebaseSubscribePort firebaseSubscribePort; private final NoticeProperties noticeProperties; private final ServerProperties serverProperties; private final PasswordEncoder passwordEncoder; @@ -107,21 +113,61 @@ public void embeddingCustomData(DataEmbeddingCommand command) { } } - /** - * TODO : 1회성 API - client v2 배포 후, 단 한번 모든 사용자를 공통 topic에 구독시킨 후 제거 예정 - */ - @Transactional @Override - public void subscribeAllUserSameTopic() { - String topic = serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC); + public void resubscribeAllUsersToTopics() { + List allUsers = userQueryPort.findAllWithSubscriptions(); + Map> topicSubscriptions = new HashMap<>(); + + //User 당 (1 + 학사일정 알림 구독여부 + 유저별 카테고리 구독 수 + 유저별 학과 구독 수) 반복 + //User 당 최대 2 + 카테고리 수 + 학과 수 => 넉넉 잡아 80 + //ex. User 5000명 * 80 => 400,000번 반복 + for (User user : allUsers) { + String fcmToken = user.getFcmToken(); + + // allDevice 토픽 - 모든 사용자 + String allDeviceTopic = serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC); + topicSubscriptions.computeIfAbsent(allDeviceTopic, k -> new LinkedList<>()).add(fcmToken); + + // academicEvent 토픽 - 학사일정 알림이 활성화된 사용자 + if (user.isAcademicEventNotificationEnabled()) { + String academicEventTopic = serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC); + topicSubscriptions.computeIfAbsent(academicEventTopic, k -> new LinkedList<>()).add(fcmToken); + } + + // 카테고리별 토픽 + user.getSubscribedCategoryList().forEach(category -> { + String categoryTopic = serverProperties.ifDevThenAddSuffix(category.getName()); + topicSubscriptions.computeIfAbsent(categoryTopic, k -> new LinkedList<>()).add(fcmToken); + }); + + // 학과별 토픽 + user.getSubscribedDepartmentList().forEach(department -> { + String departmentTopic = serverProperties.ifDevThenAddSuffix(department.getName()); + topicSubscriptions.computeIfAbsent(departmentTopic, k -> new LinkedList<>()).add(fcmToken); + }); + } + + // 토픽별로 500개씩 나누어 구독 처리 + for (Map.Entry> entry : topicSubscriptions.entrySet()) { + String topic = entry.getKey(); + List tokens = entry.getValue(); + + log.info("Resubscribing {} users to topic: {}", tokens.size(), topic); + + int successCount = 0; + int failureCount = 0; - FirebaseMessaging instance = FirebaseMessaging.getInstance(); - List allToken = adminUserFeedbackPort.findAllToken(); + for (int i = 0; i < tokens.size(); i += 500) { + List batch = tokens.subList(i, Math.min(i + 500, tokens.size())); + try { + firebaseSubscribePort.subscribeToTopic(batch, topic); + successCount += batch.size(); + } catch (Exception e) { + failureCount += batch.size(); + } + } - int size = allToken.size(); - for (int i = 0; i < size; i += 500) { - List subList = allToken.subList(i, Math.min(i + 500, size)); - instance.subscribeToTopicAsync(subList, topic); + log.info("Resubscribed {} users to topic: {}. {} users failed.", successCount, topic, failureCount); } } diff --git a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java index 168eaeea2..406454886 100644 --- a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java +++ b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java @@ -29,6 +29,7 @@ public enum ResponseCodeAndMessages { ADMIN_EMBEDDING_NOTICE_SUCCESS(HttpStatus.OK.value(), "데이터 임베딩에 생성에 성공하였습니다"), ADMIN_LOAD_BAD_WORDS(HttpStatus.OK.value(), "금칙어 로드에 성공했습니다."), ADMIN_LOAD_WHITELIST_WORDS(HttpStatus.OK.value(), "허용 단어 로드에 성공했습니다."), + ADMIN_USER_SUBSCRIPTION_UPDATE_SUCCESS(HttpStatus.OK.value(), "사용자들의 구독 정보를 성공적으로 재설정했습니다."), /* User */ USER_REGISTER_SUCCESS(HttpStatus.OK.value(), "회원가입에 성공하였습니다"), diff --git a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java index 9b465e06d..64a82cd82 100644 --- a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java +++ b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java @@ -30,11 +30,6 @@ public Page findAllFeedbackByPageRequest(Pageable pageable) { return userRepository.findAllFeedbackByPageRequest(pageable); } - @Override - public List findAllToken() { - return userRepository.findAllFcmTokens(); - } - @Override public Optional findByToken(String token) { if (token == null || token.isBlank()) { @@ -67,6 +62,11 @@ public List findAll() { return userRepository.findAll(); } + @Override + public List findAllWithSubscriptions() { + return userRepository.findAllWithSubscriptions(); + } + @Override public List findByPageRequest(Pageable pageable) { return userRepository.findByPageRequest(pageable); diff --git a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java index 4dfab8491..2bff35fa3 100644 --- a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java +++ b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java @@ -13,5 +13,7 @@ interface UserQueryRepository { List findByPageRequest(Pageable pageable); + List findAllWithSubscriptions(); + void resetAllUserQuestionCount(); } diff --git a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java index cf1fa0ad7..768390c67 100644 --- a/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java +++ b/src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java @@ -45,6 +45,16 @@ public List findByPageRequest(Pageable pageable) { .fetch(); } + @Override + public List findAllWithSubscriptions() { + return queryFactory + .selectFrom(user) + .leftJoin(user.categories.categoryNamesSet).fetchJoin() + .leftJoin(user.departments.departmentNamesSet).fetchJoin() + .distinct() + .fetch(); + } + @Transactional @Override public void resetAllUserQuestionCount() { diff --git a/src/main/java/com/kustacks/kuring/user/application/port/out/UserQueryPort.java b/src/main/java/com/kustacks/kuring/user/application/port/out/UserQueryPort.java index 00f68d0e2..047bfc380 100644 --- a/src/main/java/com/kustacks/kuring/user/application/port/out/UserQueryPort.java +++ b/src/main/java/com/kustacks/kuring/user/application/port/out/UserQueryPort.java @@ -11,6 +11,8 @@ public interface UserQueryPort { Optional findByToken(String token); List findAll(); + + List findAllWithSubscriptions(); List findByPageRequest(Pageable pageable); List findByLoggedInUserId(Long id); diff --git a/src/main/java/com/kustacks/kuring/user/domain/User.java b/src/main/java/com/kustacks/kuring/user/domain/User.java index 7446d8c6a..ea95479dd 100644 --- a/src/main/java/com/kustacks/kuring/user/domain/User.java +++ b/src/main/java/com/kustacks/kuring/user/domain/User.java @@ -35,6 +35,7 @@ public class User implements Serializable { @Column(name = "id", nullable = false) private Long id; + @Getter(AccessLevel.PUBLIC) @Column(name = "fcm_token", unique = true, nullable = false) private String fcmToken; diff --git a/src/main/resources/db/migration/V251008__Alter_academic_event_category_column_size.sql b/src/main/resources/db/migration/V251008__Alter_academic_event_category_column_size.sql new file mode 100644 index 000000000..7bc696efc --- /dev/null +++ b/src/main/resources/db/migration/V251008__Alter_academic_event_category_column_size.sql @@ -0,0 +1,2 @@ +-- 학사일정 카테고리 컬럼 사이즈를 20에서 30으로 확장 +ALTER TABLE academic_event MODIFY COLUMN category VARCHAR (30); \ No newline at end of file diff --git a/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java index 3a7ab93a7..836512b83 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java @@ -18,7 +18,17 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import static com.kustacks.kuring.acceptance.AdminStep.*; +import static com.kustacks.kuring.acceptance.AdminStep.금칙어_로드_요청; +import static com.kustacks.kuring.acceptance.AdminStep.사용자_토픽_재구독_요청; +import static com.kustacks.kuring.acceptance.AdminStep.사용자_토픽_재구독_응답_확인; +import static com.kustacks.kuring.acceptance.AdminStep.사용자_피드백_조회_요청; +import static com.kustacks.kuring.acceptance.AdminStep.신고_목록_조회_요청; +import static com.kustacks.kuring.acceptance.AdminStep.신고_목록_조회_확인; +import static com.kustacks.kuring.acceptance.AdminStep.알림_예약; +import static com.kustacks.kuring.acceptance.AdminStep.예약_알림_삭제; +import static com.kustacks.kuring.acceptance.AdminStep.예약_알림_조회; +import static com.kustacks.kuring.acceptance.AdminStep.피드백_조회_확인; +import static com.kustacks.kuring.acceptance.AdminStep.허용_단어_로드_요청; import static com.kustacks.kuring.acceptance.AuthStep.로그인_되어_있음; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -356,6 +366,19 @@ void role_client_admin_call_root_api_test() { ); } + @DisplayName("[v2] Admin은 모든 사용자의 토픽을 재구독할 수 있다") + @Test + void admin_can_resubscribe_all_users_to_topics() { + // given + String adminAccessToken = 로그인_되어_있음(ADMIN_LOGIN_ID, ADMIN_PASSWORD); + + // when + var 재구독_응답 = 사용자_토픽_재구독_요청(adminAccessToken); + + // then + 사용자_토픽_재구독_응답_확인(재구독_응답); + } + private void 댓글_작성_및_신고() { String userAccessToken = UserStep.사용자_로그인_되어_있음(USER_FCM_TOKEN, USER_EMAIL, USER_PASSWORD); diff --git a/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java b/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java index 013515f83..7707fc430 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java +++ b/src/test/java/com/kustacks/kuring/acceptance/AdminStep.java @@ -118,4 +118,23 @@ public class AdminStep { .then().log().all() .extract(); } + + public static ExtractableResponse 사용자_토픽_재구독_요청(String accessToken) { + return RestAssured + .given().log().all() + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/api/v2/admin/users/subscriptions/all") + .then().log().all() + .extract(); + } + + public static void 사용자_토픽_재구독_응답_확인(ExtractableResponse response) { + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getInt("code")).isEqualTo(200), + () -> assertThat(response.jsonPath().getString("message")).isEqualTo("사용자들의 구독 정보를 성공적으로 재설정했습니다."), + () -> assertThat(response.jsonPath().getString("data")).isNull() + ); + } } From 871565d92070a5e641eef1cc4709402d66df15fc Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:51:38 +0900 Subject: [PATCH 15/16] =?UTF-8?q?Fix:=20=ED=95=99=EC=82=AC=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=95=8C=EB=A6=BC=20=EB=B0=8F=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 학사일정 알림 스케쥴 시간 변경(매일 아침 9시) * fix: 학사일정 업데이트 시간 수정(매주 월요일 오전 5시) --- .../notification/AcademicEventNotificationScheduler.java | 4 ++-- .../kuring/worker/update/calendar/AcademicEventUpdater.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java b/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java index 69bd66abc..d05b28eba 100644 --- a/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java +++ b/src/main/java/com/kustacks/kuring/worker/notification/AcademicEventNotificationScheduler.java @@ -16,8 +16,8 @@ public class AcademicEventNotificationScheduler { private final AcademicEventNotificationUseCase academicEventNotificationUseCase; private final FeatureFlags featureFlags; - // 매주 월요일 새벽 5시 학사일정 알림 전송 - @Scheduled(cron = "0 0 5 * * 1", zone = "Asia/Seoul") + // 매일 아침 9시 학사일정 알림 전송 + @Scheduled(cron = "0 0 9 * * *", zone = "Asia/Seoul") public void sendDailyAcademicEventNotifications() { if (featureFlags.isEnabled(KuringFeatures.NOTIFY_ACADEMIC_EVENT.getFeature())) { log.info("******** 일일 학사일정 알림 발송 시작 ********"); diff --git a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java index 028807313..60bf3b3f0 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/calendar/AcademicEventUpdater.java @@ -24,8 +24,8 @@ public class AcademicEventUpdater { private final AcademicEventDbSynchronizer academicEventDbSynchronizer; private final FeatureFlags featureFlags; - //매월 1일 00시 00분 업데이트 진행 - @Scheduled(cron = "0 0 0 1 * *", zone = "Asia/Seoul") + //매주 월요일 새벽 5시 학사일정 업데이트 진행 + @Scheduled(cron = "0 0 5 * * 1", zone = "Asia/Seoul") public void update() { if (featureFlags.isEnabled(KuringFeatures.UPDATE_ACADEMIC_EVENT.getFeature())) { log.info("******** 학사일정 업데이트 시작 ********"); From f2bfeb3e0231620445d95c3efc50ce11c33bfd50 Mon Sep 17 00:00:00 2001 From: HanJu Kim <56250226+rlagkswn00@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:15:53 +0900 Subject: [PATCH 16/16] =?UTF-8?q?Feat:=20=EB=8C=80=ED=95=99=EC=9B=90=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=9E=A9=20Feature=20Flag=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 대학원 공지사항 스크랩 피처플래그 추가 * feat: 대학원 공지사항 스크랩 피처플래그 적용 --- .../common/featureflag/KuringFeatures.java | 1 + .../DepartmentGraduationNoticeUpdater.java | 32 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java b/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java index 2ff88df3f..661c8f3f8 100644 --- a/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java +++ b/src/main/java/com/kustacks/kuring/common/featureflag/KuringFeatures.java @@ -4,6 +4,7 @@ public enum KuringFeatures { UPDATE_NOTICE_EMBEDDING(new Feature("update_notice_embedding")), UPDATE_DEPARTMENT_NOTICE(new Feature("update_department_notice")), + UPDATE_DEPARTMENT_GRADUATION_NOTICE(new Feature("update_department_graduation_notice")), UPDATE_KUIS_HOMEPAGE_NOTICE(new Feature("update_kuis_homepage_notice")), UPDATE_KUIS_NOTICE(new Feature("update_kuis_notice")), UPDATE_USER(new Feature("update_user")), diff --git a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java index 4b034aaf8..dc12b4504 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java @@ -41,26 +41,28 @@ public class DepartmentGraduationNoticeUpdater { @Scheduled(cron = "0 15/20 7-19 * * *", zone = "Asia/Seoul") // 학교 공지는 오전 7:15 ~ 오후 7:55분 사이에 20분마다 업데이트 된다. public void update() { - log.info("******** 학과별 (대학원) 최신 공지 업데이트 시작 ********"); - - List graduateDeptInfoList = getGraduateDeptInfoList(); - - for (DeptInfo deptInfo : graduateDeptInfoList) { - CompletableFuture - .supplyAsync( - () -> updateDepartmentAsync(deptInfo, DeptInfo::scrapGraduateLatestPageHtml), - noticeUpdaterThreadTaskExecutor - ).thenApply( - scrapResults -> compareLatestAndUpdateDB(scrapResults, deptInfo.getDeptName()) - ).thenAccept( - notificationService::sendNotifications - ); + if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_GRADUATION_NOTICE.getFeature())) { + log.info("******** 학과별 (대학원) 최신 공지 업데이트 시작 ********"); + + List graduateDeptInfoList = getGraduateDeptInfoList(); + + for (DeptInfo deptInfo : graduateDeptInfoList) { + CompletableFuture + .supplyAsync( + () -> updateDepartmentAsync(deptInfo, DeptInfo::scrapGraduateLatestPageHtml), + noticeUpdaterThreadTaskExecutor + ).thenApply( + scrapResults -> compareLatestAndUpdateDB(scrapResults, deptInfo.getDeptName()) + ).thenAccept( + notificationService::sendNotifications + ); + } } } @Scheduled(cron = "0 0 1 * * 6", zone = "Asia/Seoul") // 전체 업데이트는 매주 토요일 오전 1시에 한다. public void updateAll() { - if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_NOTICE.getFeature())) { + if (featureFlags.isEnabled(KuringFeatures.UPDATE_DEPARTMENT_GRADUATION_NOTICE.getFeature())) { log.info("******** 학과별 (대학원) 전체 공지 업데이트 시작 ********"); List graduateDeptInfoList = getGraduateDeptInfoList();