Files
travel/server/routes/travelPlans.js
Hyungi Ahn fd5a68e44a 🎯 프로젝트 리브랜딩: Kumamoto → Travel Planner v2.0
 주요 변경사항:
- 프로젝트 이름: kumamoto-travel-planner → travel-planner
- 버전 업그레이드: v1.0.0 → v2.0.0
- 멀티유저 시스템 구현 (JWT 인증)
- PostgreSQL 마이그레이션 시스템 추가
- Docker 컨테이너 이름 변경
- UI 브랜딩 업데이트 (Travel Planner)
- API 서버 및 인증 시스템 추가
- 여행 공유 기능 구현
- 템플릿 시스템 추가

🔧 기술 스택:
- Frontend: React + TypeScript + Vite
- Backend: Node.js + Express + JWT
- Database: PostgreSQL + 마이그레이션
- Infrastructure: Docker + Docker Compose

🌟 새로운 기능:
- 사용자 인증 및 권한 관리
- 다중 여행 계획 관리
- 여행 템플릿 시스템
- 공유 링크 및 댓글 시스템
- 관리자 대시보드
2025-11-25 10:39:58 +09:00

184 lines
5.8 KiB
JavaScript

const express = require('express');
const { query } = require('../db');
const router = express.Router();
// 여행 계획 전체 조회 (최신 1개)
router.get('/', async (req, res) => {
try {
// 최신 여행 계획 조회
const planResult = await query(
'SELECT * FROM travel_plans ORDER BY created_at DESC LIMIT 1'
);
if (planResult.rows.length === 0) {
return res.json(null);
}
const plan = planResult.rows[0];
// 해당 계획의 모든 일정 조회
const schedules = await query(
`SELECT ds.id, ds.schedule_date,
json_agg(
json_build_object(
'id', a.id,
'time', a.time,
'title', a.title,
'description', a.description,
'location', a.location,
'type', a.type,
'coordinates', CASE
WHEN a.lat IS NOT NULL AND a.lng IS NOT NULL
THEN json_build_object('lat', a.lat, 'lng', a.lng)
ELSE NULL
END,
'images', a.images,
'links', a.links,
'relatedPlaces', (
SELECT json_agg(
json_build_object(
'id', rp.id,
'name', rp.name,
'description', rp.description,
'address', rp.address,
'coordinates', CASE
WHEN rp.lat IS NOT NULL AND rp.lng IS NOT NULL
THEN json_build_object('lat', rp.lat, 'lng', rp.lng)
ELSE NULL
END,
'memo', rp.memo,
'willVisit', rp.will_visit,
'category', rp.category,
'images', rp.images,
'links', rp.links
)
)
FROM related_places rp
WHERE rp.activity_id = a.id
)
) ORDER BY a.time
) FILTER (WHERE a.id IS NOT NULL) as activities
FROM day_schedules ds
LEFT JOIN activities a ON a.day_schedule_id = ds.id
WHERE ds.travel_plan_id = $1
GROUP BY ds.id, ds.schedule_date
ORDER BY ds.schedule_date`,
[plan.id]
);
const travelPlan = {
id: plan.id,
startDate: plan.start_date,
endDate: plan.end_date,
schedule: schedules.rows.map(row => ({
date: row.schedule_date,
activities: row.activities || []
})),
budget: {
total: 0,
accommodation: 0,
food: 0,
transportation: 0,
shopping: 0,
activities: 0
},
checklist: []
};
res.json(travelPlan);
} catch (error) {
console.error('Error fetching travel plan:', error);
res.status(500).json({ error: error.message });
}
});
// 여행 계획 저장/업데이트
router.post('/', async (req, res) => {
const client = await query('BEGIN');
try {
const { startDate, endDate, schedule } = req.body;
// 기존 계획 삭제 (단순화: 항상 최신 1개만 유지)
await query('DELETE FROM travel_plans');
// 새 여행 계획 생성
const planResult = await query(
'INSERT INTO travel_plans (start_date, end_date) VALUES ($1, $2) RETURNING id',
[startDate, endDate]
);
const planId = planResult.rows[0].id;
// 일정별 데이터 삽입
for (const day of schedule) {
// day_schedule 삽입
const scheduleResult = await query(
'INSERT INTO day_schedules (travel_plan_id, schedule_date) VALUES ($1, $2) RETURNING id',
[planId, day.date]
);
const scheduleId = scheduleResult.rows[0].id;
// activities 삽입
if (day.activities && day.activities.length > 0) {
for (const activity of day.activities) {
const activityResult = await query(
`INSERT INTO activities (
day_schedule_id, time, title, description, location, type, lat, lng, images, links
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
[
scheduleId,
activity.time,
activity.title,
activity.description || null,
activity.location || null,
activity.type,
activity.coordinates?.lat || null,
activity.coordinates?.lng || null,
activity.images || null,
activity.links || null
]
);
const activityId = activityResult.rows[0].id;
// related_places 삽입
if (activity.relatedPlaces && activity.relatedPlaces.length > 0) {
for (const place of activity.relatedPlaces) {
await query(
`INSERT INTO related_places (
activity_id, name, description, address, lat, lng, memo, will_visit, category, images, links
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
[
activityId,
place.name,
place.description || null,
place.address || null,
place.coordinates?.lat || null,
place.coordinates?.lng || null,
place.memo || null,
place.willVisit || false,
place.category || 'other',
place.images || null,
place.links || null
]
);
}
}
}
}
}
await query('COMMIT');
res.json({ success: true, id: planId });
} catch (error) {
await query('ROLLBACK');
console.error('Error saving travel plan:', error);
res.status(500).json({ error: error.message });
}
});
module.exports = router;