diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 9af75dc..7ce9581 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -11,5 +11,7 @@ export default { database: process.env.DB_NAME || 'traeon', connectionLimit: 10, waitForConnections: true, + timezone: '+09:00', + dateStrings: true, }, }; diff --git a/backend/src/plugins/db.js b/backend/src/plugins/db.js index 236f661..427f81f 100644 --- a/backend/src/plugins/db.js +++ b/backend/src/plugins/db.js @@ -17,6 +17,8 @@ CREATE TABLE IF NOT EXISTS parcels ( carrier_name VARCHAR(100) NOT NULL, tracking_number VARCHAR(100) NOT NULL, label VARCHAR(200), + sender_name VARCHAR(100), + recipient_name VARCHAR(100), status VARCHAR(50) DEFAULT 'PENDING', last_detail TEXT, last_checked_at DATETIME, @@ -69,6 +71,14 @@ async function dbPlugin(fastify) { } fastify.log.info('테이블 초기화 완료'); + // 마이그레이션: sender_name, recipient_name 컬럼 추가 + try { + await pool.execute('ALTER TABLE parcels ADD COLUMN sender_name VARCHAR(100) AFTER label'); + } catch (e) { /* 이미 존재 */ } + try { + await pool.execute('ALTER TABLE parcels ADD COLUMN recipient_name VARCHAR(100) AFTER sender_name'); + } catch (e) { /* 이미 존재 */ } + // 기본 택배사 데이터 삽입 for (const [id, name, shortName, color, logoUrl] of DEFAULT_CARRIERS) { await pool.execute( diff --git a/backend/src/plugins/tracker.js b/backend/src/plugins/tracker.js index bcc4782..92dfe6d 100644 --- a/backend/src/plugins/tracker.js +++ b/backend/src/plugins/tracker.js @@ -15,6 +15,8 @@ const TRACK_QUERY = ` query Track($carrierId: ID!, $trackingNumber: String!) { track(carrierId: $carrierId, trackingNumber: $trackingNumber) { trackingNumber + sender { name } + recipient { name } lastEvent { status { code name } time @@ -89,6 +91,8 @@ async function trackerPlugin(fastify) { return { trackingNumber: trackInfo.trackingNumber, + senderName: trackInfo.sender?.name || null, + recipientName: trackInfo.recipient?.name || null, lastEvent, events, }; diff --git a/backend/src/routes/parcels.js b/backend/src/routes/parcels.js index 8c9133c..35f0abe 100644 --- a/backend/src/routes/parcels.js +++ b/backend/src/routes/parcels.js @@ -1,20 +1,40 @@ export default async function parcelRoutes(fastify) { - // 전체 택배 목록 + // 전체 택배 목록 (페이징) fastify.get('/', async (request) => { - const { status } = request.query; - let sql = 'SELECT * FROM parcels ORDER BY created_at DESC'; + 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') { - sql = 'SELECT * FROM parcels WHERE status != ? ORDER BY created_at DESC'; + where = 'WHERE status != ?'; params.push('DELIVERED'); } else if (status === 'delivered') { - sql = 'SELECT * FROM parcels WHERE status = ? ORDER BY created_at DESC'; + where = 'WHERE status = ?'; params.push('DELIVERED'); } - const [rows] = await fastify.db.execute(sql, params); - return rows; + 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), + }, + }; }); // 운송장 등록 @@ -180,8 +200,11 @@ async function refreshParcel(fastify, parcelId) { await fastify.db.execute( `UPDATE parcels SET status = ?, last_detail = ?, last_checked_at = NOW(), - delivered_at = COALESCE(?, delivered_at) WHERE id = ?`, - [status, lastDetail, deliveredAt, parcelId] + 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] ); // 기존 이벤트 삭제 후 새로 삽입 diff --git a/docs/plan.md b/docs/plan.md index cd74c9a..960bc2c 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -166,9 +166,11 @@ CREATE TABLE tracking_events ( - [x] delivery-tracker 셀프호스팅 (Docker 이미지 빌드, Apollo Server 포트 4000) - [x] delivery-tracker GraphQL 클라이언트 플러그인 (tracker.js) - [x] API 라우트 구현 (parcels CRUD + carriers 목록 + 수동 새로고침) -- [ ] Frontend를 더미 데이터에서 실제 API로 전환 +- [x] Frontend를 더미 데이터에서 실제 API로 전환 - Vite 프록시 설정 (`/api/` → backend) - - React Query로 데이터 페칭 + 자동 리페칭 + - React Query로 데이터 페칭 (택배 목록, 상세, 택배사 목록) + - 등록/수정/삭제/새로고침 mutation + 캐시 무효화 + - 택배사 로고를 RustFS URL에서 로드 - [ ] 자동갱신 cron 서비스 (node-cron) - 배송 중인 택배: 30분 간격 자동 조회 - 배송 완료된 택배: 조회 중단 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 642efff..9c2f90c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "traeon-frontend", "version": "1.0.0", "dependencies": { + "@tanstack/react-query": "^5.90.16", + "@tanstack/react-virtual": "^3.13.18", "dayjs": "^1.11.19", "framer-motion": "^11.0.8", "lucide-react": "^0.344.0", @@ -1164,6 +1166,59 @@ "win32" ] }, + "node_modules/@tanstack/query-core": { + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz", + "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz", + "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.95.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 168cfa9..8f1d76f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^5.90.16", + "@tanstack/react-virtual": "^3.13.18", "dayjs": "^1.11.19", "framer-motion": "^11.0.8", "lucide-react": "^0.344.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 25ea8c4..19bc57e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,11 +1,9 @@ -import { Routes, Route } from "react-router-dom"; import MainPage from "@/pages/MainPage"; -import DetailPage from "@/pages/DetailPage"; function App() { return ( -
- {parcel.label || parcel.trackingNumber} + {parcel.label || parcel.tracking_number}
+ 배송 정보가 아직 없습니다 +
+ )} ++ 이 택배를 삭제할까요? +
+등록된 택배가 없습니다
-
{event.description}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index d719969..c506b7f 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -1,13 +1,26 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import "./index.css";
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60 * 2,
+ retry: 1,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
+
ReactDOM.createRoot(document.getElementById("root")).render(
택배를 찾을 수 없습니다
- -- 배송 정보가 아직 없습니다 -
- )} -