require('dotenv').config(); const express = require('express'); const cors = require('cors'); const { createStore } = require('./storage'); const PORT = Number(process.env.PORT || 4000); const CATEGORY_SET = new Set(['KOREAN', 'CHINESE', 'JAPANESE', 'WESTERN', 'OTHER']); async function main() { const { mode, store } = await createStore(); const app = express(); app.use(cors()); app.use(express.json()); app.get('/v1/health', (_req, res) => { res.json({ status: 'ok', mode, time: new Date().toISOString() }); }); app.get('/v1/restaurants/nearby', async (req, res) => { try { const userExternalId = getUserId(req); const lat = Number(req.query.lat); const lng = Number(req.query.lng); const radiusMeters = Number(req.query.radiusMeters || 1000); const category = typeof req.query.category === 'string' ? req.query.category : undefined; const sort = normalizeSort(req.query.sort); if (!Number.isFinite(lat) || !Number.isFinite(lng)) { return res.status(400).json({ code: 'INVALID_INPUT', message: 'lat/lng required' }); } if (category && !CATEGORY_SET.has(category)) { return res.status(400).json({ code: 'INVALID_INPUT', message: 'invalid category' }); } const items = await store.getNearby({ userExternalId, lat, lng, radiusMeters, category, sort, }); return res.json({ items, nextCursor: null }); } catch (error) { return handleError(res, error); } }); app.get('/v1/restaurants/:restaurantId', async (req, res) => { try { const userExternalId = getUserId(req); const restaurant = await store.getRestaurantById( req.params.restaurantId, userExternalId, ); if (!restaurant) { return res.status(404).json({ code: 'NOT_FOUND', message: 'restaurant not found' }); } return res.json(restaurant); } catch (error) { return handleError(res, error); } }); app.get('/v1/restaurants/:restaurantId/reviews', async (req, res) => { try { const userExternalId = getUserId(req); const items = await store.getReviews(req.params.restaurantId, userExternalId); return res.json({ items, nextCursor: null }); } catch (error) { return handleError(res, error); } }); app.post('/v1/restaurants/:restaurantId/reviews', async (req, res) => { try { const userExternalId = getUserId(req); const restaurantId = req.params.restaurantId; const rating = Number(req.body?.rating); const content = String(req.body?.content || '').trim(); if (!Number.isInteger(rating) || rating < 1 || rating > 5 || !content) { return res.status(400).json({ code: 'INVALID_INPUT', message: 'rating must be 1~5 and content required', }); } const result = await store.upsertReview(restaurantId, userExternalId, rating, content); if (!result) { return res.status(404).json({ code: 'NOT_FOUND', message: 'restaurant not found' }); } return res.json(result); } catch (error) { return handleError(res, error); } }); app.delete('/v1/reviews/:reviewId', async (req, res) => { try { const userExternalId = getUserId(req); const result = await store.deleteReview(req.params.reviewId, userExternalId); if (!result.deleted && result.reason === 'NOT_FOUND') { return res.status(404).json({ code: 'NOT_FOUND', message: 'review not found' }); } if (!result.deleted && result.reason === 'FORBIDDEN') { return res.status(403).json({ code: 'FORBIDDEN', message: 'not your review' }); } return res.status(204).send(); } catch (error) { return handleError(res, error); } }); app.post('/v1/restaurants/:restaurantId/like', async (req, res) => { try { const userExternalId = getUserId(req); const result = await store.toggleLike(req.params.restaurantId, userExternalId); if (!result) { return res.status(404).json({ code: 'NOT_FOUND', message: 'restaurant not found' }); } return res.json(result); } catch (error) { return handleError(res, error); } }); app.get('/v1/users/me/reviews', async (req, res) => { try { const userExternalId = getUserId(req); const items = await store.getMyReviews(userExternalId); return res.json({ items, nextCursor: null }); } catch (error) { return handleError(res, error); } }); app.listen(PORT, () => { console.log(`[lunch-picker-backend] mode=${mode} listening on http://localhost:${PORT}`); }); } main().catch((error) => { console.error('[lunch-picker-backend] failed to start', error); process.exit(1); }); function getUserId(req) { const value = req.header('x-user-id'); return value && value.trim() ? value.trim() : 'demo-user-1'; } function normalizeSort(input) { if (typeof input !== 'string') return 'distance'; if (input === 'rating' || input === 'likes') return input; return 'distance'; } function handleError(res, error) { console.error('[api-error]', error); return res.status(500).json({ code: 'INTERNAL_ERROR', message: 'unexpected error' }); }