import { fetchGoodsName } from '../services/goods-name.js'; export default async function parcelRoutes(fastify) { // 전체 택배 목록 (페이징) fastify.get('/', async (request) => { 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 = ''; const params = []; if (status === 'active') { where = 'WHERE status != ?'; params.push('DELIVERED'); } else if (status === 'delivered') { where = 'WHERE status = ?'; params.push('DELIVERED'); } 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), }, }; }); // 운송장 등록 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: '이미 등록된 운송장입니다' }); } // 물품명 자동 조회 (CJ대한통운, 한진택배) let goodsName = null; try { goodsName = await fetchGoodsName(carrierId, trackingNumber); } catch (err) { fastify.log.warn(`물품명 조회 실패: ${err.message}`); } // 등록 const [result] = await fastify.db.execute( 'INSERT INTO parcels (carrier_id, carrier_name, tracking_number, label, goods_name) VALUES (?, ?, ?, ?, ?)', [carrierId, carrier.name, trackingNumber, label || goodsName || null, goodsName] ); 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(), 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] ); // 기존 이벤트 삭제 후 새로 삽입 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)] ); } }