🎯 프로젝트 리브랜딩: 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 🌟 새로운 기능: - 사용자 인증 및 권한 관리 - 다중 여행 계획 관리 - 여행 템플릿 시스템 - 공유 링크 및 댓글 시스템 - 관리자 대시보드
This commit is contained in:
183
server/routes/travelPlans.js
Normal file
183
server/routes/travelPlans.js
Normal file
@@ -0,0 +1,183 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user