Skip to content
Merged
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
12 changes: 11 additions & 1 deletion src/app/(app)/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default function HomePage() {
const { data: session, status } = useSession();
const [userName, setUserName] = useState<string | null>(null);
const [profileImageUrl, setProfileImageUrl] = useState<string | null>(null);
const [loadedProfileUserId, setLoadedProfileUserId] = useState<number | null>(null);
const [communityPosts, setCommunityPosts] = useState<FeedPost[]>([]);
const [popularPlaces, setPopularPlaces] = useState<PlaceSummary[]>([]);
const [popularLoading, setPopularLoading] = useState(true);
Expand Down Expand Up @@ -116,6 +117,9 @@ export default function HomePage() {
})
.catch(() => {
if (!cancelled) setUserName(session?.user?.name ?? null);
})
.finally(() => {
if (!cancelled) setLoadedProfileUserId(userId);
});

return () => {
Expand All @@ -124,6 +128,11 @@ export default function HomePage() {
}, [session]);

const displayName = greetingName(userName ?? session?.user?.name);
const displayProfileImage = profileImageUrl ?? session?.user?.image ?? null;
const sessionUserId = session?.user?.id != null ? Number(session.user.id) : null;
const profileLoading =
status === "loading" ||
(sessionUserId !== null && loadedProfileUserId !== sessionUserId && !displayProfileImage);

async function openFallbackPlace(place: (typeof POPULAR_FALLBACK_PLACES)[number]) {
if (resolvingFallback) return;
Expand Down Expand Up @@ -157,8 +166,9 @@ export default function HomePage() {
return (
<PageFade className="flex min-h-full shrink-0 flex-col bg-[#f8fbff] text-[#0b2146]">
<HomeHeader
profileImageUrl={profileImageUrl}
profileImageUrl={displayProfileImage}
profileLabel={avatarLabel(displayName)}
profileLoading={profileLoading}
/>

<section className="relative h-[clamp(6rem,24vw,7.5rem)] shrink-0 overflow-hidden">
Expand Down
34 changes: 2 additions & 32 deletions src/app/(app)/trips/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import Image from "next/image";
import Link from "next/link";
import localFont from "next/font/local";
import { useRouter } from "next/navigation";
import { ArrowRight, ChevronRight, Settings, Trash2 } from "lucide-react";
import { ArrowRight, ChevronRight, Settings } from "lucide-react";
import { useTripDraft } from "@/store/trip-draft";
import { deleteSchedule, getSchedules } from "@/lib/api/schedules";
import { getSchedules } from "@/lib/api/schedules";
import { ApiError } from "@/lib/api/axios";
import type { ScheduleSummary } from "@/types/api/schedule";

Expand Down Expand Up @@ -162,7 +162,6 @@ export default function TripsPage() {
const [showAllSchedules, setShowAllSchedules] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deletingScheduleId, setDeletingScheduleId] = useState<string | null>(null);
const [now, setNow] = useState(() => new Date());

useEffect(() => {
Expand Down Expand Up @@ -207,22 +206,6 @@ export default function TripsPage() {
const visibleSchedules = showAllSchedules ? sorted : sorted.slice(0, 3);
const canToggleSchedules = sorted.length > 3;

async function handleDeleteSchedule(schedule: ScheduleSummary) {
const confirmed = window.confirm(`"${getScheduleTitle(schedule)}" 일정을 삭제할까요?`);
if (!confirmed) return;

setDeletingScheduleId(schedule.id);
setError(null);
try {
await deleteSchedule(schedule.id);
setSchedules((items) => items.filter((item) => item.id !== schedule.id));
} catch (err) {
setError(err instanceof ApiError ? err.payload.message : "일정을 삭제하지 못했습니다.");
} finally {
setDeletingScheduleId(null);
}
}

return (
<div className={`${pageFont.className} flex w-full min-w-0 flex-1 flex-col overflow-x-clip bg-[#F8FBFF] text-[#14233F]`}>
<header className="relative aspect-[2/1] min-h-[166px] shrink-0 overflow-hidden bg-white">
Expand Down Expand Up @@ -394,19 +377,6 @@ export default function TripsPage() {
</span>
<ChevronRight className="size-[clamp(19px,4.46vw,23px)] shrink-0 text-[#64758E]" strokeWidth={1.8} aria-hidden />
</Link>
<button
type="button"
disabled={deletingScheduleId === t.id}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
void handleDeleteSchedule(t);
}}
className="absolute top-3 right-3 z-10 flex size-8 items-center justify-center rounded-full bg-white/90 text-[#94A3B8] shadow-sm transition-colors hover:bg-[#FFE8E5] hover:text-[#D74432] disabled:cursor-wait disabled:opacity-50"
aria-label={`${getScheduleTitle(t)} 일정 삭제`}
>
<Trash2 className="size-4" strokeWidth={2} aria-hidden />
</button>
</li>
))}
</ul>
Expand Down
6 changes: 5 additions & 1 deletion src/components/layout/HomeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ const NOTIFICATION_COUNT_POLLING_MS = 60_000;
export default function HomeHeader({
profileImageUrl,
profileLabel = "누비",
profileLoading = false,
}: {
profileImageUrl?: string | null;
profileLabel?: string;
profileLoading?: boolean;
}) {
const { status } = useSession();
const profileImage = profileImageUrl;
Expand Down Expand Up @@ -79,7 +81,9 @@ export default function HomeHeader({
aria-label="내 정보"
className="relative grid size-9 shrink-0 place-items-center overflow-hidden rounded-full bg-linear-to-br from-[#79A9F2] to-[#574BCB] text-xs font-bold text-white shadow-[0_5px_12px_rgba(61,85,169,0.16)] ring-1 ring-white/75"
>
{profileImage ? (
{profileLoading ? (
<span className="size-full animate-pulse bg-[#dce7f7]" aria-hidden="true" />
) : profileImage ? (
<Image
src={profileImage}
alt="프로필"
Expand Down
31 changes: 29 additions & 2 deletions src/components/trip/ScheduleDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Pencil } from "lucide-react";
import { Pencil, Trash2 } from "lucide-react";
import { toast } from "sonner";
import ScheduleCourseView, {
type ScheduleCourseMarker,
type ScheduleCoursePlace,
} from "@/components/trip/ScheduleCourseView";
import PlaceDetailSheet from "@/components/sheet/PlaceDetailSheet";
import ShareLinkSheet from "@/components/sheet/ShareLinkSheet";
import { getSchedule, getScheduleMap } from "@/lib/api/schedules";
import { deleteSchedule, getSchedule, getScheduleMap } from "@/lib/api/schedules";
import { createShareLink } from "@/lib/api/shares";
import { formatCourseTime, formatKoreanReturnTime } from "@/lib/schedule-course";
import { placeCategoryLabel } from "@/utils/place-category";
Expand Down Expand Up @@ -44,6 +44,7 @@ export default function ScheduleDetail({ scheduleId }: { scheduleId: string }) {
const [detailPlaceId, setDetailPlaceId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [sharing, setSharing] = useState(false);
const [deleting, setDeleting] = useState(false);
const [shareUrl, setShareUrl] = useState<string | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -155,6 +156,23 @@ export default function ScheduleDetail({ scheduleId }: { scheduleId: string }) {
}
}

async function handleDelete() {
if (deleting || !window.confirm("이 일정을 삭제할까요?\n삭제한 일정은 복구할 수 없습니다.")) {
return;
}

setDeleting(true);
try {
await deleteSchedule(scheduleId);
toast.success("일정을 삭제했습니다.");
router.replace("/trips");
router.refresh();
} catch (cause) {
toast.error(cause instanceof Error ? cause.message : "일정을 삭제하지 못했습니다.");
setDeleting(false);
}
}

if (error) {
return (
<div className="flex flex-1 flex-col items-center justify-center px-6 text-center">
Expand Down Expand Up @@ -198,6 +216,15 @@ export default function ScheduleDetail({ scheduleId }: { scheduleId: string }) {
>
<Pencil size={18} />
</button>
<button
type="button"
aria-label="일정 삭제"
disabled={deleting}
onClick={() => void handleDelete()}
className="grid size-8 shrink-0 place-items-center rounded-full text-zinc-600 hover:bg-red-50 hover:text-red-500 disabled:cursor-wait disabled:opacity-50"
>
<Trash2 size={18} />
</button>
<button
type="button"
aria-label="공유"
Expand Down
Loading