diff --git a/README.md b/README.md index ec09dd6..1a646bd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@
- 날씨로그

날씨로그

예보보다 가까운, 이웃이 직접 전하는 지금 날씨 @@ -14,11 +13,7 @@ 날씨로그는 같은 동네의 이웃들이 직접 느낀 날씨를 사진과 함께 공유하는 모바일 웹 서비스입니다. 숫자로 표시된 예보만으로 알기 어려운 체감온도, 비, 햇빛 상태를 최근 제보로 빠르게 확인할 수 있습니다. -## 서비스 미리보기 - -

- 맑은 하늘 제보 -

+## 핵심 기능 ### 동네 날씨 피드 diff --git a/app/globals.css b/app/globals.css index 4c1eb7e..fb6ce04 100644 --- a/app/globals.css +++ b/app/globals.css @@ -16,6 +16,7 @@ html { background: #deeff8; } body { margin: 0; min-height: 100vh; + min-height: 100dvh; color: var(--ink); background: linear-gradient(145deg, #deeff8 0%, #f4fbff 50%, #ddecf5 100%); font-family: "Pretendard Variable", Pretendard, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif; @@ -27,12 +28,13 @@ button { cursor: pointer; } position: relative; width: min(100%, 480px); min-height: 100vh; + min-height: 100dvh; margin: 0 auto; overflow-x: clip; background: #eef9ff; box-shadow: 0 0 48px rgba(48, 98, 126, .12); } -.page { min-height: 100vh; padding: 0 20px 112px; } +.page { min-height: 100vh; min-height: 100dvh; padding: 0 20px 112px; } .main-tab-page { padding-bottom: 80px; } .safe-top { padding-top: max(8px, env(safe-area-inset-top)); } .home-header-top { padding-top: max(9px, env(safe-area-inset-top)); } diff --git a/components/auth-callback-screen.tsx b/components/auth-callback-screen.tsx index 82ff26b..881b0bb 100644 --- a/components/auth-callback-screen.tsx +++ b/components/auth-callback-screen.tsx @@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { SocialIcon } from "@/components/social-icon"; import { authApi, type OAuthCallbackResult } from "@/lib/api/auth-api"; +import { markAccountPanelReturn } from "@/lib/account-panel-return"; import { resolveApiUrl } from "@/lib/api/config"; import { logger } from "@/lib/logging"; import type { SocialProvider } from "@/lib/types"; @@ -55,6 +56,7 @@ export function AuthCallbackScreen() { const code = searchParams.get("code"); if (result === "LINK_SUCCESS") { authLogger.info("authentication_completed", { result }); + markAccountPanelReturn(); showToast("소셜 계정을 연동했어요.", "SUCCESS"); router.replace("/mypage"); return; @@ -128,7 +130,7 @@ export function AuthCallbackScreen() { return (
-
+

계정 연동

diff --git a/components/home-screen.tsx b/components/home-screen.tsx index fc0cd73..3e71241 100644 --- a/components/home-screen.tsx +++ b/components/home-screen.tsx @@ -17,7 +17,7 @@ import { getLocationName } from "@/lib/constants"; import { useToastStore } from "@/store/toast-store"; export function HomeScreen() { - const { location, setLocation, isDetecting, detectionError, needsManualInput, setNeedsManualInput, detectLocation } = useCurrentLocation({ refreshOnResume: true }); + const { location, setLocation, isDetecting, detectionError, needsManualInput, setNeedsManualInput, detectLocation } = useCurrentLocation(); const showToast = useToastStore((state) => state.showToast); const loadMoreRef = useRef(null); const locationLabel = location ? getLocationName(location, "short") : ""; @@ -55,7 +55,7 @@ export function HomeScreen() { const hasWeatherData = hasSummaryData || hasReportData; const hasWeatherError = summary.isError || reports.isError; const showLocationError = !location && !isDetecting && Boolean(detectionError); - const isInitialWeatherLoading = !locationLabel || (!hasWeatherData && (summary.isPending || reports.isPending)); + const isInitialWeatherLoading = !isDetecting && Boolean(locationLabel) && !hasWeatherData && (summary.isPending || reports.isPending); const showFullWeatherError = Boolean(locationLabel) && !hasWeatherData && hasWeatherError && !summary.isPending && !reports.isPending; return (
diff --git a/components/home-weather-controls.tsx b/components/home-weather-controls.tsx index f808132..78e2998 100644 --- a/components/home-weather-controls.tsx +++ b/components/home-weather-controls.tsx @@ -2,6 +2,7 @@ import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { ChevronDown, MapPin, RotateCw } from "lucide-react"; import { useSyncExternalStore } from "react"; +import { LocationDetectingIndicator } from "@/components/location-detecting-indicator"; let openedAt = 0; const openedAtListeners = new Set<() => void>(); @@ -42,11 +43,11 @@ export function HomeWeatherControls({ : "현재 시각"; return ( -
+
diff --git a/components/location-detecting-indicator.tsx b/components/location-detecting-indicator.tsx new file mode 100644 index 0000000..0f516f8 --- /dev/null +++ b/components/location-detecting-indicator.tsx @@ -0,0 +1,30 @@ +import { LoaderCircle } from "lucide-react"; + +export function LocationDetectingIndicator({ + className = "", + compact = false, + iconPosition = "left", +}: { + className?: string; + compact?: boolean; + iconPosition?: "left" | "right"; +}) { + const spinner = ( + + ); + + return ( + + {iconPosition === "left" && spinner} + 위치를 찾는 중 + {iconPosition === "right" && spinner} + + ); +} diff --git a/components/location-picker.tsx b/components/location-picker.tsx index 22f8887..3045b23 100644 --- a/components/location-picker.tsx +++ b/components/location-picker.tsx @@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, Check, Flame, LockKeyhole, LocateFixed, MapPin, RefreshCw, Search, Star, X } from "lucide-react"; import { FormEvent, useEffect, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; +import { LocationDetectingIndicator } from "@/components/location-detecting-indicator"; import { getLocationName } from "@/lib/constants"; import { locationApi } from "@/lib/api/location-api"; import { truncateText } from "@/lib/text"; @@ -106,14 +107,14 @@ function LocationPickerDialog({ current, isDetecting, detectionError, required,
{!required && }
-
- -

현재 설정된 위치

{isDetecting ?
:

{selectedLocation ? getLocationName(selectedLocation, "full") : current ? getLocationName(current, "full") : "아직 설정되지 않았어요"}

}
- +
+ +
{isDetecting ? :

{selectedLocation ? getLocationName(selectedLocation, "full") : current ? getLocationName(current, "full") : "아직 설정되지 않았어요"}

}
+
{detectionError &&

{detectionError}

}
-
+
setActiveTab("SEARCH")} icon={} label="검색" /> setActiveTab("FAVORITES")} icon={} label="즐겨찾기" /> setActiveTab("POPULAR")} icon={} label="인기" /> @@ -130,8 +131,8 @@ function LocationPickerDialog({ current, isDetecting, detectionError, required, {!serverSearch.isError && searchResults.map((location) => )} {serverSearch.isFetching &&
{Array.from({ length: 3 }).map((_, index) =>
)}
} {serverSearch.isError && serverSearch.refetch()} />} - {value.trim() && !serverSearch.isFetching && !serverSearch.isError && searchResults.length === 0 &&

지원하는 지역을 찾지 못했어요.

} - {!value.trim() &&

찾고 싶은 시·구 또는 동 이름을 입력해 주세요.

} + {value.trim() && !serverSearch.isFetching && !serverSearch.isError && searchResults.length === 0 &&

검색 결과가 없습니다.

} + {!value.trim() &&

검색 결과가 없습니다.

}
} {activeTab === "FAVORITES" && (isMember diff --git a/components/main-tab-navigation.tsx b/components/main-tab-navigation.tsx index a766359..30f6b6e 100644 --- a/components/main-tab-navigation.tsx +++ b/components/main-tab-navigation.tsx @@ -43,7 +43,9 @@ export function MainTabNavigation() { const profileActive = pathname === "/mypage"; const profileContent = (