2026-03-24 18:57:42 +09:00
|
|
|
export default async function parcelRoutes(fastify) {
|
2026-03-24 19:36:40 +09:00
|
|
|
// 전체 택배 목록 (페이징)
|
2026-03-24 18:57:42 +09:00
|
|
|
fastify.get('/', async (request) => {
|
2026-03-24 19:36:40 +09:00
|
|
|
const { status, page = 1, limit = 20 } = request.query;
|
|
|
|
|
const offset = (Math.max(1, Number(page)) - 1) * Number(limit);
|
|
|
|
|
const lim = Math.min(100, Math.max(1, Number(limit)));
|
|
|
|
|
|
|
|
|
|
let where = '';
|
2026-03-24 18:57:42 +09:00
|
|
|
const params = [];
|
|
|
|
|
|
|
|
|
|
if (status === 'active') {
|
2026-03-24 19:36:40 +09:00
|
|
|
where = 'WHERE status != ?';
|
2026-03-24 18:57:42 +09:00
|
|
|
params.push('DELIVERED');
|
|
|
|
|
} else if (status === 'delivered') {
|
2026-03-24 19:36:40 +09:00
|
|
|
where = 'WHERE status = ?';
|
2026-03-24 18:57:42 +09:00
|
|
|
params.push('DELIVERED');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 19:36:40 +09:00
|
|
|
const [[{ total }]] = await fastify.db.execute(
|
|
|
|
|
`SELECT COUNT(*) as total FROM parcels ${where}`,
|
|
|
|
|
params
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const [rows] = await fastify.db.execute(
|
|
|
|
|
`SELECT * FROM parcels ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
|
|
|
|
[...params, String(lim), String(offset)]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: rows,
|
|
|
|
|
pagination: {
|
|
|
|
|
page: Number(page),
|
|
|
|
|
limit: lim,
|
|
|
|
|
total,
|
|
|
|
|
totalPages: Math.ceil(total / lim),
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-03-24 18:57:42 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 운송장 등록
|
|
|
|
|
fastify.post('/', async (request, reply) => {
|
|
|
|
|
const { carrierId, trackingNumber, label } = request.body;
|
|
|
|
|
|
|
|
|
|
// 택배사 조회
|
|
|
|
|
const [carriers] = await fastify.db.execute(
|
|
|
|
|
'SELECT id, name FROM carriers WHERE id = ?',
|
|
|
|
|
[carrierId]
|
|
|
|
|
);
|
|
|
|
|
if (carriers.length === 0) {
|
|
|
|
|
return reply.code(400).send({ error: '지원하지 않는 택배사입니다' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const carrier = carriers[0];
|
|
|
|
|
|
|
|
|
|
// 중복 확인
|
|
|
|
|
const [existing] = await fastify.db.execute(
|
|
|
|
|
'SELECT id FROM parcels WHERE carrier_id = ? AND tracking_number = ?',
|
|
|
|
|
[carrierId, trackingNumber]
|
|
|
|
|
);
|
|
|
|
|
if (existing.length > 0) {
|
|
|
|
|
return reply.code(409).send({ error: '이미 등록된 운송장입니다' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 등록
|
|
|
|
|
const [result] = await fastify.db.execute(
|
|
|
|
|
'INSERT INTO parcels (carrier_id, carrier_name, tracking_number, label) VALUES (?, ?, ?, ?)',
|
|
|
|
|
[carrierId, carrier.name, trackingNumber, label || null]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const parcelId = result.insertId;
|
|
|
|
|
|
|
|
|
|
// 즉시 배송 조회
|
|
|
|
|
try {
|
|
|
|
|
await refreshParcel(fastify, parcelId);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fastify.log.warn(`등록 후 조회 실패: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [parcels] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM parcels WHERE id = ?',
|
|
|
|
|
[parcelId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return reply.code(201).send(parcels[0]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 개별 택배 상세 + 추적 이벤트
|
|
|
|
|
fastify.get('/:id', async (request, reply) => {
|
|
|
|
|
const { id } = request.params;
|
|
|
|
|
|
|
|
|
|
const [parcels] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM parcels WHERE id = ?',
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
if (parcels.length === 0) {
|
|
|
|
|
return reply.code(404).send({ error: '택배를 찾을 수 없습니다' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [events] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM tracking_events WHERE parcel_id = ? ORDER BY event_time ASC',
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { ...parcels[0], events };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 별칭 수정
|
|
|
|
|
fastify.put('/:id', async (request, reply) => {
|
|
|
|
|
const { id } = request.params;
|
|
|
|
|
const { label } = request.body;
|
|
|
|
|
|
|
|
|
|
const [result] = await fastify.db.execute(
|
|
|
|
|
'UPDATE parcels SET label = ? WHERE id = ?',
|
|
|
|
|
[label, id]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result.affectedRows === 0) {
|
|
|
|
|
return reply.code(404).send({ error: '택배를 찾을 수 없습니다' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [parcels] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM parcels WHERE id = ?',
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
return parcels[0];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 운송장 삭제
|
|
|
|
|
fastify.delete('/:id', async (request, reply) => {
|
|
|
|
|
const { id } = request.params;
|
|
|
|
|
|
|
|
|
|
const [result] = await fastify.db.execute(
|
|
|
|
|
'DELETE FROM parcels WHERE id = ?',
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result.affectedRows === 0) {
|
|
|
|
|
return reply.code(404).send({ error: '택배를 찾을 수 없습니다' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { success: true };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 수동 새로고침
|
|
|
|
|
fastify.post('/:id/refresh', async (request, reply) => {
|
|
|
|
|
const { id } = request.params;
|
|
|
|
|
|
|
|
|
|
const [parcels] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM parcels WHERE id = ?',
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
if (parcels.length === 0) {
|
|
|
|
|
return reply.code(404).send({ error: '택배를 찾을 수 없습니다' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await refreshParcel(fastify, id);
|
|
|
|
|
|
|
|
|
|
const [updated] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM parcels WHERE id = ?',
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
const [events] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM tracking_events WHERE parcel_id = ? ORDER BY event_time ASC',
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { ...updated[0], events };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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', ' ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshParcel(fastify, parcelId) {
|
|
|
|
|
const [parcels] = await fastify.db.execute(
|
|
|
|
|
'SELECT * FROM parcels WHERE id = ?',
|
|
|
|
|
[parcelId]
|
|
|
|
|
);
|
|
|
|
|
if (parcels.length === 0) return;
|
|
|
|
|
|
|
|
|
|
const parcel = parcels[0];
|
|
|
|
|
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 = ?',
|
|
|
|
|
[parcelId]
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 상태 업데이트
|
|
|
|
|
const status = result.lastEvent?.status || 'UNKNOWN';
|
|
|
|
|
const lastDetail = result.lastEvent?.description || '';
|
|
|
|
|
const deliveredAt = status === 'DELIVERED' ? toMysqlDatetime(result.lastEvent?.time) : null;
|
|
|
|
|
|
|
|
|
|
await fastify.db.execute(
|
|
|
|
|
`UPDATE parcels SET status = ?, last_detail = ?, last_checked_at = NOW(),
|
2026-03-24 19:36:40 +09:00
|
|
|
delivered_at = COALESCE(?, delivered_at),
|
|
|
|
|
sender_name = COALESCE(?, sender_name),
|
|
|
|
|
recipient_name = COALESCE(?, recipient_name)
|
|
|
|
|
WHERE id = ?`,
|
|
|
|
|
[status, lastDetail, deliveredAt, result.senderName, result.recipientName, parcelId]
|
2026-03-24 18:57:42 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 기존 이벤트 삭제 후 새로 삽입
|
|
|
|
|
await fastify.db.execute('DELETE FROM tracking_events WHERE parcel_id = ?', [parcelId]);
|
|
|
|
|
|
|
|
|
|
for (const event of result.events) {
|
|
|
|
|
await fastify.db.execute(
|
|
|
|
|
`INSERT INTO tracking_events (parcel_id, status, status_name, description, location, event_time)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
[parcelId, event.status, event.statusName, event.description, event.location, toMysqlDatetime(event.time)]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|