Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CalendarPage from "@/pages/CalendarPage";
import SchedulePage from "@/pages/SchedulePage";
import ScheduleEditPage from "@/pages/ScheduleEditPage";
import CreateTripPage from "@/pages/CreateTripPage";
import EditTripPage from "@/pages/EditTripPage";

export default function App() {
return (
Expand All @@ -20,8 +21,9 @@ export default function App() {
<Route path="/folders" element={<FolderListPage />} />
<Route path="/folders/:id" element={<FolderDetailPage />} />
<Route path="/trips/new" element={<CreateTripPage />} />
<Route path="/trips/:tripId" element={<SchedulePage />} />
<Route path="/trips/:tripId/edit" element={<ScheduleEditPage />} />
<Route path="/trips/:tripId/edit" element={<EditTripPage />} />
<Route path="/trips/:tripId/schedule" element={<SchedulePage />} />
<Route path="/trips/:tripId/schedule/edit" element={<ScheduleEditPage />} />
</Route>
</Routes>
</BrowserRouter>
Expand Down
2 changes: 1 addition & 1 deletion src/features/member/api/member.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { MemberInvite, Member } from "../types/member.type";
import { MOCK_MEMBERS } from "../mock/members.mock";

const USE_MOCK = import.meta.env.VITE_USE_MOCK === "true";
const API_BASE = import.meta.env.VITE_API_BASE_URL || "";
// const API_BASE = import.meta.env.VITE_API_BASE_URL || "";

export const inviteMembers = async (
tripId: number,
Expand Down
7 changes: 4 additions & 3 deletions src/features/schedule/components/ScheduleHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useNavigate } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, MoreVertical } from "lucide-react";
import { useState, useRef, useEffect } from "react";

export default function ScheduleHeader({ tripId }: { tripId: number }) {
const navigate = useNavigate();
const { tripId } = useParams<{ tripId: string }>();
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -43,10 +44,10 @@ export default function ScheduleHeader({ tripId }: { tripId: number }) {
{menuOpen && (
<div className="absolute right-0 mt-2 w-36 bg-white rounded-lg shadow-md text-gray-700 text-sm z-50">
<button
onClick={() => navigate(`/trips/${tripId}/edit`)}
onClick={() => tripId && navigate(`/trips/${tripId}/schedule/edit`)}
className="block w-full text-left px-4 py-2 hover:bg-gray-50"
>
여행 수정
일정 수정
</button>

<button
Expand Down
107 changes: 96 additions & 11 deletions src/features/trip/api/trip.api.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,116 @@
import type { Trip } from "../types/trip.type";
import type { TripCreatePayload } from "../types/form";
import type { Trip } from "@/features/trip/types/trip.type";
import type { TripResponseDto } from "@/features/trip/types/trip.dto";
import type {
TripCreatePayload,
TripUpdatePayload,
} from "@/features/trip/types/trip.payload";

import { MOCK_TRIPS } from "@/features/trip/mock/trips.mock";

/* ======================
* 조회
* ====================== */

export const getTrips = async (): Promise<Trip[]> => {
// const { data } = await axios.get("/api/trips");
// return data;
// TODO: 실제 API 연동
return [];
};

export const createTrip = async (payload: TripCreatePayload): Promise<Trip> => {
// 실제 연동 예시 (파일 포함 시):
export const getTripDetail = async (
tripId: number
): Promise<TripResponseDto> => {
const trip = MOCK_TRIPS.find((t) => t.id === tripId);
if (!trip) throw new Error(`Trip with id ${tripId} not found`);

return {
id: trip.id,
title: trip.title,
region: trip.destinations[0] ?? "",
status: trip.status,
startDate: trip.startDate?.toISOString() ?? null,
endDate: trip.endDate?.toISOString() ?? null,
days:
trip.startDate && trip.endDate
? Math.round(
(trip.endDate.getTime() - trip.startDate.getTime()) /
(1000 * 60 * 60 * 24)
) + 1
: undefined,
thumbnail: trip.thumbnail ?? null,
destinations: trip.destinations,
participants: trip.participants,
folderId: trip.folderId,
};
};

/* ======================
* 생성
* ====================== */

export const createTrip = async (
payload: TripCreatePayload
): Promise<Trip> => {
// 실제 연동 시:
// const fd = new FormData();
// Object.entries(payload).forEach(([k, v]) => {
// if (v !== undefined && v !== null) fd.append(k, v as any);
// });
// const { data } = await axios.post("/api/trips", fd);
// return data;

// mock 응답: 폼 payload → Trip 형태로 매핑
// mock
return {
id: Math.floor(Math.random() * 100000),
id: Math.floor(Math.random() * 100_000),
folderId: payload.folderId ?? 0,
title: payload.title,
status: payload.status ?? "예정",
startDate: payload.startDate ? new Date(payload.startDate) : new Date(),
endDate: payload.endDate ? new Date(payload.endDate) : new Date(),
startDate: payload.startDate
? new Date(payload.startDate)
: null,
endDate: payload.endDate
? new Date(payload.endDate)
: null,
days: payload.days,
destinations: payload.destinations ?? [payload.region],
participants: payload.participants ?? 1,
thumbnail: undefined, // coverImage 업로드 후 서버가 생성해 줄 값
thumbnail: undefined,
};
};

/* ======================
* 수정
* ====================== */

export const updateTrip = async (
tripId: number,
payload: TripUpdatePayload
): Promise<TripResponseDto> => {
const fd = new FormData();
Object.entries(payload).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
fd.append(key, value as any);
}
});

// 실제 연동 시:
// const { data } = await axios.put(`/api/trips/${tripId}`, fd, {
// headers: { "Content-Type": "multipart/form-data" },
// });
// return data;

// mock
return {
id: tripId,
title: payload.title ?? "제목 없음",
region: payload.region ?? "",
status: payload.status ?? "예정",
startDate: payload.startDate ?? null,
endDate: payload.endDate ?? null,
days: payload.days,
destinations: payload.destinations ?? [],
participants: payload.participants ?? 1,
thumbnail: payload.coverImage
? payload.coverImage.name
: null,
};
};
22 changes: 17 additions & 5 deletions src/features/trip/components/TripCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
MoreVertical,
UsersRound,
Expand All @@ -19,6 +20,7 @@ interface TripCardProps {
}

export default function TripCard({ trip, onClick }: TripCardProps) {
const navigate = useNavigate();
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);

Expand Down Expand Up @@ -80,8 +82,16 @@ export default function TripCard({ trip, onClick }: TripCardProps) {
ref={menuRef}
role="menu"
>
<MenuItem icon={<Eye />} label="상세 보기" />
<MenuItem icon={<Pencil />} label="여행 수정" />
<MenuItem
icon={<Eye />}
label="상세 보기"
onClick={() => navigate(`/trips/${trip.id}/schedule`)}
/>
<MenuItem
icon={<Pencil />}
label="여행 수정"
onClick={() => navigate(`/trips/${trip.id}/edit`)}
/>
<MenuItem icon={<Trash2 />} label="여행 삭제" />
<hr className="my-1 border-[var(--color-border)]" />
<MenuItem icon={<Share2 />} label="공유하기" />
Expand All @@ -99,8 +109,8 @@ export default function TripCard({ trip, onClick }: TripCardProps) {
<div className="flex items-center text-[var(--color-text-sub)] text-sm">
<Calendar className="w-4 h-4 mr-2 text-[var(--color-primary)]" />
<span>
{trip.startDate.toLocaleDateString("ko-KR")} ~{" "}
{trip.endDate.toLocaleDateString("ko-KR")}
{trip.startDate?.toLocaleDateString("ko-KR") ?? "날짜 미정"} ~{" "}
{trip.endDate?.toLocaleDateString("ko-KR") ?? "날짜 미정"}
</span>
</div>

Expand All @@ -121,13 +131,15 @@ export default function TripCard({ trip, onClick }: TripCardProps) {
function MenuItem({
icon,
label,
onClick,
}: {
icon: React.ReactNode;
label: string;
onClick?: () => void;
}) {
return (
<button
onClick={() => alert(`${label} 클릭`)}
onClick={onClick || (() => alert(`${label} 클릭`))}
className="w-full flex items-center px-3 py-2 text-sm hover:bg-[var(--color-surface)]
text-[var(--color-text-main)] transition-colors"
role="menuitem"
Expand Down
8 changes: 7 additions & 1 deletion src/features/trip/components/TripCoverUploader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState } from "react";
import { useRef, useState, useEffect } from "react";
import { ImagePlus } from "lucide-react";
import defaultCover from "@/assets/sample/trip_thumbnail.jpg";

Expand All @@ -14,6 +14,12 @@ export default function TripCoverUploader({
const [preview, setPreview] = useState(defaultImage);
const fileInputRef = useRef<HTMLInputElement | null>(null);

useEffect(() => {
if (defaultImage) {
setPreview(defaultImage);
}
}, [defaultImage]);

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
Expand Down
Loading