Skip to content
6 changes: 6 additions & 0 deletions src/app/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { ProtectedRoute } from "@/app/router/ProtectedRoute";
import AuthCallbackPage from "@/pages/AuthCallbackPage";
import DevClickPlacePage from "@/pages/dev/DevClickPlacePage";
import DevSelectOptionPage from "@/pages/dev/DevSelectOptionPage";
import EditPlacePage from "@/pages/EditPlacePage";
import EntryPage from "@/pages/EntryPage";
import LoginPage from "@/pages/LoginPage";
import { mapHomeLoader } from "@/pages/map/map-home-loader";
import NicknamePage from "@/pages/onboarding/NicknamePage";
import TermsAgreementPage from "@/pages/onboarding/TermsAgreementPage";
import ReelsPlaceSelectPage from "@/pages/ReelsPlaceSelectPage";
import RegisterSelectRoomPage from "@/pages/RegisterSelectRoomPage";
import SplashScreenPage from "@/pages/SplashScreenPage";
import { APP_ROUTES } from "@/shared/config/routes";

Expand All @@ -30,6 +33,9 @@ export const router = createBrowserRouter([
{ path: "dev/splash", element: <SplashScreenPage /> },
{ path: "dev/click_place", element: <DevClickPlacePage /> },
{ path: "dev/SelectOption", element: <DevSelectOptionPage /> },
{ path: "dev/register_place", element: <ReelsPlaceSelectPage /> },
{ path: "edit_place", element: <EditPlacePage /> },
{ path: "register-select-room", element: <RegisterSelectRoomPage /> },
{
path: "dev/list",
element: (
Expand Down
5 changes: 4 additions & 1 deletion src/components/common/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type SearchFieldProps = Omit<React.ComponentProps<"input">, "type"> & {
inputClassName?: string;
searchButtonLabel?: string;
onSubmitSearch?: () => void;
searchButtonDisabled?: boolean;
};

export const SearchField = React.forwardRef<HTMLInputElement, SearchFieldProps>(
Expand All @@ -18,6 +19,7 @@ export const SearchField = React.forwardRef<HTMLInputElement, SearchFieldProps>(
inputClassName,
searchButtonLabel = "검색",
onSubmitSearch,
searchButtonDisabled = false,
id,
placeholder = SEARCH_FIELD_DEFAULT_PLACEHOLDER,
"aria-label": ariaLabel,
Expand Down Expand Up @@ -51,8 +53,9 @@ export const SearchField = React.forwardRef<HTMLInputElement, SearchFieldProps>(
/>
<button
type="button"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute end-1.5 top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-full outline-none focus-visible:ring-2"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute end-1.5 top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-full outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-40"
aria-label={searchButtonLabel}
disabled={searchButtonDisabled}
onClick={onSubmitSearch}
>
<Search className="size-4" aria-hidden />
Expand Down
2 changes: 1 addition & 1 deletion src/components/map/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function CategoryChipSkeletonList() {
<ul className={CATEGORY_CHIP_GRID_CLASS} role="presentation" aria-hidden>
{Array.from({ length: CATEGORY_CHIP_SKELETON_COUNT }, (_, index) => (
<li key={`category-chip-skeleton-${index}`} className="min-w-0">
<div className="border-border/65 bg-background/78 h-7 w-full animate-pulse rounded-full border" />
<div className="border-border/65 bg-background/85 h-7 w-full animate-pulse rounded-full border" />
</li>
))}
</ul>
Expand Down
19 changes: 10 additions & 9 deletions src/components/map/MapSearchSuggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MapPin } from "lucide-react";
import type { MapSearchSuggestion } from "@/features/map/utils/map-search";
import { cn } from "@/lib/utils";

import { MAP_SEARCH_SUGGESTIONS_PANEL_CLASS } from "./chip-style";
import { MAP_FILTER_PANEL_BASE_CLASS } from "./chip-style";

type MapSearchSuggestionsProps = {
suggestions: MapSearchSuggestion[];
Expand All @@ -23,13 +23,7 @@ export function MapSearchSuggestions({
}

return (
<div
className={cn(
MAP_SEARCH_SUGGESTIONS_PANEL_CLASS,
"mt-2 overflow-hidden rounded-3xl shadow-sm",
className,
)}
>
<div className={cn(MAP_FILTER_PANEL_BASE_CLASS, className)}>
{suggestions.length > 0 ? (
<ul
role="list"
Expand All @@ -56,7 +50,14 @@ export function MapSearchSuggestions({
))}
</ul>
) : (
<div className="px-5 py-4 text-sm font-medium text-neutral-500">관련 장소가 없음</div>
<div className="px-5 py-4 text-center" role="status" aria-live="polite">
<p className="text-muted-foreground text-sm leading-snug font-semibold">
저장한 장소에서 찾지 못했어요
</p>
<p className="text-muted-foreground/85 mt-1 text-xs leading-relaxed font-normal">
장소 이름·주소·동네 이름을 바꿔 검색해 보세요
</p>
</div>
)}
</div>
);
Expand Down
21 changes: 9 additions & 12 deletions src/components/map/chip-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ export const MAP_CHIP_BASE_CLASS =

export const MAP_CHIP_SELECTED_CLASS = "border-primary bg-primary text-primary-foreground";

export const MAP_CHIP_UNSELECTED_CLASS =
"bg-background/92 text-muted-foreground/85 hover:bg-background border-border/80 backdrop-blur-sm";
/** 지도 검색 입력·카테고리 칩(비선택·패널 포커스) 공통 글래스 — 배경 불투명도 85% 통일 */
export const MAP_OVERLAY_GLASS_SURFACE_CLASS = "border-border/35 bg-background/85 backdrop-blur-md";

/** 태그 패널에서 이 카테고리를 보고 있을 때 — 회색 톤(active 코랄 실색과 구분) */
export const MAP_CHIP_PANEL_FOCUS_CLASS =
"border-border/90 bg-muted/85 text-foreground/88 backdrop-blur-sm hover:bg-muted";
export const MAP_CHIP_UNSELECTED_CLASS = `${MAP_OVERLAY_GLASS_SURFACE_CLASS} text-muted-foreground/85 hover:bg-background/90`;

export const MAP_CHIP_PANEL_FOCUS_CLASS = `${MAP_OVERLAY_GLASS_SURFACE_CLASS} text-foreground/85 ring-1 ring-inset ring-foreground/12 hover:bg-background/90 dark:ring-white/15`;

/** active 칩이면서 태그 패널 포커스일 때 — 코랄 위 대비용 얕은 링 */
export const MAP_CHIP_PANEL_FOCUS_ON_ACTIVE_CLASS = "ring-2 ring-inset ring-primary-foreground/45";
Expand All @@ -26,17 +26,14 @@ export const MAP_CHIP_BADGE_SELECTED_MOBILE_CLASS =
export const MAP_CHIP_BADGE_UNSELECTED_MOBILE_CLASS =
"max-md:absolute max-md:-top-1 max-md:-right-0.5 max-md:z-1 max-md:flex max-md:h-4 max-md:min-w-4 max-md:items-center max-md:justify-center max-md:rounded-full max-md:px-1 max-md:tabular-nums max-md:font-semibold max-md:shadow-sm max-md:border max-md:border-primary/30 max-md:bg-primary max-md:text-primary-foreground";

/** 검색창·검색 제안 패널 공통 글래스 (입력창은 아래 클래스에서 border-width는 기본 input과 함께 사용) */
export const MAP_SEARCH_GLASS_SURFACE_CLASS = "border-border/55 bg-background/85 backdrop-blur-md";
/** 검색 입력(`SearchField`) — `MAP_OVERLAY_GLASS_SURFACE_CLASS` 별칭 */
export const MAP_SEARCH_GLASS_SURFACE_CLASS = MAP_OVERLAY_GLASS_SURFACE_CLASS;

/** 검색창 한 줄만 — 넓은 글래스 패널 없이 얇게 */
export const MAP_SEARCH_INPUT_GLASS_CLASS = `${MAP_SEARCH_GLASS_SURFACE_CLASS} placeholder:text-muted-foreground/90`;

/** 검색 제안 드롭다운 패널 (div 래퍼용 — `border` 명시) */
export const MAP_SEARCH_SUGGESTIONS_PANEL_CLASS = `border ${MAP_SEARCH_GLASS_SURFACE_CLASS}`;

export const MAP_FILTER_PANEL_BASE_CLASS =
"mt-2 overflow-hidden rounded-2xl border border-border/35 bg-background/88 shadow-filter-panel backdrop-blur-md transition-all duration-200 ease-out";
/** 태그 필터 패널·검색 제안 드롭다운 공통 래퍼 */
export const MAP_FILTER_PANEL_BASE_CLASS = `mt-2 overflow-hidden rounded-2xl border ${MAP_OVERLAY_GLASS_SURFACE_CLASS} shadow-filter-panel transition-all duration-200 ease-out`;

export const MAP_FILTER_PANEL_HEADER_ACTION_CLASS =
"text-xs font-medium text-muted-foreground/90 hover:text-muted-foreground";
Expand Down
7 changes: 5 additions & 2 deletions src/components/mypage/SavedPlaceItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MoreVertical } from "lucide-react";
import { MapPin, MoreVertical } from "lucide-react";
import { useCallback, useRef } from "react";

import { usePointerDownOutside } from "@/hooks/use-pointer-down-outside";
Expand Down Expand Up @@ -47,7 +47,10 @@ export function SavedPlaceItem({
<div className="flex gap-3">
<button type="button" onClick={() => onSelect(place)} className="min-w-0 flex-1 text-left">
<h3 className="truncate text-sm font-semibold text-[#222222]">{place.name}</h3>
<p className="mt-1 truncate text-[0.68rem] font-medium text-[#777777]">{place.address}</p>
<p className="mt-1 flex min-w-0 items-center gap-1.5 font-medium">
<MapPin className="size-4 shrink-0 text-neutral-400" aria-hidden />
<span className="min-w-0 truncate text-[0.68rem] text-[#777777]">{place.address}</span>
</p>
</button>

{!readOnly ? (
Expand Down
34 changes: 34 additions & 0 deletions src/components/reels/EditPlaceResultCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RoundSelectionCheck } from "@/components/ui/RoundSelectionCheck";
import type { SavedPlace } from "@/shared/types/map-home";

type EditPlaceResultCardProps = {
place: SavedPlace;
selected: boolean;
onSelect: () => void;
};

export function EditPlaceResultCard({ place, selected, onSelect }: EditPlaceResultCardProps) {
return (
<li>
<button
type="button"
className="flex min-h-[84px] w-full items-center gap-3 border-b border-black/5 px-5 py-4 text-left transition-colors active:bg-gray-50"
onClick={onSelect}
aria-pressed={selected}
>
<span className="inline-flex h-9 w-9 shrink-0 items-center justify-center">
<img src="/assets/map-marker.svg" alt="" className="h-9 w-9 object-contain" />
</span>

<span className="min-w-0 flex-1">
<span className="text-foreground block truncate text-base font-semibold">
{place.name}
</span>
<span className="mt-1 block truncate text-[11px] text-black/70">{place.address}</span>
</span>

<RoundSelectionCheck selected={selected} />
</button>
</li>
);
}
81 changes: 81 additions & 0 deletions src/components/reels/PlaceSelectCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { MapPin, Pencil } from "lucide-react";

import { RoundSelectionCheck } from "@/components/ui/RoundSelectionCheck";
import { cn } from "@/lib/utils";
import type { SavedPlace } from "@/shared/types/map-home";

type PlaceSelectCardProps = {
place: SavedPlace;
selected: boolean;
disabled: boolean;
onSelect: () => void;
onEdit: () => void;
};

export function PlaceSelectCard({
place,
selected,
disabled,
onSelect,
onEdit,
}: PlaceSelectCardProps) {
const handleSelect = () => {
if (!disabled) {
onSelect();
}
};

return (
<li>
<article
className={cn(
"flex min-h-[70px] w-full items-center gap-3 border-b border-black/5 bg-white px-5 py-4 text-left transition-colors",
!disabled && "cursor-pointer active:bg-gray-50",
disabled && "cursor-default",
)}
onClick={handleSelect}
aria-disabled={disabled}
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<span className="text-foreground truncate text-base font-semibold">{place.name}</span>
<button
type="button"
className="text-foreground inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full transition-colors hover:bg-black/5 focus-visible:ring-2 focus-visible:ring-black/20 focus-visible:outline-none"
aria-label={`${place.name} 수정`}
onClick={(event) => {
event.stopPropagation();
onEdit();
}}
>
<Pencil className="h-4 w-4" strokeWidth={2.4} />
</button>
</div>
<p className="mt-2 flex min-w-0 items-center gap-1.5 text-[11px] leading-tight">
<MapPin className="size-4 shrink-0 text-neutral-400" aria-hidden />
<span className="min-w-0 truncate text-black/70">{place.address}</span>
</p>
</div>

{disabled ? (
<span className="shrink-0 text-xs font-medium whitespace-nowrap text-black/45">
이미 저장된 장소입니다
</span>
) : (
<button
type="button"
className="inline-flex shrink-0"
aria-label={`${place.name} 선택`}
aria-pressed={selected}
onClick={(event) => {
event.stopPropagation();
handleSelect();
}}
>
<RoundSelectionCheck selected={selected} />
</button>
)}
</article>
</li>
);
}
3 changes: 2 additions & 1 deletion src/components/room/FriendRoomItemView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const FriendRoomItemView = memo(function FriendRoomItemView({
onKeyDown,
}: FriendRoomItemViewProps) {
const pinned = Boolean(row.isPinned);
const placeCountText = row.placeCount >= 1000 ? "1k+개 장소" : `${row.placeCount}개 장소`;

return (
<div
Expand Down Expand Up @@ -78,7 +79,7 @@ export const FriendRoomItemView = memo(function FriendRoomItemView({

<div className="col-start-3 row-span-2 row-start-1 flex shrink-0 flex-col items-center justify-center self-center">
<span className="text-room-meta text-primary cursor-pointer text-center">
{row.placeCount}개 장소
{placeCountText}
</span>
</div>
</div>
Expand Down
85 changes: 85 additions & 0 deletions src/components/room/RoomConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { PillButton } from "@/components/ui/PillButton";
import { cn } from "@/lib/utils";

import { RoomModalShell } from "./RoomModalShell";

type RoomConfirmModalProps = {
open: boolean;
message: string;
cancelLabel?: string;
confirmLabel: string;
onCancel?: () => void;
onConfirm: () => void;
};

export function RoomConfirmModal({
open,
message,
cancelLabel,
confirmLabel,
onCancel,
onConfirm,
}: RoomConfirmModalProps) {
if (!open) {
return null;
}

const isSingleAction = !cancelLabel;

return (
<RoomModalShell
visible
onOverlayClick={() => {
if (!isSingleAction) {
onCancel?.();
}
}}
className="z-60"
>
{isSingleAction ? (
<div className="px-5 pt-4 pb-4">
<p className="text-foreground text-center text-base leading-snug font-bold whitespace-pre-line">
{message}
</p>
<PillButton
type="button"
variant="modal"
className="mt-4"
aria-label={confirmLabel}
onClick={onConfirm}
>
{confirmLabel}
</PillButton>
</div>
) : (
<>
<div className="px-6 pt-8 pb-5 text-center">
<p className="text-foreground text-base leading-snug font-bold whitespace-pre-line">
{message}
</p>
</div>

<div className="border-border/50 flex border-t">
<button
type="button"
className={cn(
"flex-1 py-4 text-sm font-medium transition-colors",
"border-border/50 text-muted-foreground hover:bg-muted/25 active:bg-muted/35 border-r",
)}
onClick={onCancel}
>
{cancelLabel}
</button>
<button
type="button"
className="text-foreground hover:bg-muted/25 active:bg-muted/35 flex-1 py-4 text-sm font-medium transition-colors"
onClick={onConfirm}
>
{confirmLabel}
</button>
</div>
</>
)}
</RoomModalShell>
);
}
8 changes: 2 additions & 6 deletions src/components/room/link-add/PlaceSelectionScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PillButton } from "@/components/ui/PillButton";
import { RoundSelectionCheck } from "@/components/ui/RoundSelectionCheck";
import type { MockPlaceCandidate } from "@/features/room/link-add";
import { cn } from "@/lib/utils";

Expand Down Expand Up @@ -52,12 +53,7 @@ export function PlaceSelectionScreen({
onClick={() => onSelectPlace(place.id)}
>
<span className="text-foreground">{place.name}</span>
<span
className={cn(
"border-border inline-flex h-5 w-5 rounded-full border",
checked && "border-primary bg-primary",
)}
/>
<RoundSelectionCheck selected={checked} />
</button>
</li>
);
Expand Down
Loading
Loading