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
52 changes: 26 additions & 26 deletions src/app/(main)/map/_components/useMobileRouteView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,34 +459,34 @@ export function useMobileRouteView({
<div className="pointer-events-none absolute inset-0 z-10 flex flex-col justify-end pb-3.5">
{/* 펼치면 X·일차 탭은 사라지고 카드가 그 자리까지 커진다 */}
<div className={cn("flex flex-col transition-opacity duration-200", isExpanded && "invisible h-0 opacity-0")}>
<div className="flex justify-end pr-[18px]">
<div className="mb-[9px] flex items-center gap-2 pr-[18px]">
<nav
ref={tabsRef}
aria-label="경로 일차 선택"
// relative: 탭 위치(offsetLeft)를 탭 줄 기준으로 재서 가로 스크롤 위치를 정한다
className="pointer-events-auto relative flex h-[34px] min-w-0 flex-1 gap-2.5 overflow-x-auto px-[15px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
{schedules.map((s, index) => {
const selected = index + 1 === tabDayNumber;
return (
<button
key={s.scheduleId}
ref={selected ? selectedTabRef : undefined}
type="button"
onClick={() => goToDay(index + 1)}
aria-pressed={selected}
className={cn(
"shrink-0 cursor-pointer rounded-[30px] border border-border bg-fill-subtle px-3.5 py-1.5 text-label-m-emphasis",
selected ? "text-primary" : "text-text-disabled",
)}
>
Day {index + 1}
</button>
);
})}
</nav>
<MapCloseButton label="경로 보기 닫기" onClick={close} className="pointer-events-auto" />
</div>
<nav
ref={tabsRef}
aria-label="경로 일차 선택"
// relative: 탭 위치(offsetLeft)를 탭 줄 기준으로 재서 가로 스크롤 위치를 정한다
className="pointer-events-auto relative mb-[9px] mt-3 flex h-[34px] gap-2.5 overflow-x-auto px-[15px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
{schedules.map((s, index) => {
const selected = index + 1 === tabDayNumber;
return (
<button
key={s.scheduleId}
ref={selected ? selectedTabRef : undefined}
type="button"
onClick={() => goToDay(index + 1)}
aria-pressed={selected}
className={cn(
"shrink-0 cursor-pointer rounded-[30px] border border-border bg-fill-subtle px-3.5 py-1.5 text-label-m-emphasis",
selected ? "text-primary" : "text-text-disabled",
)}
>
Day {index + 1}
</button>
);
})}
</nav>
</div>
{/* 접힘: 가장 긴 카드에 맞춰 모든 카드가 같은 높이 / 펼침: 지도의 70% */}
<div className={cn("transition-[height] duration-300 ease-out", isExpanded && "h-[70%]")}>
Expand Down
4 changes: 1 addition & 3 deletions src/app/(main)/plan/_components/itinerary/PlanPlaceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,7 @@ export function PlanPlaceCard({
className={cn(
"w-full select-none transition-colors",
PLAN_PLACE_CARD_TW.article,
// 고른 카드는 계속 켜져 있으므로 배경은 그대로 두고 테두리와 옅게 번지는 파란 그림자로만 표시한다
// (그림자 토큰이 없어 파랑 원시 토큰으로 지정)
focused && "border-primary shadow-[0_0_12px_var(--blue-200)]",
focused && "border-primary",
dragDisabled
? "cursor-default"
: "cursor-grab active:cursor-grabbing",
Expand Down
59 changes: 55 additions & 4 deletions src/app/(main)/plan/_components/mobile/MobilePlanPlaceCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useId, useState, type PointerEvent as ReactPointerEvent } from "react";
import { useEffect, useId, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";

import {
ChevronDownIcon,
Expand All @@ -10,6 +10,7 @@ import {
TimeClockIcon,
WriteIcon,
} from "@/assets/icons";
import { useOnClickOutside } from "@/hooks/useOnClickOutside";
import { formatScheduleTimeRange } from "@/lib/plan/scheduleTime";
import type { PlanPlace } from "@/lib/plan/types";
import { renderTextWithLinks } from "@/lib/text/renderTextWithLinks";
Expand Down Expand Up @@ -104,7 +105,7 @@ export function MobilePlanPlaceCard({
</span>
<div className="min-w-0 flex-1">
<h3 className="line-clamp-2 break-words text-title-s text-text">{place.title}</h3>
{meta ? <p className="truncate text-body-s-regular text-text-subtle">{meta}</p> : null}
{meta ? <PlaceMeta place={place} meta={meta} /> : null}
<PlaceTimeAndExpense place={place} expenseSummary={expenseSummary}
addWhenEmpty={addWhenEmpty} onEditTime={onEditTime} onOpenExpenses={onOpenExpenses} />
</div>
Expand Down Expand Up @@ -139,6 +140,57 @@ export function MobilePlanPlaceCard({
);
}

/** 분류·주소 줄 — 좁은 화면에서 잘렸을 때만 눌러서 전체 주소를 아래 드롭다운으로 보여 준다 */
function PlaceMeta({ place, meta }: Readonly<{ place: PlanPlace; meta: string }>) {
const wrapRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLParagraphElement>(null);
const addressId = useId();
const [truncated, setTruncated] = useState(false);
const [open, setOpen] = useState(false);
useOnClickOutside(wrapRef, () => setOpen(false));

// 카드 폭(회전·넘겨 보기 칸)이 바뀌면 잘렸는지 다시 잰다
useEffect(() => {
const el = textRef.current;
if (!el || typeof ResizeObserver === "undefined") return;
const measure = () => setTruncated(el.scrollWidth > el.clientWidth);
measure();
const observer = new ResizeObserver(measure);
observer.observe(el);
return () => observer.disconnect();
}, [meta]);

const expandable = truncated && Boolean(place.subtitle);
return (
<div ref={wrapRef} className="relative">
<p ref={textRef} className="truncate text-body-s-regular text-text-subtle">{meta}</p>
{expandable ? (
// 잘리지 않았으면 버튼을 두지 않아 이 줄을 눌러도 카드 본문(열기)이 눌린다
<button
type="button"
onClick={(e) => {
stop(e);
setOpen((v) => !v);
}}
aria-expanded={open}
aria-controls={addressId}
aria-label={`${place.title} 전체 주소 ${open ? "닫기" : "보기"}`}
className="pointer-events-auto absolute inset-0 cursor-pointer"
/>
) : null}
{expandable && open ? (
// pointer-events-auto: 드롭다운을 눌러도 아래 카드 본문(열기)이 눌리지 않게 막는다
<p
id={addressId}
className="pointer-events-auto absolute inset-x-0 top-full z-20 mt-1 break-words rounded-[12px] bg-fill-elevate px-3.5 py-3 text-body-s-regular text-text shadow-[0_2px_10px_rgba(0,0,0,0.1)]"
>
{place.subtitle}
</p>
) : null}
</div>
);
}

function PlaceTimeAndExpense({ place, expenseSummary, addWhenEmpty, onEditTime, onOpenExpenses }:
Pick<MobilePlanPlaceCardProps, "place" | "expenseSummary" | "addWhenEmpty" | "onEditTime" | "onOpenExpenses">) {
const summaryId = useId();
Expand Down Expand Up @@ -268,7 +320,7 @@ function PlaceMemo({ place, addWhenEmpty, onEditMemo }:
</div>
) : null}
{!memo && addMemo ? (
// 메모가 있을 때와 같은 줄(구분선·아이콘·꺾쇠)을 흐리게 — 누르면 메모 시트
// 메모가 있을 때와 같은 줄(구분선·아이콘)을 흐리게 — 펼칠 내용이 없어 꺾쇠는 두지 않는다. 누르면 메모 시트
<div className="pointer-events-none relative mx-3.5 border-t border-border-subtle py-3">
<button
type="button"
Expand All @@ -283,7 +335,6 @@ function PlaceMemo({ place, addWhenEmpty, onEditMemo }:
<span className="min-w-0 flex-1 truncate text-caption-l-regular leading-5 text-text-disabled">
메모를 추가해보세요.
</span>
<ChevronDownIcon size={20} className="shrink-0 text-icon-disabled" />
</button>
</div>
) : null}
Expand Down
11 changes: 0 additions & 11 deletions src/app/(main)/room-settings/page.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/layout/MainSettingsPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactNode } from "react";

import { SetSectionMaxWidth } from "@/contexts/SectionWidthContext";

/** member-settings·room-settings 등 s1 본문 폭 설정 탭 공통 레이아웃 */
/** member-settings 등 s1 본문 폭 설정 탭 공통 레이아웃 */
export function MainSettingsPageLayout({ children }: { children: ReactNode }) {
return (
<div className="flex flex-col gap-6 rounded-2xl pb-10">
Expand Down
75 changes: 45 additions & 30 deletions src/components/map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { usePathname } from "next/navigation";
import { Bookmark } from "lucide-react";
import {
AdvancedMarker,
ControlPosition,
Map as GoogleMap,
MapControl,
useMap,
useMapsLibrary,
type MapCameraChangedEvent,
Expand Down Expand Up @@ -465,6 +467,27 @@ export default function TripMap({
clearPlanFocus();
};

const bookmarkToggle = (
<button
type="button"
onClick={() => setShowBookmarkPins((v) => !v)}
aria-pressed={showBookmarkPins}
aria-label={
showBookmarkPins
? "북마크 장소 표시 끄기"
: "북마크 장소 표시 켜기"
}
title="북마크 장소 표시"
className={`pointer-events-auto flex h-10 w-10 cursor-pointer items-center justify-center rounded-full shadow-md ring-2 ring-black/5 transition ${
showBookmarkPins
? "bg-primary text-white"
: "bg-white text-dark-gray hover:bg-gray-50"
}`}
>
<Bookmark className="h-5 w-5" strokeWidth={2.2} aria-hidden />
</button>
);

return (
<div className="relative h-full min-h-0 w-full min-w-0">
<div className="absolute inset-0 min-h-0 overflow-hidden">
Expand Down Expand Up @@ -519,6 +542,13 @@ export default function TripMap({
)}

{!routeView && <SelectedPlaceMarker />}

{/* PC는 확대·축소(+/−) 바로 위 — 같은 오른쪽 아래 컨트롤 자리에 넣으면 지도가 +/− 위로 쌓아 준다 */}
{routeView || isMobileDevice ? null : (
<MapControl position={ControlPosition.RIGHT_BOTTOM}>
<div className="mr-2.5">{bookmarkToggle}</div>
</MapControl>
)}
</GoogleMap>
)}
</div>
Expand All @@ -537,36 +567,21 @@ export default function TripMap({
{!bootstrap.ready || routeView ? null : (
<>
<MapSearchHereButton discoverCategoryId={selectedCategoryId} />
<div className="pointer-events-none absolute bottom-6 left-4 z-[16] flex flex-col items-center gap-3">
{onOpenRouteView ? (
<button
type="button"
onClick={onOpenRouteView}
aria-label="경로 보기"
className="pointer-events-auto flex size-[42px] cursor-pointer items-center justify-center rounded-full bg-primary text-icon-inverse shadow-md transition-colors active:bg-primary-strong"
>
<RouteIcon size={24} />
</button>
) : null}
<button
type="button"
onClick={() => setShowBookmarkPins((v) => !v)}
aria-pressed={showBookmarkPins}
aria-label={
showBookmarkPins
? "북마크 장소 표시 끄기"
: "북마크 장소 표시 켜기"
}
title="북마크 장소 표시"
className={`pointer-events-auto flex h-10 w-10 cursor-pointer items-center justify-center rounded-full shadow-md ring-2 ring-black/5 transition ${
showBookmarkPins
? "bg-primary text-white"
: "bg-white text-dark-gray hover:bg-gray-50"
}`}
>
<Bookmark className="h-5 w-5" strokeWidth={2.2} aria-hidden />
</button>
</div>
{isMobileDevice ? (
<div className="pointer-events-none absolute bottom-6 left-4 z-[16] flex flex-col items-center gap-3">
{onOpenRouteView ? (
<button
type="button"
onClick={onOpenRouteView}
aria-label="경로 보기"
className="pointer-events-auto flex size-[42px] cursor-pointer items-center justify-center rounded-full bg-primary text-icon-inverse shadow-md transition-colors active:bg-primary-strong"
>
<RouteIcon size={24} />
</button>
) : null}
{bookmarkToggle}
</div>
) : null}
</>
)}
</div>
Expand Down
10 changes: 7 additions & 3 deletions src/components/rooms/RoomTripEditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import {
RoomTripDeleteButton,
RoomTripSettingsSection,
} from "@/app/(main)/room-settings/_components/RoomTripSettingsSection";
} from "@/components/rooms/RoomTripSettingsSection";
import { SettingsDialog } from "@/components/settings/SettingsDialog";

export function RoomTripEditDialog({ onClose }: { onClose: () => void }) {
return (
<SettingsDialog title="여행 정보 수정" onClose={onClose} size="medium" className="rounded-xl" titleAccessory={<RoomTripDeleteButton />}>
<SettingsDialog title="여행 정보 수정" onClose={onClose} size="medium" className="rounded-xl">
<div className="pt-4">
<RoomTripSettingsSection embedded onCancel={onClose} onSaved={onClose} />
<RoomTripSettingsSection onSaved={onClose} />
</div>
{/* 여행 삭제는 적용하기 아래 모달 오른쪽 끝 — 방장이 아니면 버튼이 없어 줄째 숨긴다 */}
<div className="mt-3 flex justify-end empty:hidden">
<RoomTripDeleteButton />
</div>
</SettingsDialog>
);
Expand Down
23 changes: 2 additions & 21 deletions src/components/rooms/RoomTripEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@
type Props = {
room: RoomTripFormSource;
readOnly?: boolean;
onCancel?: () => void;
onSaved?: () => void;
};

export function RoomTripEditForm(props: Props) {
return <RoomTripEditSession key={props.room.id} {...props} />;
}

function RoomTripEditSession({ room, readOnly = false, onCancel, onSaved }: Props) {
function RoomTripEditSession({ room, readOnly = false, onSaved }: Props) {

Check warning on line 38 in src/components/rooms/RoomTripEditForm.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=uttae_frontend-server&issues=AaE5VLl3ZkbAOKxE6txX&open=AaE5VLl3ZkbAOKxE6txX&pullRequest=237
const saved = toTripFormValues(room);

const [title, setTitle] = useState(saved.title);
Expand Down Expand Up @@ -124,16 +123,6 @@
onSaved,
]);

function handleCancel() {
const next = toTripFormValues(room);
setTitle(next.title);
setDestinations(next.destinations);
setStartDate(next.startDate);
setEndDate(next.endDate);
setShrinkConfirmOpen(false);
onCancel?.();
}

function handleApply() {
if (!canApply) return;

Expand Down Expand Up @@ -176,15 +165,7 @@
)}

{!readOnly && (
<SettingsActionButtonRow className="gap-3 pt-0">
<SettingsActionButton
variant="secondary"
className="h-12 rounded-lg border-border bg-fill-subtle py-0 text-[16px] font-bold text-text hover:bg-fill"
onClick={handleCancel}
disabled={(!onCancel && !isDirty) || isPending}
>
취소
</SettingsActionButton>
<SettingsActionButtonRow className="pt-0">
<SettingsActionButton
variant="primary"
className="h-12 rounded-lg py-0 text-[16px] font-bold enabled:hover:bg-primary-strong hover:opacity-100 disabled:bg-fill disabled:text-text-disabled disabled:opacity-100"
Expand Down
Loading
Loading