feat: 자동갱신 cron + react-router 제거
- node-cron 30분 간격 배송 자동 조회 (DELIVERED 제외) - 상태 변경 시 로그 기록 - react-router-dom 제거 (다이얼로그 전환으로 불필요) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
458efe3e5d
commit
edc3a2c3d7
5 changed files with 109 additions and 9 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { buildApp } from './app.js';
|
import { buildApp } from './app.js';
|
||||||
import config from './config/index.js';
|
import config from './config/index.js';
|
||||||
|
import { startCronJobs } from './services/cron.js';
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
const app = await buildApp();
|
const app = await buildApp();
|
||||||
|
|
@ -10,6 +11,8 @@ async function start() {
|
||||||
host: config.server.host,
|
host: config.server.host,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
startCronJobs(app);
|
||||||
|
|
||||||
app.log.info(`서버 시작: http://${config.server.host}:${config.server.port}`);
|
app.log.info(`서버 시작: http://${config.server.host}:${config.server.port}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
app.log.error(err);
|
app.log.error(err);
|
||||||
|
|
|
||||||
101
backend/src/services/cron.js
Normal file
101
backend/src/services/cron.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import cron from 'node-cron';
|
||||||
|
|
||||||
|
function toMysqlDatetime(isoString) {
|
||||||
|
if (!isoString) return null;
|
||||||
|
const d = new Date(isoString);
|
||||||
|
if (isNaN(d.getTime())) return null;
|
||||||
|
return d.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startCronJobs(fastify) {
|
||||||
|
// 30분 간격으로 배송중인 택배 자동 조회
|
||||||
|
cron.schedule('*/30 * * * *', async () => {
|
||||||
|
try {
|
||||||
|
const [parcels] = await fastify.db.execute(
|
||||||
|
"SELECT * FROM parcels WHERE status != 'DELIVERED'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parcels.length === 0) return;
|
||||||
|
|
||||||
|
fastify.log.info(`[cron] 배송중 택배 ${parcels.length}건 자동 조회 시작`);
|
||||||
|
|
||||||
|
for (const parcel of parcels) {
|
||||||
|
try {
|
||||||
|
const result = await fastify.tracker.track(
|
||||||
|
parcel.carrier_id,
|
||||||
|
parcel.tracking_number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
await fastify.db.execute(
|
||||||
|
'UPDATE parcels SET last_checked_at = NOW() WHERE id = ?',
|
||||||
|
[parcel.id]
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = result.lastEvent?.status || 'UNKNOWN';
|
||||||
|
const lastDetail = result.lastEvent?.description || '';
|
||||||
|
const deliveredAt =
|
||||||
|
status === 'DELIVERED'
|
||||||
|
? toMysqlDatetime(result.lastEvent?.time)
|
||||||
|
: null;
|
||||||
|
const prevStatus = parcel.status;
|
||||||
|
|
||||||
|
await fastify.db.execute(
|
||||||
|
`UPDATE parcels SET status = ?, last_detail = ?, last_checked_at = NOW(),
|
||||||
|
delivered_at = COALESCE(?, delivered_at),
|
||||||
|
sender_name = COALESCE(?, sender_name),
|
||||||
|
recipient_name = COALESCE(?, recipient_name)
|
||||||
|
WHERE id = ?`,
|
||||||
|
[
|
||||||
|
status,
|
||||||
|
lastDetail,
|
||||||
|
deliveredAt,
|
||||||
|
result.senderName,
|
||||||
|
result.recipientName,
|
||||||
|
parcel.id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 이벤트 갱신
|
||||||
|
await fastify.db.execute(
|
||||||
|
'DELETE FROM tracking_events WHERE parcel_id = ?',
|
||||||
|
[parcel.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const event of result.events) {
|
||||||
|
await fastify.db.execute(
|
||||||
|
`INSERT INTO tracking_events (parcel_id, status, status_name, description, location, event_time)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
parcel.id,
|
||||||
|
event.status,
|
||||||
|
event.statusName,
|
||||||
|
event.description,
|
||||||
|
event.location,
|
||||||
|
toMysqlDatetime(event.time),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevStatus !== status) {
|
||||||
|
fastify.log.info(
|
||||||
|
`[cron] ${parcel.carrier_name} ${parcel.tracking_number}: ${prevStatus} → ${status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
fastify.log.warn(
|
||||||
|
`[cron] 조회 실패 (${parcel.tracking_number}): ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fastify.log.info(`[cron] 자동 조회 완료`);
|
||||||
|
} catch (err) {
|
||||||
|
fastify.log.error(`[cron] 오류: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.log.info('[cron] 자동 조회 스케줄러 시작 (30분 간격)');
|
||||||
|
}
|
||||||
|
|
@ -171,10 +171,9 @@ CREATE TABLE tracking_events (
|
||||||
- React Query로 데이터 페칭 (택배 목록, 상세, 택배사 목록)
|
- React Query로 데이터 페칭 (택배 목록, 상세, 택배사 목록)
|
||||||
- 등록/수정/삭제/새로고침 mutation + 캐시 무효화
|
- 등록/수정/삭제/새로고침 mutation + 캐시 무효화
|
||||||
- 택배사 로고를 RustFS URL에서 로드
|
- 택배사 로고를 RustFS URL에서 로드
|
||||||
- [ ] 자동갱신 cron 서비스 (node-cron)
|
- [x] 자동갱신 cron 서비스 (node-cron, 30분 간격)
|
||||||
- 배송 중인 택배: 30분 간격 자동 조회
|
- 배송 완료되지 않은 택배만 자동 조회 (DELIVERED 제외)
|
||||||
- 배송 완료된 택배: 조회 중단
|
- 상태 변경 시 로그 기록
|
||||||
- 상태 변경 감지 시 알림 트리거
|
|
||||||
- [ ] 알림 서비스
|
- [ ] 알림 서비스
|
||||||
- Discord Webhook 또는 Telegram Bot API
|
- Discord Webhook 또는 Telegram Bot API
|
||||||
- 설정 가능한 Webhook URL (.env)
|
- 설정 가능한 Webhook URL (.env)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.22.3",
|
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
@ -18,9 +17,7 @@ const queryClient = new QueryClient({
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<App />
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue