Skip to content

Commit

Permalink
Release/v2.2.1 (#261)
Browse files Browse the repository at this point in the history
* [IDLE-000] Production CI 스크립트 작성

* [IDLE-000] 센터 공고 수정 API 내 접수 방법 null 비허용

* [IDLE-000] 공고 지원자 조회 시, 삭제된 유저는 조회되지 않도록 수정

* [IDLE-363] 센터 관리자 전화 인증 요청 API

* [IDLE-363] 개발 환경 ddl-auto 옵션 임시로 create 옵션으로 변경

* [IDLE-365] 공고 크롤러 selenium 로직 수정 및 로컬 동작 확인

* [IDLE-365] 테스트를 위한 스케줄러 기준 시각 변경

* [IDLE-365] batch job enable 옵션 비활성화

* [IDLE-365] ddl-auto update로 변경

* [IDLE-365] 크롤링 공고 필드 null 비허용

* [IDLE-365] 스케줄러 시간 02시로 설정

* [IDLE-366] spring batch selenium 크롤링을 위한 빌드 스크립트 수정

* [IDLE-366] worknet 사이트가 고용 24 사이트로 통합됨에 따라, 크롤링 스크립트 수정

* [IDLE-366] 크롤러 동작 스케줄링 시간 변경

* [IDLE-366] 스프링 초기 실행 시, spring batch 자동 실행 방지 옵션 추가

* [IDLE-366] spring batch 5 버전에서 업데이트 된 변경사항 적용 및 별도의 JobLauncher 구현

* [IDLE-366] 불필요 의존성 및 옵션 제거

* [IDLE-366] 크롤링 조회 API 내 entity status 필드 추가 및 쿼리 수정

* [IDLE-358] 운영 환경 CD 구축 및 운영 환경 profile 설정 추가

* [IDLE-358] 운영 환경 docker run 실행 시 예외 처리

* [IDLE-000] 크롤링 전체 조회 DTO 생성자 추가

* [IDLE-000] 공고 전체 조회 fetchJoin() 중복 이슈 해결을 위한 subquery 분리

* [IDLE-000] AI 코드리뷰 coderabbit 도입

* [IDLE-000] 크롤링 공고 반경범위 조회 필터를 위한 where절 추가

* [IDLE-000] TimeZone 설정이 적용되지 않는 문제 해결

* [IDLE-000] TimeZone 지정 및 @EnableScheduling 설정

* [IDLE-000] TimeZone 설정 제거

* [IDLE-000] 공고 전체 조회 쿼리 롤백

* [IDLE-000] 기존 공고 내 location을 기반으로 위.경도 값을 decoding하도록 변경

* [IDLE-000] 배포 전 최종 QA

* [IDLE-000] 크롤링 공고 생성일자 필드 type 변경(timestamp -> date)

* [IDLE-000] Redis 비밀번호 설정 추가 (#171)

* [IDLE-000] Redis 비밀번호 설정 추가

* [IDLE-000] yaml 파일에 password 필드 추가

* [IDLE-000] redis local default password 설정

* [IDLE-000] compose 파일 내, 비밀번호 지정 command 설정

* [IDLE-000] Redis 볼륨 설정

* [IDLE-000] ci triggering branch 임시 변경

* [IDLE-000] ci triggering branch 롤백

* [IDLE-000] 테스트 심사 통과를 위한 전화번호 검증 로직 추가

* [IDLE-000] 테스트 심사 통과를 위한 전화번호 검증 로직 추가

* [IDLE-389] 센터 인증 요청 이벤트 발생 시, 디스코드 웹훅 알림을 전송하는 로직 작성

* [IDLE-389] 사업자 등록번호 client properties 클래스명 변경

* [IDLE-389] 코드 리뷰 반영

* [IDLE-000] 트랜잭션 전파 레벨 변경(REQUIRED -> REQUIRES_NEW)

* [IDLE-396] FCM 모듈 추가 및 firebase 의존성 설정

* readme 제목 수정

* readme 제목 수정

* [IDLE-399] FCM Device Token 관리 API

* [IDLE-399] 알림 도메인 설계

* [IDLE-000] fcm service 설정값 주입을 위한 디렉토리 구조 변경

* [IDLE-000] fcm service 설정값 주입을 위한 디렉토리 구조 변경

* [IDLE-400] 채용 공고 지원자 발생 시, 센터 관리자에게 알림을 발송한다.

* [IDLE-400] fcm 모듈 설정 추가

* [IDLE-400] 공고 지원자 발생 시, 모든 센터 관리자들에게 다중 알림을 발송한다.

* [IDLE-415] 알림 조회 처리 API

* [IDLE-417] 읽지 않은 알림 수 집계 API

* [IDLE-418] 알림 목록 조회 API

* [IDLE-418] 피드백 반영

* [IDLE-423] soft-delete가 적용된 즐겨찾기 entity에서, 즐겨찾기 해제 후 다시 설정하는 경우 발생하는 버그를 해결한다.

* [IDLE-424] 요양 보호사 공고 전체 조회 시, 삭제된 공고가 함께 보이는 문제 해결

* [IDLE-400] 채용 공고 지원자 발생 시, 센터 관리자에게 알림을 발송한다. (#183)

* [IDLE-400] 채용 공고 지원자 발생 시, 센터 관리자에게 알림을 발송한다.

* [IDLE-400] fcm 모듈 설정 추가

* [IDLE-400] 공고 지원자 발생 시, 모든 센터 관리자들에게 다중 알림을 발송한다.

* [IDLE-400] 채용 공고 지원자 발생 시, 센터 관리자에게 알림을 발송한다.

* [IDLE-400] fcm 모듈 설정 추가

* [IDLE-400] 공고 지원자 발생 시, 모든 센터 관리자들에게 다중 알림을 발송한다.

* [IDLE-415] 알림 조회 처리 API

* [IDLE-417] 읽지 않은 알림 수 집계 API

* [IDLE-418] 알림 목록 조회 API

* [IDLE-418] 피드백 반영

* [IDLE-423] soft-delete가 적용된 즐겨찾기 entity에서, 즐겨찾기 해제 후 다시 설정하는 경우 발생하는 버그를 해결한다.

* [IDLE-424] 요양 보호사 공고 전체 조회 시, 삭제된 공고가 함께 보이는 문제 해결

* [IDLE-400] 알림 명세 변경

* [IDLE-400] fcm 설정 파일 디렉토리 path 변경

* [IDLE-400] ddl 옵션 변경

* [IDLE-400] ci/cd 옵션 변경

* [IDLE-000] ci triggering branch 복구

* [IDLE-000] develop 환경에서 fcm service 설정 파일 path 수정

* [IDLE-000] fcm service json 파일명 변경

* [IDLE-000] firebase service key 설정 경로 체크를 위해, cd 스크립트를 수정합니다.

* Update README.md

* [IDLE-000] fcm service key 경로를 class path 경로로 수정 시도

* [IDLE-000] ci triggering branch 임시 변경

* [IDLE-000] firebaseApp 초기화 임시 비활성화

* [IDLE-000] firebaseApp 초기화 임시 비활성화

* [IDLE-000] firebaseApp 초기화 임시 비활성화

* [IDLE-000] file path 앞에 ./ 제외

* [IDLE-000] 절대 경로로 변경 시도

* [IDLE-000] fcm service key 생성 path 수정

* [IDLE-000] fcm service key json file 생성 스크립트 작성

* [IDLE-000] ci triggering branch develop으로 롤백

* [IDLE-000] 불필요 스크립트 제거

* [IDLE-000] firebase config의 현재 경로를 출력하도록 print문 추가 (#197)

* [IDLE-000] firebase config의 현재 경로를 출력하도록 print문 추가

* [IDLE-000] ci triggering branch 수정

* [IDLE-000] ci triggering branch 수정

* [IDLE-000] firebase app 초기화 로직 주석 처리

* [IDLE-000] firebase app 초기화 로직 주석 처리

* [IDLE-000] firebase app 초기화 로직 주석 처리

* [IDLE-000] file 대신 string으로 주입받아 초기화하는 방식으로 전환

* [IDLE-000] 일반 string 대신 base64 인코딩 된 문자열을 주입하도록 처리

* [IDLE-000] 일반 string 주입으로 rollback

* [IDLE-000] json string log 추가

* [IDLE-000] base64 문자열로 재 변경

* [IDLE-000] firebase config의 현재 경로를 출력하도록 print문 추가

* [IDLE-429] DB 형상관리를 위한 Flyway 적용

* [IDLE-429] Flyway latest version으로 설정

* [IDLE-429] 피드백 반영

* [IDLE-000] firebase config의 현재 경로를 출력하도록 print문 추가

* [IDLE-000] 알림 조회 처리 로직에 @transactional 추가

* [IDLE-000] 알림 조회 시, 생성 시각 기준이 아닌 uuid v7 id 기준으로 내림차순 정렬하도록 변경

* [IDLE-000] CI 트리거 브랜치 복구

* [IDLE-454] 테스트를 위한 크롤링 수행 시각 변경 및 로깅 추가

* [IDLE-000] 공고 범위 검색 내 중복 데이터 발생 방지

* [IDLE-000] ci 트리거 브랜치 변경

* [IDLE-000] ci 트리거 브랜치 롤백

* [IDLE-000] monitoring 모듈 추가 및 actuator, prometheus 의존성 추가

* [IDLE-456] monitoring.yml 작성

* [IDLE-456] monitoring profile 추가

* [IDLE-456] actuator dependency group name 수정

* [IDLE-000] 크롤링 주기 하루 2회로 변경

* [IDLE-000] 불필요 로그 제거

* [IDLE-000] 인증번호 SMS 내용 수정

* [IDLE-000] classPath 하위 yaml 파일 확장자 모두 .yml로 통일

* [IDLE-000] 즐겨찾기 facade service에 트랜잭션 추가 (#206)

* [IDLE-000] firebase config의 현재 경로를 출력하도록 print문 추가

* [IDLE-000] facade service에 transaction 추가

* [IDLE-461] 유저가 다중 디바이스에서 알림을 받을 수 있도록 개선한다. (#207)

* [IDLE-000] firebase config의 현재 경로를 출력하도록 print문 추가

* [IDLE-461] 한 유저가 다중 디바이스 설정이 가능하도록 한다.

* [IDLE-000] firebase config의 현재 경로를 출력하도록 print문 추가

* [IDLE-000] presentation module의 gradle 파일에 monitoring 모듈 dependency 추가

* [IDLE-000] deviceToken 서비스에서 트랜잭션 어노테이션 추가

* [IDLE-000] batch 테스트를 위해 30분마다 동작하도록 변경

* [IDLE-475] 채팅, 채팅방 도메인 설계

* [IDLE-475] 채팅 메세지 최소, 최대 길이 제한 설정

* [IDLE-000] Batch 메타데이터 테이블 스크립트 변경 및 크롤링 수행 주기 설정

* [IDLE-000] 운영 환경 CD 스크립트에서 불필요한 step 제거

* [IDLE-000] 도텐브 파일 공백 제거 스크립트 작성, 크롤링 대상일자 전날 등록된 공고로 변경

* [IDLE-000] 크롤링 기준 시각 15시로 임시 변경

* [IDLE-000] cd env 파일 생성 스크립트 변경

* [IDLE-492] 동일 유저가 여러 개의 디바이스를 사용 시, 알림이 중복해서 누적되는 현상

* [IDLE-493] flyway 스크립트 오탈자 수정

* [IDLE-493] flyway 스크립트 오탈자 수정

* [IDLE-494] 요양 보호사 및 센터 프로필에서 긴 텍스트를 입력 가능한 항목을 TEXT 컬럼으로 지정한다.

* [IDLE-495] 요양 보호사는 마감된 공고에 지원이 불가능하다.

* [IDLE-495] 공고 마감 처리 메서드명 변경 complete -> completed

* [IDLE-496] 스프링 프로파일 지정을 위한 환경변수 주입

* [IDLE-496] 로그 일부 수정

* [IDLE-000] 안드로이드 app link를 위한 asset 추가

* [IDLE-190] 센터 관리자 인증 요청 목록 조회 API

* [IDLE-000] 크롤링 주기 변경 및 테스트용 에러 로그 추가

* [IDLE-000] 크롤링 대상 사이트에 알림창(alert)이 뜨는 케이스에 대한 처리

* [IDLE-509] 크롤링 전체 조회 시, 공고가 중복 노출되는 현상 해결

* [IDLE-509] 피드백 반영

* [IDLE-509] 사용하지 않는 하위 서비스 의존성 제거

* [IDLE-000] 크롤링 진행 시각 저녁 11시로 변경

* [IDLE-000] 크롤링 공고 즐겨찾기 조회 로직 버그 수정

* [IDLE-000] 요양 보호사 및 센터 관리자 전화번호 컬럼에 unique index 추가

* [IDLE-512] 센터 관리자 인증 요청 event 변경 및 NotificationInfo 인터페이스 패키지 이동

* [IDLE-512] 센터 관리자 인증 요청 event 변경 및 NotificationInfo 인터페이스 패키지 이동

* [IDLE-513] 요양 보호사 location 필드 추가 및 기존 데이터 마이그레이션

* [IDLE-513] 센터 관리자 공고 등록 시, 주변 요양보호사에게 FCM 알림을 일괄 전송한다.

* [IDLE-513] 센터 관리자 공고 등록 시, 주변 요양보호사에게 FCM 알림을 일괄 전송한다.

* [IDLE-000] notification type enum 속성 추가

* [IDLE-000] 센터 공고 등록 알림 제목 및 프로필 url null로 수정

* [IDLE-000] Readme 업데이트

* [IDLE-504] 센터 관리자 인증 승인 및 거절 API

* [IDLE-456] prometheus, grafana 설정을 위한 monitoring.yml 설정

* [IDLE-476] 웹소켓, Redis pub/sub을 이용한 채팅 전송 기능 (#217)

* [IDLE-476] 웹소켓 dependency 추가

* [IDLE-476] 웹소켓, Redis pub/sub을 이용한 채팅 전송 기능

* [IDLE-476] 불필요 클래스 제거

* [IDLE-476] 채팅 메세지 생성 책임을 하위 도메인에서 생성하도록 수정

* [IDLE-476] hash 역직렬화 시 필요한 처리를 Serializer 설정 추가

* [IDLE-476] json 역직렬화 시, 특수문자 허용

* [IDLE-476] websocket stomp 엔드포인트 노출 설정 변경

* [IDLE-476] 채팅 메세지 길이 정책 적용

* [IDLE-518] bastion서버를 통해 production 서버로 접근 후, 배포하도록 설정 (#252)

* [IDLE-518] ECR 레파지토리 변경 (#254)

* [IDLE-518] bastion서버를 통해 production 서버로 접근 후, 배포하도록 설정

* [IDLE-518] ECR 레파지토리 변경

* [IDLE-518] docker-compose 파일 위치 변경 (#256)

* [IDLE-518] bastion서버를 통해 production 서버로 접근 후, 배포하도록 설정

* [IDLE-518] ECR 레파지토리 변경

* [IDLE-518] docker-compose 파일 경로 변경

* [IDLE-518] 소스파일 경로 변경

* [IDLE-518] target 경로 변경

* [IDLE-518] 소스파일 경로 변경

* [IDLE-518] 타겟파일 경로 원상복구

* [IDLE-518] 타겟파일 경로 수정

* [IDLE-518] compose 파일 실행 경로 수정

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] Configuration Env file 스탭 추가

* [IDLE-518] secrets로 변경

* [IDLE-518] CI/CD재가동 전, 수동 실행으로 변경 (#257)

* [IDLE-518] bastion서버를 통해 production 서버로 접근 후, 배포하도록 설정

* [IDLE-518] ECR 레파지토리 변경

* [IDLE-518] docker-compose 파일 경로 변경

* [IDLE-518] 소스파일 경로 변경

* [IDLE-518] target 경로 변경

* [IDLE-518] 소스파일 경로 변경

* [IDLE-518] 타겟파일 경로 원상복구

* [IDLE-518] 타겟파일 경로 수정

* [IDLE-518] compose 파일 실행 경로 수정

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] Configuration Env file 스탭 추가

* [IDLE-518] secrets로 변경

* [IDLE-518] CI/CD 재가동 전 수동으로 수정

* [IDLE-518] CI/CD 재가동 전 수동으로 수정

* [IDLE-518] ECR 레파지토리 작성

* [IDLE-518] 서버 재구축 및 CI/CD 재가동 (#258)

* [IDLE-518] bastion서버를 통해 production 서버로 접근 후, 배포하도록 설정

* [IDLE-518] ECR 레파지토리 변경

* [IDLE-518] docker-compose 파일 경로 변경

* [IDLE-518] 소스파일 경로 변경

* [IDLE-518] target 경로 변경

* [IDLE-518] 소스파일 경로 변경

* [IDLE-518] 타겟파일 경로 원상복구

* [IDLE-518] 타겟파일 경로 수정

* [IDLE-518] compose 파일 실행 경로 수정

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] 하드코딩된 변수를 secrets로 변경

* [IDLE-518] Configuration Env file 스탭 추가

* [IDLE-518] secrets로 변경

* [IDLE-518] CI/CD 재가동 전 수동으로 수정

* [IDLE-518] CI/CD 재가동 전 수동으로 수정

* [IDLE-518] ECR 레파지토리 작성

* [IDLE-518] .env 파일 경로 변경

* [IDLE-518] 레지스트리 내용 변경

* [IDLE-518] DB 이름을 caremeet으로 변경

* [IDLE-518] 서비스 간 통신을 위한 컨테이너간 네트워크 설정 추가

* [IDLE-518] ddl create 적용

* [IDLE-518] private_key.pem 파일을 통해 접근하도록 수정

* [IDLE-518] 호스트키 체크 우회 수정

* [IDLE-518] SSH 세션에 터미널을 할당하도록 옵션 추가

* [IDLE-518] 터널링 방법을 수정

* [IDLE-518] Private Subnet에 존재하는 서버의 22번 포트를 로컬 2222 포트로 연결

* [IDLE-518] .env파일 생성하는 명령어 수정

* [IDLE-518] 들여쓰기 수정

* [IDLE-518] INPUT_으로 시작하는 항목, .env 파일에 제외

* [IDLE-518] null 값을 제외하는 처리를 추가

* [IDLE-518] 병합 전에 null 값을 제외하는 처리를 추가

* [IDLE-518] context 출력문 추가

* [IDLE-518] JSON 형식으로 변환한 후, SSH 스크립트 내부에서 이를 파일로 저장

* [IDLE-518] .env 파일에 INSTANCE_PEM_KEY가 들어가지 않도록 수정

* [IDLE-518] ddl을 validate으로 수정

* [IDLE-518] 브랜치 push시에 동작하도록 수정

* [IDLE-531] 배치 작업 정상화 (#260)

* [IDLE-534] Flayway 재설정 및 배치 메타데이터 테이블 생성

* [IDLE-548] Tasklet을 Chunk로 변경

* [IDLE-549] 배치 실행 API 추가

* [IDLE-533] GeoCodeService 전환 메서드 static으로 전환

* [IDLE-533] 책임별 클래스 분리

* [IDLE-547] 멀티스레드 적용 및 공유자원 분리

* [IDLE-531] 크롤링 기준 날짜 변경

---------

Co-authored-by: wonjunYou <[email protected]>
Co-authored-by: Wonjun You <[email protected]>
  • Loading branch information
3 people authored Jan 30, 2025
1 parent 6c6eb7c commit 4dee296
Show file tree
Hide file tree
Showing 27 changed files with 851 additions and 553 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.swm.idle.batch.common.scheduler
package com.swm.idle.batch.common.launcher

import com.swm.idle.batch.job.CrawlingJobConfig
import com.swm.idle.batch.job.JobConfig
import org.springframework.batch.core.JobParameters
import org.springframework.batch.core.JobParametersBuilder
import org.springframework.batch.core.configuration.JobRegistry
import org.springframework.batch.core.launch.JobLauncher
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class CrawlingJobScheduler(
class CrawlingJobLauncher(
private val jobLauncher: JobLauncher,
private val crawlingJobConfig: CrawlingJobConfig,
private val jobRegistry: JobRegistry,
private val crawlingJobConfig: JobConfig,
) {

@Scheduled(cron = "0 0 23 * * *")
Expand All @@ -22,4 +24,11 @@ class CrawlingJobScheduler(
jobLauncher.run(crawlingJobConfig.crawlingJob(), jobParameters)
}

fun jobStart() {
val jobParameters: JobParameters = JobParametersBuilder()
.addLong("timestamp", System.currentTimeMillis())
.toJobParameters()

jobLauncher.run(jobRegistry.getJob("crawlingJob"), jobParameters)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.swm.idle.batch.crawler

enum class CrawlerConsts(val location: String, val value: String) {
CRAWLING_TARGET_URL_FORMAT("CRAWLING_TARGET_URL_FORMAT","https://www.work24.go.kr/wk/a/b/1200/retriveDtlEmpSrchList.do?basicSetupYn=&careerTo=&keywordJobCd=&occupation=&seqNo=&cloDateEndtParam=&payGbn=&templateInfo=&rot2WorkYn=&shsyWorkSecd=&srcKeywordParam=%EC%9A%94%EC%96%91%EB%B3%B4%ED%98%B8%EC%82%AC&resultCnt=50&keywordJobCont=&cert=&moreButtonYn=Y&minPay=&codeDepth2Info=11000&currentPageNo=1&eventNo=&mode=&major=&resrDutyExcYn=&eodwYn=&sortField=DATE&staArea=&sortOrderBy=DESC&keyword=%EC%9A%94%EC%96%91%EB%B3%B4%ED%98%B8%EC%82%AC&termSearchGbn=all&carrEssYns=&benefitSrchAndOr=O&disableEmpHopeGbn=&actServExcYn=&keywordStaAreaNm=&maxPay=&emailApplyYn=&codeDepth1Info=11000&keywordEtcYn=&regDateStdtParam={yesterday}&publDutyExcYn=&keywordJobCdSeqNo=&viewType=&exJobsCd=&templateDepthNmInfo=&region=&employGbn=&empTpGbcd=&computerPreferential=&infaYn=&cloDateStdtParam=&siteClcd=WORK&searchMode=Y&birthFromYY=&indArea=&careerTypes=&subEmpHopeYn=&tlmgYn=&academicGbn=&templateDepthNoInfo=&foriegn=&entryRoute=&mealOfferClcd=&basicSetupYnChk=&station=&holidayGbn=&srcKeyword=%EC%9A%94%EC%96%91%EB%B3%B4%ED%98%B8%EC%82%AC&academicGbnoEdu=noEdu&enterPriseGbn=all&cloTermSearchGbn=all&birthToYY=&keywordWantedTitle=&stationNm=&benefitGbn=&notSrcKeywordParam=&keywordFlag=&notSrcKeyword=&essCertChk=&depth2SelCode=&keywordBusiNm=&preferentialGbn=&rot3WorkYn=&regDateEndtParam={yesterday}&pfMatterPreferential=&pageIndex={pageIndex}&termContractMmcnt=&careerFrom=&laborHrShortYn=#scrollLoc"),
JOB_POSTING_COUNT_PER_PAGE("JOB_POSTING_COUNT_PER_PAGE","50"),
JOB_POSTING_COUNT("JOB_POSTING_COUNT","//*[@id=\"mForm\"]/div[2]/div/div[1]/div[1]/span/span"),

//공고 정보
TITLE("TITLE", "//*[@id=\"contents\"]/div/div/div/div[1]/div[3]/div[1]/div[1]/strong"),
CONTENT("CONTENT", "//*[@id=\"tab-panel01\"]/div[1]/div"),

//근무 정보
PAY_INFO("PAY_INFO", "//*[@id=\"tab-panel02\"]/div/table/tbody/tr[1]/td[2]"),
WORK_TIME("WORK_TIME","//*[@id=\"tab-panel02\"]/div/table/tbody/tr[2]/td"),
WORK_SCHEDULE("WORK_SCHEDULE","//*[@id=\"tab-panel02\"]/div/table/tbody/tr[3]/td[2]"),

//모집 정보
RECRUITMENT_PROCESS("RECRUITMENT_PROCESS","//*[@id=\"tab-panel05\"]/div[2]/div/div[2]/p[1]"),
REQUIRED_DOCUMENT("REQUIRED_DOCUMENT","//*[@id=\"tab-panel05\"]/div[2]/div/div[2]/p[2]"),
APPLY_METHOD("APPLY_METHOD","//*[@id=\"tab-panel05\"]/div[2]/div/div[2]/p[1]"),
APPLY_DEADLINE("APPLY_DEADLINE","//*[@id=\"tab-panel05\"]/div[2]/div/div[1]/div[1]/p"),
CREATED_AT("CREATED_AT","//*[@id=\"contents\"]/div/div/div/div[1]/div[5]/div[11]/div[2]/table/tbody/tr[1]/td[1]"),

//센터 정보
CENTER_NAME("CENTER_NAME","//*[@id=\"contents\"]/div/div/div/div[1]/div[3]/div[1]/div[1]/p/strong"),
CENTER_ADDRESS1("CENTER_ADDRESS1","//*[@id=\"tab-panel02\"]/div/table/tbody/tr[5]/td/div[1]/p"),
CENTER_ADDRESS2("CENTER_ADDRESs2","//*[@id=\"tab-panel02\"]/div/table/tbody/tr[5]/td/div[1]/p"),
CENTER_ADDRESS3("CENTER_ADDRESS3","//*[@id=\"tab-panel02\"]/div/table/tbody/tr[5]/td/div[1]/p"),

//노인 주소
CLIENT_ADDRESS1("CLIENT_ADDRESS1","//*[@id=\"tab-panel02\"]/div/table/tbody/tr[5]/td/div[1]/p"),
CLIENT_ADDRESS2("CLIENT_ADDRESS2","//*[@id=\"tab-panel02\"]/div/table/tbody/tr[5]/td/div[1]/p"),

//ChromDriver-Options
HEADLESS("HEADLESS","--headless"),
NO_SANDBOX("NO_SANDBOX","--no-sandbox"),
DISABLE_DEV_SHM_USAGE("DISABLE_DEV_SHM_USAGE","--disable-dev-shm-usage"),
DISABLE_GPU("DISABLE_GPU","--disable-gpu"),
WINDOW_SIZE("WINDOW_SIZE","window-size=1920x1080"),
DISABLE_SOFTWARE_RASTERIZER("DISABLE_SOFTWARE_RASTERIZER","--disable-software-rasterizer"),
IGNORE_SSL_ERRORS("IGNORE_SSL_ERRORS","--ignore-ssl-errors=yes"),
IGNORE_CERTIFICATE_ERRORS("IGNORE_CERTIFICATE_ERRORS","--ignore-certificate-errors");

companion object {
fun getChromOptions(): Array<String> {
return arrayOf(
HEADLESS.value,
NO_SANDBOX.value,
DISABLE_DEV_SHM_USAGE.value,
DISABLE_GPU.value,
WINDOW_SIZE.value,
DISABLE_SOFTWARE_RASTERIZER.value,
IGNORE_SSL_ERRORS.value,
IGNORE_CERTIFICATE_ERRORS.value
)
}
}

fun getIntValue(): Int {
return value.toInt()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.swm.idle.batch.crawler

import io.github.oshai.kotlinlogging.KotlinLogging
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeDriverService
import org.openqa.selenium.chrome.ChromeOptions
import java.io.File

object DriverInitializer {
private val logger = KotlinLogging.logger { }

fun init(): ChromeDriver {
return runCatching {
ChromeDriver(
ChromeDriverService.Builder()
.usingDriverExecutable(File(System.getenv("CHROMEDRIVER_BIN")))
.build()
.also { logger.info { System.getenv("CHROMEDRIVER_BIN") } },
ChromeOptions().apply {
addArguments(*CrawlerConsts.getChromOptions())
setBinary(System.getenv("CHROME_BIN"))
}.also { logger.info { System.getenv("CHROME_BIN")} }
)
}.getOrElse {
logger.error { "ChromeDriver initialization failed: ${it.message}" }
throw RuntimeException("ChromeDriver initialization failed, application will exit.") // 이후 코드가 실행되지 않도록 예외 던짐
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.swm.idle.batch.crawler

import com.swm.idle.batch.step.PostingReader
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait
import java.time.Duration
import java.time.LocalDate
import java.time.format.DateTimeFormatter

class WorknetPageCrawler {
private var driver: WebDriver = DriverInitializer.init()

fun initCounts(reader: PostingReader) {
reader.crawlingUrl = CrawlerConsts.CRAWLING_TARGET_URL_FORMAT.value
.replace("{yesterday}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")))
.replace("{pageIndex}", "1")

moveToPage(reader)

reader.postingCount = driver
.findElement(By.xpath(CrawlerConsts.JOB_POSTING_COUNT.value))
.text.toInt()
.takeIf { it > 0 }
?: run {
driver.quit()
throw Exception("크롤링 할 공고가 없습니다.")
}

reader.pageCount = (reader.postingCount + CrawlerConsts.JOB_POSTING_COUNT_PER_PAGE.getIntValue() - 1) /
CrawlerConsts.JOB_POSTING_COUNT_PER_PAGE.getIntValue()
reader.lastPageJobPostingCount = reader.postingCount % CrawlerConsts.JOB_POSTING_COUNT_PER_PAGE.getIntValue()
driver.quit()
}

private fun moveToPage(reader: PostingReader) {
driver.get(reader.crawlingUrl)
WebDriverWait(driver, Duration.ofSeconds(10))
.also {
it.until(ExpectedConditions.visibilityOfElementLocated(By.xpath(CrawlerConsts.JOB_POSTING_COUNT.value)))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.swm.idle.batch.crawler

import com.swm.idle.batch.common.dto.CrawledJobPostingDto
import io.github.oshai.kotlinlogging.KotlinLogging
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait
import java.time.Duration
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import org.openqa.selenium.WebElement

class WorknetPostCrawler {
private val logger = KotlinLogging.logger { }
private var driver: WebDriver = DriverInitializer.init()
private var errorCountMap: MutableMap<String, Int> = mutableMapOf()

fun crawlPosts(end: Int, url: String): List<CrawledJobPostingDto> {
moveToPage(url)

val crawledPostings = mutableListOf<CrawledJobPostingDto>()
repeat(end) { i ->
val originalWindow = driver.windowHandle
val titleElement = findElementSafe(By.xpath("//*[@id=\"list${i+1}\"]/td[1]/div/div[2]/a")) ?: return@repeat

moveToPostDetailWindow(titleElement, originalWindow)

try {
val post: CrawledJobPostingDto = createPost()
crawledPostings.add(post)
} catch (e: Exception) {
logger.warn { "실패" }
}

backWindow(originalWindow)
}
errorCountMap.asSequence().forEach { (key, value) -> println("$key -> $value") }
driver.quit()
return crawledPostings
}

private fun moveToPage(url: String) {
driver.get(url)
WebDriverWait(driver, Duration.ofSeconds(10))
.until(
ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#list1"))
)
}

private fun createPost(): CrawledJobPostingDto {
return CrawledJobPostingDto(
title = extractText(CrawlerConsts.TITLE),
content = extractText(CrawlerConsts.CONTENT),
createdAt = extractText(CrawlerConsts.CREATED_AT),
payInfo = extractText(CrawlerConsts.PAY_INFO),
workSchedule = extractText(CrawlerConsts.WORK_SCHEDULE),
recruitmentProcess = extractText(CrawlerConsts.RECRUITMENT_PROCESS),
applyMethod = extractText(CrawlerConsts.APPLY_METHOD),
requiredDocument = extractText(CrawlerConsts.REQUIRED_DOCUMENT),
centerName = extractText(CrawlerConsts.CENTER_NAME),
applyDeadline = extractApplyDeadline(CrawlerConsts.APPLY_DEADLINE),
workTime = extractWorkTime(CrawlerConsts.WORK_TIME),
centerAddress = extractAddress(
CrawlerConsts.CLIENT_ADDRESS1,
CrawlerConsts.CLIENT_ADDRESS2
),
clientAddress = extractAddress(
CrawlerConsts.CENTER_ADDRESS1,
CrawlerConsts.CENTER_ADDRESS2,
CrawlerConsts.CENTER_ADDRESS3
),
directUrl = driver.currentUrl
)
}


private inline fun <T> errorRecord(location: String, action: () -> T): T {
return runCatching { action() }
.getOrElse { e ->
logError(location)
throw e
}
}

private fun findElementSafe(by: By): WebElement? {
return runCatching { driver.findElement(by) }.getOrNull()
}

private fun moveToPostDetailWindow(titleElement: WebElement, originalWindow: String) {
titleElement.click()
WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.numberOfWindowsToBe(2))
driver.switchTo().window(driver.windowHandles.first { it != originalWindow })
}

private fun extractText(con: CrawlerConsts): String {
return errorRecord(con.location) { driver.findElement(By.xpath(con.value)).text }
}

private fun extractApplyDeadline(con: CrawlerConsts): String {
return errorRecord(con.location) {
driver.findElement(By.xpath(con.value)).text.let {
if (it.contains("채용시까지"))
LocalDate.now().plusDays(15).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
else
it
}
}
}

private fun extractAddress(vararg cons: CrawlerConsts): String {
for (con in cons) {
runCatching {
val address = driver.findElement(By.xpath(con.value)).text
return address.replace("지도보기", "").trim().replace(Regex("\\(\\d{5}\\)"), "").trim()
} .getOrElse { e ->
logError(con.location)
throw e
}
}
throw NoSuchElementException("Center address not found using any of the provided XPaths")
}

private fun extractWorkTime(con: CrawlerConsts): String {
return errorRecord(con.location) {
driver.findElement(By.xpath(con.value)).text
.replace("도움말", "")
.replace("(근무시간)", "")
.replace("\n", "")
}
}

private fun logError(location: String) {
errorCountMap[location] = errorCountMap.getOrDefault(location, 0) + 1
}

private fun backWindow(originalWindow: String?) {
driver.close()
driver.switchTo().window(originalWindow)
}
}

This file was deleted.

Loading

0 comments on commit 4dee296

Please sign in to comment.