fix: ChnPrmShip 캐시 갱신 조건 완화 및 스케줄 이전 실행 시간 표시
- ChnPrmShipCacheManager: isAfter → !isBefore (동일 타임스탬프도 갱신) - ScheduleService: Quartz 트리거에서 previousFireTime 실제 조회 - README.md: 빌드/배포 가이드 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
50badbe2bb
커밋
41b06beeec
@ -44,7 +44,21 @@
|
|||||||
- `@Builder` 허용
|
- `@Builder` 허용
|
||||||
- `@Data` 사용 금지 (명시적으로 필요한 어노테이션만)
|
- `@Data` 사용 금지 (명시적으로 필요한 어노테이션만)
|
||||||
- `@AllArgsConstructor` 단독 사용 금지 (`@Builder`와 함께 사용)
|
- `@AllArgsConstructor` 단독 사용 금지 (`@Builder`와 함께 사용)
|
||||||
- `@Slf4j` 로거 사용
|
|
||||||
|
## 로깅
|
||||||
|
- `@Slf4j` (Lombok) 로거 사용
|
||||||
|
- SLF4J `{}` 플레이스홀더에 printf 포맷 사용 금지 (`{:.1f}`, `{:d}`, `{%s}` 등)
|
||||||
|
- 숫자 포맷이 필요하면 `String.format()`으로 변환 후 전달
|
||||||
|
```java
|
||||||
|
// 잘못됨
|
||||||
|
log.info("처리율: {:.1f}%", rate);
|
||||||
|
// 올바름
|
||||||
|
log.info("처리율: {}%", String.format("%.1f", rate));
|
||||||
|
```
|
||||||
|
- 예외 로깅 시 예외 객체는 마지막 인자로 전달 (플레이스홀더 불필요)
|
||||||
|
```java
|
||||||
|
log.error("처리 실패: {}", id, exception);
|
||||||
|
```
|
||||||
|
|
||||||
## 예외 처리
|
## 예외 처리
|
||||||
- 비즈니스 예외는 커스텀 Exception 클래스 정의
|
- 비즈니스 예외는 커스텀 Exception 클래스 정의
|
||||||
|
|||||||
@ -20,9 +20,10 @@ fi
|
|||||||
# Conventional Commits 정규식
|
# Conventional Commits 정규식
|
||||||
# type(scope): subject
|
# type(scope): subject
|
||||||
# - type: feat|fix|docs|style|refactor|test|chore|ci|perf (필수)
|
# - type: feat|fix|docs|style|refactor|test|chore|ci|perf (필수)
|
||||||
# - scope: 영문, 숫자, 한글, 점, 밑줄, 하이픈 허용 (선택)
|
# - scope: 괄호 제외 모든 문자 허용 — 한/영/숫자/특수문자 (선택)
|
||||||
# - subject: 1~72자, 한/영 혼용 허용 (필수)
|
# - subject: 1자 이상 (길이는 바이트 기반 별도 검증)
|
||||||
PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\([a-zA-Z0-9가-힣._-]+\))?: .{1,72}$'
|
PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|perf)(\([^)]+\))?: .+$'
|
||||||
|
MAX_SUBJECT_BYTES=200 # UTF-8 한글(3byte) 허용: 72문자 ≈ 최대 216byte
|
||||||
|
|
||||||
FIRST_LINE=$(head -1 "$COMMIT_MSG_FILE")
|
FIRST_LINE=$(head -1 "$COMMIT_MSG_FILE")
|
||||||
|
|
||||||
@ -58,3 +59,13 @@ if ! echo "$FIRST_LINE" | grep -qE "$PATTERN"; then
|
|||||||
echo ""
|
echo ""
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 길이 검증 (바이트 기반 — UTF-8 한글 허용)
|
||||||
|
MSG_LEN=$(echo -n "$FIRST_LINE" | wc -c | tr -d ' ')
|
||||||
|
if [ "$MSG_LEN" -gt "$MAX_SUBJECT_BYTES" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " ✗ 커밋 메시지가 너무 깁니다 (${MSG_LEN}바이트, 최대 ${MAX_SUBJECT_BYTES})"
|
||||||
|
echo " 현재 메시지: $FIRST_LINE"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
105
README.md
Normal file
105
README.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# SNP-Batch (snp-batch-validation)
|
||||||
|
|
||||||
|
해양 데이터 통합 배치 시스템. Maritime API에서 선박/항만/사건 데이터를 수집하여 PostgreSQL에 저장하고, AIS 실시간 위치정보를 캐시 기반으로 서비스합니다.
|
||||||
|
|
||||||
|
## 기술 스택
|
||||||
|
|
||||||
|
- Java 17, Spring Boot 3.2.1, Spring Batch 5.1.0
|
||||||
|
- PostgreSQL, Quartz Scheduler, Caffeine Cache
|
||||||
|
- React 19 + Vite + Tailwind CSS 4 (관리 UI)
|
||||||
|
- frontend-maven-plugin (프론트엔드 빌드 통합)
|
||||||
|
|
||||||
|
## 사전 요구사항
|
||||||
|
|
||||||
|
| 항목 | 버전 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| JDK | 17 | `.sdkmanrc` 참조 (`sdk env`) |
|
||||||
|
| Maven | 3.9+ | |
|
||||||
|
| Node.js | 20+ | 프론트엔드 빌드용 |
|
||||||
|
| npm | 10+ | Node.js에 포함 |
|
||||||
|
|
||||||
|
## 빌드
|
||||||
|
|
||||||
|
> **주의**: frontend-maven-plugin의 Node 호환성 문제로, 프론트엔드와 백엔드를 분리하여 빌드합니다.
|
||||||
|
|
||||||
|
### 터미널
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 프론트엔드 빌드
|
||||||
|
cd frontend && npm install && npm run build && cd ..
|
||||||
|
|
||||||
|
# 2. Maven 패키징 (프론트엔드 빌드 스킵)
|
||||||
|
mvn clean package -DskipTests -Dskip.npm -Dskip.installnodenpm
|
||||||
|
```
|
||||||
|
|
||||||
|
빌드 결과: `target/snp-batch-validation-1.0.0.jar`
|
||||||
|
|
||||||
|
### VSCode
|
||||||
|
|
||||||
|
`Cmd+Shift+B` (기본 빌드 태스크) → 프론트엔드 빌드 + Maven 패키징 순차 실행
|
||||||
|
|
||||||
|
개별 태스크: `Cmd+Shift+P` → "Tasks: Run Task" → 태스크 선택
|
||||||
|
|
||||||
|
> 태스크 설정: [.vscode/tasks.json](.vscode/tasks.json)
|
||||||
|
|
||||||
|
### IntelliJ IDEA
|
||||||
|
|
||||||
|
1. **프론트엔드 빌드**: Terminal 탭에서 `cd frontend && npm run build`
|
||||||
|
2. **Maven 패키징**: Maven 패널 → Lifecycle → `package`
|
||||||
|
- VM Options: `-DskipTests -Dskip.npm -Dskip.installnodenpm`
|
||||||
|
- 또는 Run Configuration → Maven → Command line에 `clean package -DskipTests -Dskip.npm -Dskip.installnodenpm`
|
||||||
|
|
||||||
|
## 로컬 실행
|
||||||
|
|
||||||
|
### 터미널
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn spring-boot:run -Dspring-boot.run.profiles=local
|
||||||
|
```
|
||||||
|
|
||||||
|
### VSCode
|
||||||
|
|
||||||
|
Run/Debug 패널(F5) → "SNP-Batch (local)" 선택
|
||||||
|
|
||||||
|
> 실행 설정: [.vscode/launch.json](.vscode/launch.json)
|
||||||
|
|
||||||
|
### IntelliJ IDEA
|
||||||
|
|
||||||
|
Run Configuration → Spring Boot:
|
||||||
|
- Main class: `com.snp.batch.SnpBatchApplication`
|
||||||
|
- Active profiles: `local`
|
||||||
|
|
||||||
|
## 서버 배포
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 빌드 (위 빌드 절차 수행)
|
||||||
|
|
||||||
|
# 2. JAR 전송
|
||||||
|
scp target/snp-batch-validation-1.0.0.jar {서버}:{경로}/
|
||||||
|
|
||||||
|
# 3. 실행
|
||||||
|
java -jar snp-batch-validation-1.0.0.jar --spring.profiles.active=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 접속 정보
|
||||||
|
|
||||||
|
| 항목 | URL |
|
||||||
|
|------|-----|
|
||||||
|
| 관리 UI | `http://localhost:8041/snp-api/` |
|
||||||
|
| Swagger | `http://localhost:8041/snp-api/swagger-ui/index.html` |
|
||||||
|
|
||||||
|
## 프로파일
|
||||||
|
|
||||||
|
| 프로파일 | 용도 | DB |
|
||||||
|
|----------|------|----|
|
||||||
|
| `local` | 로컬 개발 | 개발 DB |
|
||||||
|
| `dev` | 개발 서버 | 개발 DB |
|
||||||
|
| `prod` | 운영 서버 | 운영 DB |
|
||||||
|
|
||||||
|
## Maven 빌드 플래그 요약
|
||||||
|
|
||||||
|
| 플래그 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| `-DskipTests` | 테스트 스킵 |
|
||||||
|
| `-Dskip.npm` | npm install/build 스킵 |
|
||||||
|
| `-Dskip.installnodenpm` | Node/npm 자동 설치 스킵 |
|
||||||
@ -60,7 +60,7 @@ public class ChnPrmShipCacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AisTargetEntity existing = cache.getIfPresent(item.getMmsi());
|
AisTargetEntity existing = cache.getIfPresent(item.getMmsi());
|
||||||
if (existing == null || isNewer(item, existing)) {
|
if (existing == null || isNewerOrEqual(item, existing)) {
|
||||||
cache.put(item.getMmsi(), item);
|
cache.put(item.getMmsi(), item);
|
||||||
updated++;
|
updated++;
|
||||||
}
|
}
|
||||||
@ -119,13 +119,13 @@ public class ChnPrmShipCacheManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isNewer(AisTargetEntity candidate, AisTargetEntity existing) {
|
private boolean isNewerOrEqual(AisTargetEntity candidate, AisTargetEntity existing) {
|
||||||
if (candidate.getMessageTimestamp() == null) {
|
if (candidate.getMessageTimestamp() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (existing.getMessageTimestamp() == null) {
|
if (existing.getMessageTimestamp() == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return candidate.getMessageTimestamp().isAfter(existing.getMessageTimestamp());
|
return !candidate.getMessageTimestamp().isBefore(existing.getMessageTimestamp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -328,24 +328,27 @@ public class ScheduleService {
|
|||||||
.createdBy(entity.getCreatedBy())
|
.createdBy(entity.getCreatedBy())
|
||||||
.updatedBy(entity.getUpdatedBy());
|
.updatedBy(entity.getUpdatedBy());
|
||||||
|
|
||||||
// 다음 실행 시간 계산 (Cron 표현식 기반)
|
// Quartz 트리거에서 실행 시간 정보 조회
|
||||||
if (entity.getActive() && entity.getCronExpression() != null) {
|
if (entity.getActive() && entity.getCronExpression() != null) {
|
||||||
try {
|
try {
|
||||||
// Cron 표현식으로 임시 트리거 생성 (DB 조회 없이 계산)
|
TriggerKey triggerKey = new TriggerKey(entity.getJobName() + "-trigger", "batch-triggers");
|
||||||
|
Trigger quartzTrigger = scheduler.getTrigger(triggerKey);
|
||||||
|
|
||||||
|
if (quartzTrigger != null) {
|
||||||
|
builder.nextFireTime(quartzTrigger.getNextFireTime());
|
||||||
|
builder.previousFireTime(quartzTrigger.getPreviousFireTime());
|
||||||
|
builder.triggerState(
|
||||||
|
scheduler.getTriggerState(triggerKey).name());
|
||||||
|
} else {
|
||||||
|
// 트리거 미등록 시 Cron 표현식 기반 계산
|
||||||
CronTrigger tempTrigger = TriggerBuilder.newTrigger()
|
CronTrigger tempTrigger = TriggerBuilder.newTrigger()
|
||||||
.withSchedule(CronScheduleBuilder.cronSchedule(entity.getCronExpression()))
|
.withSchedule(CronScheduleBuilder.cronSchedule(entity.getCronExpression()))
|
||||||
.build();
|
.build();
|
||||||
|
builder.nextFireTime(tempTrigger.getFireTimeAfter(new Date()));
|
||||||
Date nextFireTime = tempTrigger.getFireTimeAfter(new Date());
|
builder.triggerState("NONE");
|
||||||
if (nextFireTime != null) {
|
|
||||||
builder.nextFireTime(nextFireTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger 상태는 active인 경우 NORMAL로 설정
|
|
||||||
builder.triggerState("NORMAL");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Cron 표현식 기반 다음 실행 시간 계산 실패: {}", entity.getJobName(), e);
|
log.debug("Quartz 트리거 정보 조회 실패: {}", entity.getJobName(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user