Add dark mode with system preference auto-detection

All user-facing components now support dark mode via prefers-color-scheme.
- Dark backgrounds: gray-950/900/800
- Dark text: gray-100/200/300/400
- Orange brand colors adapt with darker tints
- Glass effects work in both modes
- Skeletons, cards, filters, bottom sheet all themed
- Google Maps InfoWindow stays light (maps don't support dark)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-09 16:14:29 +09:00
parent 99660bf07b
commit 6223691b33
9 changed files with 102 additions and 111 deletions

View File

@@ -89,7 +89,7 @@ export default function BottomSheet({ open, onClose, children }: BottomSheetProp
{/* Sheet */}
<div
ref={sheetRef}
className="fixed bottom-0 left-0 right-0 z-50 md:hidden flex flex-col bg-white/85 backdrop-blur-xl rounded-t-2xl shadow-2xl"
className="fixed bottom-0 left-0 right-0 z-50 md:hidden flex flex-col bg-white/85 dark:bg-gray-900/90 backdrop-blur-xl rounded-t-2xl shadow-2xl"
style={{
height: `${height * 100}vh`,
transition: dragging ? "none" : "height 0.3s cubic-bezier(0.2, 0, 0, 1)",
@@ -100,7 +100,7 @@ export default function BottomSheet({ open, onClose, children }: BottomSheetProp
>
{/* Handle bar */}
<div className="flex justify-center pt-2 pb-1 shrink-0 cursor-grab">
<div className="w-10 h-1 bg-gray-300 rounded-full" />
<div className="w-10 h-1 bg-gray-300 dark:bg-gray-600 rounded-full" />
</div>
{/* Content */}

View File

@@ -50,13 +50,13 @@ export default function RestaurantDetail({
<div className="p-4 space-y-4">
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<h2 className="text-lg font-bold">{restaurant.name}</h2>
<h2 className="text-lg font-bold dark:text-gray-100">{restaurant.name}</h2>
{getToken() && (
<button
onClick={handleToggleFavorite}
disabled={favLoading}
className={`text-xl leading-none transition-colors ${
favorited ? "text-rose-500" : "text-gray-300 hover:text-rose-400"
favorited ? "text-rose-500" : "text-gray-300 dark:text-gray-600 hover:text-rose-400"
}`}
title={favorited ? "찜 해제" : "찜하기"}
>
@@ -64,19 +64,19 @@ export default function RestaurantDetail({
</button>
)}
{restaurant.business_status === "CLOSED_PERMANENTLY" && (
<span className="px-2 py-0.5 bg-red-100 text-red-700 rounded text-xs font-semibold">
<span className="px-2 py-0.5 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded text-xs font-semibold">
</span>
)}
{restaurant.business_status === "CLOSED_TEMPORARILY" && (
<span className="px-2 py-0.5 bg-yellow-100 text-yellow-700 rounded text-xs font-semibold">
<span className="px-2 py-0.5 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 rounded text-xs font-semibold">
</span>
)}
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-xl leading-none"
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 text-xl leading-none"
>
x
</button>
@@ -84,39 +84,39 @@ export default function RestaurantDetail({
{restaurant.rating && (
<div className="flex items-center gap-2 text-sm">
<span className="text-yellow-500">{"★".repeat(Math.round(restaurant.rating))}</span>
<span className="font-medium">{restaurant.rating}</span>
<span className="text-yellow-500 dark:text-yellow-400">{"★".repeat(Math.round(restaurant.rating))}</span>
<span className="font-medium dark:text-gray-200">{restaurant.rating}</span>
{restaurant.rating_count && (
<span className="text-gray-400 text-xs">({restaurant.rating_count.toLocaleString()})</span>
<span className="text-gray-400 dark:text-gray-500 text-xs">({restaurant.rating_count.toLocaleString()})</span>
)}
</div>
)}
<div className="space-y-1 text-sm">
<div className="space-y-1 text-sm dark:text-gray-300">
{restaurant.cuisine_type && (
<p>
<span className="text-gray-500">:</span> {restaurant.cuisine_type}
<span className="text-gray-500 dark:text-gray-400">:</span> {restaurant.cuisine_type}
</p>
)}
{restaurant.address && (
<p>
<span className="text-gray-500">:</span> {restaurant.address}
<span className="text-gray-500 dark:text-gray-400">:</span> {restaurant.address}
</p>
)}
{restaurant.region && (
<p>
<span className="text-gray-500">:</span> {restaurant.region}
<span className="text-gray-500 dark:text-gray-400">:</span> {restaurant.region}
</p>
)}
{restaurant.price_range && (
<p>
<span className="text-gray-500">:</span> {restaurant.price_range}
<span className="text-gray-500 dark:text-gray-400">:</span> {restaurant.price_range}
</p>
)}
{restaurant.phone && (
<p>
<span className="text-gray-500">:</span>{" "}
<a href={`tel:${restaurant.phone}`} className="text-orange-600 hover:underline">
<span className="text-gray-500 dark:text-gray-400">:</span>{" "}
<a href={`tel:${restaurant.phone}`} className="text-orange-600 dark:text-orange-400 hover:underline">
{restaurant.phone}
</a>
</p>
@@ -127,7 +127,7 @@ export default function RestaurantDetail({
href={`https://www.google.com/maps/place/?q=place_id:${restaurant.google_place_id}`}
target="_blank"
rel="noopener noreferrer"
className="text-orange-600 hover:underline text-xs"
className="text-orange-600 dark:text-orange-400 hover:underline text-xs"
>
Google Maps에서
</a>
@@ -136,37 +136,37 @@ export default function RestaurantDetail({
</div>
<div>
<h3 className="font-semibold text-sm mb-2"> </h3>
<h3 className="font-semibold text-sm mb-2 dark:text-gray-200"> </h3>
{loading ? (
<div className="space-y-3 animate-pulse">
{[1, 2].map((i) => (
<div key={i} className="border rounded-lg p-3 space-y-2">
<div key={i} className="border dark:border-gray-700 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2">
<div className="h-4 w-16 bg-gray-200 rounded-sm" />
<div className="h-3 w-20 bg-gray-200 rounded" />
<div className="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded-sm" />
<div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded" />
</div>
<div className="h-4 w-full bg-gray-200 rounded" />
<div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded" />
<div className="flex gap-1">
<div className="h-5 w-14 bg-gray-200 rounded" />
<div className="h-5 w-16 bg-gray-200 rounded" />
<div className="h-5 w-14 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-5 w-16 bg-gray-200 dark:bg-gray-700 rounded" />
</div>
</div>
))}
</div>
) : videos.length === 0 ? (
<p className="text-sm text-gray-500"> </p>
<p className="text-sm text-gray-500 dark:text-gray-400"> </p>
) : (
<div className="space-y-3">
{videos.map((v) => (
<div key={v.video_id} className="border rounded-lg p-3">
<div key={v.video_id} className="border dark:border-gray-700 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
{v.channel_name && (
<span className="inline-block px-1.5 py-0.5 bg-orange-50 text-orange-600 rounded text-[10px] font-medium">
<span className="inline-block px-1.5 py-0.5 bg-orange-50 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 rounded text-[10px] font-medium">
{v.channel_name}
</span>
)}
{v.published_at && (
<span className="text-[10px] text-gray-400">
<span className="text-[10px] text-gray-400 dark:text-gray-500">
{v.published_at.slice(0, 10)}
</span>
)}
@@ -175,7 +175,7 @@ export default function RestaurantDetail({
href={v.url}
target="_blank"
rel="noopener noreferrer"
className="block text-sm font-medium text-orange-600 hover:underline"
className="block text-sm font-medium text-orange-600 dark:text-orange-400 hover:underline"
>
{v.title}
</a>
@@ -184,7 +184,7 @@ export default function RestaurantDetail({
{v.foods_mentioned.map((f, i) => (
<span
key={i}
className="px-2 py-0.5 bg-orange-50 text-orange-700 rounded text-xs"
className="px-2 py-0.5 bg-orange-50 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 rounded text-xs"
>
{f}
</span>
@@ -192,12 +192,12 @@ export default function RestaurantDetail({
</div>
)}
{v.evaluation?.text && (
<p className="mt-1 text-xs text-gray-600">
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
{v.evaluation.text}
</p>
)}
{v.guests.length > 0 && (
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
: {v.guests.join(", ")}
</p>
)}
@@ -208,11 +208,11 @@ export default function RestaurantDetail({
</div>
{videos.length > 0 && (
<div className="bg-gray-50 rounded-lg px-4 py-3 text-center space-y-1">
<p className="text-xs text-gray-500">
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3 text-center space-y-1">
<p className="text-xs text-gray-500 dark:text-gray-400">
.
</p>
<p className="text-xs text-gray-400">
<p className="text-xs text-gray-400 dark:text-gray-500">
!
</p>
</div>

View File

@@ -23,7 +23,7 @@ export default function RestaurantList({
if (!restaurants.length) {
return (
<div className="p-4 text-center text-gray-500 text-sm">
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
</div>
);
@@ -37,38 +37,38 @@ export default function RestaurantList({
onClick={() => onSelect(r)}
className={`w-full text-left p-3 rounded-xl shadow-sm border transition-all hover:shadow-md hover:-translate-y-0.5 ${
selectedId === r.id
? "bg-orange-50 border-orange-300 shadow-orange-100"
: "bg-white border-gray-100 hover:bg-gray-50"
? "bg-orange-50 dark:bg-orange-900/20 border-orange-300 dark:border-orange-700 shadow-orange-100 dark:shadow-orange-900/10"
: "bg-white dark:bg-gray-900 border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800"
}`}
>
<div className="flex items-start justify-between gap-2">
<h4 className="font-semibold text-sm">
<h4 className="font-semibold text-sm dark:text-gray-100">
<span className="mr-1">{getCuisineIcon(r.cuisine_type)}</span>
{r.name}
</h4>
{r.rating && (
<span className="text-xs text-yellow-600 font-medium whitespace-nowrap shrink-0">
<span className="text-xs text-yellow-600 dark:text-yellow-400 font-medium whitespace-nowrap shrink-0">
{r.rating}
</span>
)}
</div>
<div className="flex flex-wrap gap-x-2 gap-y-0.5 mt-1.5 text-xs text-gray-500">
<div className="flex flex-wrap gap-x-2 gap-y-0.5 mt-1.5 text-xs">
{r.cuisine_type && (
<span className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-600">{r.cuisine_type}</span>
<span className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-gray-600 dark:text-gray-400">{r.cuisine_type}</span>
)}
{r.price_range && (
<span className="px-1.5 py-0.5 bg-gray-50 rounded text-gray-600">{r.price_range}</span>
<span className="px-1.5 py-0.5 bg-gray-50 dark:bg-gray-800 rounded text-gray-600 dark:text-gray-400">{r.price_range}</span>
)}
</div>
{r.region && (
<p className="mt-1 text-xs text-gray-400 truncate">{r.region}</p>
<p className="mt-1 text-xs text-gray-400 dark:text-gray-500 truncate">{r.region}</p>
)}
{r.foods_mentioned && r.foods_mentioned.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5">
{r.foods_mentioned.slice(0, 5).map((f, i) => (
<span
key={i}
className="px-1.5 py-0.5 bg-orange-50 text-orange-700 rounded text-[10px]"
className="px-1.5 py-0.5 bg-orange-50 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 rounded text-[10px]"
>
{f}
</span>
@@ -83,7 +83,7 @@ export default function RestaurantList({
{r.channels.map((ch) => (
<span
key={ch}
className="px-1.5 py-0.5 bg-orange-50 text-orange-600 rounded-full text-[10px] font-medium"
className="px-1.5 py-0.5 bg-orange-50 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 rounded-full text-[10px] font-medium"
>
{ch}
</span>

View File

@@ -124,7 +124,7 @@ function ReviewForm({
<button
type="submit"
disabled={submitting}
className="px-3 py-1 bg-orange-500 text-white text-sm rounded hover:bg-orange-600 disabled:opacity-50"
className="px-3 py-1 bg-orange-500 dark:bg-orange-600 text-white text-sm rounded hover:bg-orange-600 dark:hover:bg-orange-500 disabled:opacity-50"
>
{submitting ? "저장 중..." : submitLabel}
</button>
@@ -202,13 +202,13 @@ export default function ReviewSection({ restaurantId }: ReviewSectionProps) {
{loading ? (
<div className="space-y-3 animate-pulse">
<div className="flex items-center gap-2">
<div className="h-4 w-24 bg-gray-200 rounded" />
<div className="h-4 w-8 bg-gray-200 rounded" />
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-4 w-8 bg-gray-200 dark:bg-gray-700 rounded" />
</div>
{[1, 2].map((i) => (
<div key={i} className="space-y-1">
<div className="h-3 w-20 bg-gray-200 rounded" />
<div className="h-3 w-full bg-gray-200 rounded" />
<div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-3 w-full bg-gray-200 dark:bg-gray-700 rounded" />
</div>
))}
</div>
@@ -225,7 +225,7 @@ export default function ReviewSection({ restaurantId }: ReviewSectionProps) {
{user && !myReview && !showForm && (
<button
onClick={() => setShowForm(true)}
className="mb-3 px-3 py-1 bg-orange-500 text-white text-sm rounded hover:bg-orange-600"
className="mb-3 px-3 py-1 bg-orange-500 dark:bg-orange-600 text-white text-sm rounded hover:bg-orange-600 dark:hover:bg-orange-500"
>
</button>

View File

@@ -25,12 +25,12 @@ export default function SearchBar({ onSearch, isLoading }: SearchBarProps) {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="식당, 지역, 음식..."
className="flex-1 min-w-0 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 text-sm"
className="flex-1 min-w-0 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 text-sm bg-white dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-500"
/>
<select
value={mode}
onChange={(e) => setMode(e.target.value as typeof mode)}
className="shrink-0 px-2 py-2 border border-gray-300 rounded-lg text-sm bg-white"
className="shrink-0 px-2 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-sm bg-white dark:bg-gray-800 dark:text-gray-300"
>
<option value="hybrid"></option>
<option value="keyword"></option>

View File

@@ -2,13 +2,13 @@
/** Pulsing skeleton block */
function Block({ className = "" }: { className?: string }) {
return <div className={`animate-pulse bg-gray-200 rounded ${className}`} />;
return <div className={`animate-pulse bg-gray-200 dark:bg-gray-700 rounded ${className}`} />;
}
/** Skeleton for a single restaurant card */
export function RestaurantCardSkeleton() {
return (
<div className="p-3 rounded-xl border border-gray-100 shadow-sm space-y-2">
<div className="p-3 rounded-xl border border-gray-100 dark:border-gray-800 shadow-sm space-y-2">
<div className="flex items-start justify-between">
<Block className="h-4 w-3/5" />
<Block className="h-3 w-10" />
@@ -64,7 +64,7 @@ export function RestaurantDetailSkeleton() {
<div className="space-y-3">
<Block className="h-4 w-20" />
{[1, 2].map((i) => (
<div key={i} className="border rounded-lg p-3 space-y-2">
<div key={i} className="border dark:border-gray-700 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2">
<Block className="h-4 w-16 rounded-sm" />
<Block className="h-3 w-20" />