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
1 change: 1 addition & 0 deletions components/auth-callback-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function AuthCallbackScreen() {
const code = searchParams.get("code");
if (result === "LINK_SUCCESS") {
authLogger.info("authentication_completed", { result });
queryClient.removeQueries({ queryKey: ["members", "me"], exact: true });
markAccountPanelReturn();
showToast("소셜 계정을 연동했어요.", "SUCCESS");
router.replace("/mypage");
Expand Down
20 changes: 15 additions & 5 deletions components/home-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { CloudOff, RefreshCw } from "lucide-react";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { AppHeader } from "@/components/app-header";
import { EmptyState } from "@/components/empty-state";
import { ErrorState } from "@/components/error-state";
Expand All @@ -17,9 +17,10 @@ import { getLocationName } from "@/lib/constants";
import { useToastStore } from "@/store/toast-store";

export function HomeScreen() {
const { location, setLocation, isDetecting, detectionError, needsManualInput, setNeedsManualInput, detectLocation } = useCurrentLocation();
const { location, setLocation, isDetecting, detectionError, needsManualInput, setNeedsManualInput, detectLocation } = useCurrentLocation({ refreshOnHomeResume: true });
const showToast = useToastStore((state) => state.showToast);
const loadMoreRef = useRef<HTMLDivElement>(null);
const [isManualRefreshAnimating, setIsManualRefreshAnimating] = useState(false);
const locationLabel = location ? getLocationName(location, "short") : "";
const locationKey = location?.id ?? locationLabel;
const summary = useQuery({ queryKey: ["weather-summary", locationKey], queryFn: () => weatherApi.getSummary(location!), enabled: !!location, refetchInterval: 10_000, refetchIntervalInBackground: false });
Expand Down Expand Up @@ -48,7 +49,16 @@ export function HomeScreen() {
const isEmptyFeed = Boolean(reports.data && items.length === 0);
const showFeedEndMessage = Boolean(reports.data && !reports.hasNextPage && items.length > 0);
const refreshWeather = async () => {
await Promise.all([summary.refetch(), reports.refetch()]);
if (isManualRefreshAnimating) return;
setIsManualRefreshAnimating(true);
const minimumAnimation = new Promise<void>((resolve) => {
window.setTimeout(resolve, 650);
});
try {
await Promise.all([summary.refetch(), reports.refetch(), minimumAnimation]);
} finally {
setIsManualRefreshAnimating(false);
}
};
const hasSummaryData = Boolean(summary.data);
const hasReportData = Boolean(reports.data);
Expand All @@ -64,12 +74,12 @@ export function HomeScreen() {
location={locationLabel}
isDetecting={isDetecting && !needsManualInput}
updatedAt={summary.dataUpdatedAt}
isRefreshing={summary.isFetching || reports.isRefetching}
isRefreshing={summary.isFetching || reports.isRefetching || isManualRefreshAnimating}
canRefresh={Boolean(location)}
onLocationClick={() => setNeedsManualInput(true)}
onRefresh={() => void refreshWeather()}
/>
{hasWeatherData && hasWeatherError && <ConnectionNotice onRetry={refreshWeather} isRetrying={summary.isFetching || reports.isFetching} />}
{hasWeatherData && hasWeatherError && <ConnectionNotice onRetry={refreshWeather} isRetrying={summary.isFetching || reports.isFetching || isManualRefreshAnimating} />}
{showLocationError ? <ErrorState message="현재 동네를 불러오지 못했어요." transparent /> : showFullWeatherError ? <ErrorState onRetry={refreshWeather} transparent /> : <>
{summary.data
? <WeatherSummary summary={summary.data} />
Expand Down
2 changes: 1 addition & 1 deletion components/home-weather-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function HomeWeatherControls({
className="flex h-10 shrink-0 items-center gap-1.5 rounded-2xl px-2 text-xs font-bold text-[#718594] transition-colors hover:text-[#268fc7] disabled:cursor-wait disabled:opacity-50"
aria-label={`${updatedAtLabel}, 날씨 통계와 피드 새로고침`}
>
<RotateCw size={14} strokeWidth={2.3} className={isRefreshing ? "animate-spin [animation-duration:2.4s]" : ""} />
<RotateCw size={14} strokeWidth={2.3} className={isRefreshing ? "animate-spin [animation-duration:1.1s]" : ""} />
<span>{updatedAtLabel}</span>
</button>
</section>
Expand Down
15 changes: 10 additions & 5 deletions components/legal-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,43 @@
import { useEffect } from "react";
import { ArrowLeft, X } from "lucide-react";
import { PrivacyPolicyContent, TermsContent } from "@/components/legal-content";
import { useModalNavigation } from "@/hooks/use-modal-navigation";
import { useLegalModalStore } from "@/store/legal-modal-store";

export function LegalModal() {
const document = useLegalModalStore((state) => state.document);
const origin = useLegalModalStore((state) => state.origin);
const close = useLegalModalStore((state) => state.closeLegalDocument);
const closeModal = useModalNavigation({
open: Boolean(document),
onBack: close,
});

useEffect(() => {
if (!document) return;
const previousOverflow = window.document.body.style.overflow;
window.document.body.style.overflow = "hidden";
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") close();
if (event.key === "Escape") closeModal();
};
window.addEventListener("keydown", closeOnEscape);
return () => {
window.document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", closeOnEscape);
};
}, [close, document]);
}, [closeModal, document]);

if (!document) return null;
const isPrivacy = document === "PRIVACY";
const title = isPrivacy ? "개인정보처리방침" : "서비스 이용약관";

return (
<div className="fixed inset-0 z-[90] flex items-end justify-center bg-[#173144]/35 p-4 backdrop-blur-[2px] sm:items-center" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget) close(); }}>
<div className="fixed inset-0 z-[90] flex items-end justify-center bg-[#173144]/35 p-4 backdrop-blur-[2px] sm:items-center" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget) closeModal(); }}>
<section className="flex max-h-[90dvh] w-full max-w-[440px] flex-col overflow-hidden rounded-[24px] bg-[#eef9ff] shadow-2xl" role="dialog" aria-modal="true" aria-labelledby="legal-modal-title">
<header className="grid shrink-0 grid-cols-[42px_1fr_42px] items-center border-b-2 border-[#dcecf4] px-5 py-4">
{origin === "SETTINGS" ? <button type="button" autoFocus onClick={close} className="header-back-button" aria-label="설정으로 돌아가기"><ArrowLeft size={18} /></button> : <span />}
{origin === "SETTINGS" ? <button type="button" autoFocus onClick={() => closeModal()} className="header-back-button" aria-label="설정으로 돌아가기"><ArrowLeft size={18} /></button> : <span />}
<h2 id="legal-modal-title" className="text-center text-lg font-extrabold">{title}</h2>
<button type="button" autoFocus={origin !== "SETTINGS"} onClick={close} className="icon-button" aria-label={`${title} 닫기`}><X size={20} /></button>
<button type="button" autoFocus={origin !== "SETTINGS"} onClick={() => closeModal()} className="icon-button" aria-label={`${title} 닫기`}><X size={20} /></button>
</header>
<div className="overflow-y-auto overscroll-contain px-5 pb-6 pt-4">
<p className="text-xs font-bold text-[#718594]">시행일 2026년 7월 21일</p>
Expand Down
Loading