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
2 changes: 2 additions & 0 deletions src/app/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { mapHomeLoader } from "@/pages/map/map-home-loader";
import NicknamePage from "@/pages/onboarding/NicknamePage";
import TermsAgreementPage from "@/pages/onboarding/TermsAgreementPage";
import SplashScreenPage from "@/pages/SplashScreenPage";
import MyPagePreview from "@/pages/tabs/MyPage";

const MapHomePage = lazy(() => import("@/pages/MapHomePage"));
const RoomMainPage = lazy(() => import("@/pages/room/RoomMainPage"));
Expand All @@ -29,6 +30,7 @@ export const router = createBrowserRouter([
{ path: "dev/splash", element: <SplashScreenPage /> },
{ path: "dev/click_place", element: <DevClickPlacePage /> },
{ path: "dev/SelectOption", element: <DevSelectOptionPage /> },
{ path: "dev/mypage", element: <MyPagePreview /> },
{ path: "login", element: <LoginPage /> },
{ path: "dev/course", element: <CoursePlannerPage skipRoomGuard /> },
{ path: "app", element: <Navigate to="/" replace /> },
Expand Down
92 changes: 48 additions & 44 deletions src/components/course-planner/CoursePlaceInfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ type CoursePlaceInfoPanelProps = {
onBack: () => void;
/** `fromEditMode`: 편집 후 저장이면 상위에서 상세 화면만 갱신, 아니면 신규 저장 플로우(예: 플래너 초기화) */
onSave: (nextTitle: string, nextStops: CourseStop[], fromEditMode: boolean) => void;
/** true면 조회 모드에서 「데이트코스 저장하기」 버튼을 숨김 — 이미 저장된 코스(마이 페이지 등) */
hideNewCourseSaveButton?: boolean;
};

export function CoursePlaceInfoPanel({
courseTitle,
stops,
onBack,
onSave,
hideNewCourseSaveButton = false,
}: CoursePlaceInfoPanelProps) {
const openDetail = usePlaceDetailStore((s) => s.openDetail);

Expand Down Expand Up @@ -104,49 +107,50 @@ export function CoursePlaceInfoPanel({
const displayStops = isEditing ? draftStops : stops;

return (
<section className="bg-background px-6 pt-8 pb-0">
<header className="flex items-center gap-2">
<button
type="button"
onClick={() => {
if (isEditing) {
handleCancelEdit();
return;
}
onBack();
}}
className="text-muted-foreground hover:bg-muted/45 focus-visible:ring-ring/50 inline-flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-3 focus-visible:outline-none"
aria-label={isEditing ? "편집 취소" : "코스 목록으로 돌아가기"}
>
<ChevronLeft className="size-4" aria-hidden />
</button>
<section
className={cn(
"bg-background px-6 pt-8",
hideNewCourseSaveButton && !isEditing
? "pb-[max(1.25rem,calc(env(safe-area-inset-bottom)+1rem))]"
: "pb-0",
)}
>
{isEditing ? (
<header className="w-full">
<input
value={draftTitle}
onChange={(event) => setDraftTitle(event.target.value)}
className="text-foreground placeholder:text-muted-foreground border-border focus:border-primary w-full border-b bg-transparent pt-0.5 pb-2 text-lg leading-snug font-semibold transition-colors outline-none"
placeholder="코스 이름"
aria-label="코스 이름 편집"
/>
</header>
) : (
<header className="flex w-full items-center gap-2">
<button
type="button"
onClick={onBack}
className="text-muted-foreground hover:bg-muted/45 focus-visible:ring-ring/50 inline-flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-3 focus-visible:outline-none"
aria-label="코스 목록으로 돌아가기"
>
<ChevronLeft className="size-4" aria-hidden />
</button>

<div className="flex min-w-0 flex-1 items-center gap-1">
{isEditing ? (
<input
value={draftTitle}
onChange={(event) => setDraftTitle(event.target.value)}
className="text-foreground placeholder:text-muted-foreground border-border focus:border-primary min-w-0 flex-1 shrink border-b bg-transparent pt-0.5 pb-2 text-lg leading-snug font-semibold transition-colors outline-none"
placeholder="코스 이름"
aria-label="코스 이름 편집"
/>
) : (
<>
<h1 className="text-foreground min-w-0 shrink truncate text-lg leading-snug font-semibold">
{courseTitle}
</h1>
<button
type="button"
onClick={handleStartEdit}
className="text-muted-foreground hover:bg-muted/45 focus-visible:ring-ring/50 inline-flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-3 focus-visible:outline-none"
aria-label="코스 편집하기"
>
<Pencil className="size-3.5" aria-hidden />
</button>
</>
)}
</div>
</header>
<div className="flex min-w-0 flex-1 items-center gap-1">
<h1 className="text-foreground min-w-0 shrink truncate text-lg leading-snug font-semibold">
{courseTitle}
</h1>
<button
type="button"
onClick={handleStartEdit}
className="text-muted-foreground hover:bg-muted/45 focus-visible:ring-ring/50 inline-flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-3 focus-visible:outline-none"
aria-label="코스 편집하기"
>
<Pencil className="size-3.5" aria-hidden />
</button>
</div>
</header>
)}

<div className="mt-6 flex flex-col gap-5">
{isEditing ? (
Expand Down Expand Up @@ -214,7 +218,7 @@ export function CoursePlaceInfoPanel({
</div>

{isEditing ? (
<div className="mt-6 flex gap-2">
<div className="mt-6 flex w-full gap-2">
<button
type="button"
onClick={handleCancelEdit}
Expand All @@ -233,7 +237,7 @@ export function CoursePlaceInfoPanel({
수정하기
</button>
</div>
) : (
) : hideNewCourseSaveButton ? null : (
<button
type="button"
onClick={() => setIsSaveConfirmOpen(true)}
Expand Down
63 changes: 45 additions & 18 deletions src/components/course-planner/CoursePlaceTagSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { CategoryChip } from "@/components/map/CategoryChip";
import { MAP_FILTER_PANEL_GROUP_TITLE_CLASS } from "@/components/map/chip-style";
import type { MapFilterBarProps } from "@/components/map/filters/map-filter-bar-props";
import { getMapCategoryChipHighlighted } from "@/components/map/filters/map-filter-selection";
import { TagChip } from "@/components/map/TagChip";
import { cn } from "@/lib/utils";
import { MAP_ALL_CATEGORY_FILTER_CHIP } from "@/shared/types/map-home";
import type { Tag } from "@/features/map/api/place-taxonomy-types";
import { isDefaultGroup, isEmptyGroup } from "@/features/map/utils/filter-panel-group";
import { MAP_ALL_CATEGORY_FILTER_CHIP, type MapPrimaryCategory } from "@/shared/types/map-home";

const CATEGORY_CHIP_GRID_CLASS =
"grid w-full min-w-0 grid-cols-4 gap-2 overflow-visible pb-0.5 pt-0.5";
Expand Down Expand Up @@ -42,8 +44,30 @@ export function CoursePlaceTagSelector({
? (filterCategories.find((category) => category.code === focusedCategory) ?? null)
: null;
const selectedKeys = focusedSection ? (selectedTagKeysByCategory[focusedSection.code] ?? []) : [];
const focusedTags =
focusedSection?.tagGroups.flatMap((group) => group.tags).filter((tag) => tag.name.trim()) ?? [];

function renderTagChipRow(tags: Tag[], categoryCode: MapPrimaryCategory) {
const visibleTags = tags.filter((tag) => tag.name.trim());
if (visibleTags.length === 0) return null;

return (
<div className="flex flex-wrap gap-2" role="group">
{visibleTags.map((tag) => (
<TagChip
key={`${categoryCode}-${tag.code}`}
label={tag.name}
selected={selectedKeys.includes(tag.code)}
onClick={() => onToggleTagInCategory(categoryCode, tag.code)}
/>
))}
</div>
);
}

const hasRenderableTagGroups =
focusedSection?.tagGroups.some((group) => {
if (isEmptyGroup(group)) return false;
return group.tags.some((tag) => tag.name.trim());
}) ?? false;

return (
<div className="grid gap-2">
Expand Down Expand Up @@ -87,23 +111,26 @@ export function CoursePlaceTagSelector({
</div>
) : null}

{focusedSection && focusedTags.length > 0 ? (
{focusedSection && hasRenderableTagGroups ? (
<div
className={cn(
"border-border/35 bg-background/88 scrollbar-hide flex max-h-32 flex-wrap gap-2 overflow-y-auto rounded-2xl border px-3 py-3",
"md:max-h-40",
)}
role="group"
className="grid gap-3 pt-1"
role="region"
aria-label={`${focusedSection.name} 상세 태그`}
>
{focusedTags.map((tag) => (
<TagChip
key={`${focusedSection.code}-${tag.code}`}
label={tag.name}
selected={selectedKeys.includes(tag.code)}
onClick={() => onToggleTagInCategory(focusedSection.code, tag.code)}
/>
))}
{focusedSection.tagGroups.map((group) => {
if (isEmptyGroup(group)) return null;
const row = renderTagChipRow(group.tags, focusedSection.code);
if (!row) return null;

return (
<div key={`${focusedSection.code}-${group.code}`}>
{!isDefaultGroup(group) ? (
<div className={MAP_FILTER_PANEL_GROUP_TITLE_CLASS}>{group.name}</div>
) : null}
{row}
</div>
);
})}
</div>
) : null}
</div>
Expand Down
7 changes: 1 addition & 6 deletions src/components/course-planner/CoursePlannerBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ export function CoursePlannerBottomSheet({
children,
}: CoursePlannerBottomSheetProps) {
return (
<BottomSheet
open={open}
onClose={onClose}
panelClassName="max-h-[calc(100dvh-2rem)]"
enableHistory={false}
>
<BottomSheet open={open} onClose={onClose} intrinsicPanelHeight enableHistory={false}>
{children}
</BottomSheet>
);
Expand Down
9 changes: 7 additions & 2 deletions src/components/course-planner/DateTimeSelectionPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,15 @@ function getMonthMatrixBase(date: Date) {
type DateCalendarPanelProps = {
selectedDate: string | null;
onSelectDate: (date: string) => void;
className?: string;
};

/** 날짜만 선택하는 캘린더 카드 */
export function DateCalendarPanel({ selectedDate, onSelectDate }: DateCalendarPanelProps) {
export function DateCalendarPanel({
selectedDate,
onSelectDate,
className,
}: DateCalendarPanelProps) {
const parsedAnchorDate = useMemo(() => parseDateAnchor(selectedDate), [selectedDate]);
const [visibleMonth, setVisibleMonth] = useState(() => startOfMonth(parsedAnchorDate));

Expand All @@ -72,7 +77,7 @@ export function DateCalendarPanel({ selectedDate, onSelectDate }: DateCalendarPa
};

return (
<div className="border-border bg-card overflow-hidden rounded-xl border">
<div className={cn("border-border bg-card overflow-hidden rounded-xl border", className)}>
<header className="flex items-center justify-between px-3.5 py-4">
<button
type="button"
Expand Down
26 changes: 15 additions & 11 deletions src/components/map/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function CategoryChipSkeletonList() {
}

export function FilterBar({
hideTagPanel = false,
categories,
categoryNameByCode,
filterCategories,
Expand All @@ -42,6 +43,7 @@ export function FilterBar({
onCloseTagPanel,
}: MapFilterBarProps) {
const highlightCtx = { activeCategories, focusedCategory };
const panelOpenForUi = hideTagPanel ? false : isTagPanelOpen;

return (
<div>
Expand All @@ -63,9 +65,9 @@ export function FilterBar({
: (categoryNameByCode[category] ?? category)
}
highlighted={getMapCategoryChipHighlighted(category, highlightCtx)}
panelFocused={isTagPanelOpen && focusedCategory === category}
panelFocused={panelOpenForUi && focusedCategory === category}
selectedTagCount={
category === MAP_ALL_CATEGORY_FILTER_CHIP
hideTagPanel || category === MAP_ALL_CATEGORY_FILTER_CHIP
? 0
: (selectedTagCountByCategory[category] ?? 0)
}
Expand All @@ -89,15 +91,17 @@ export function FilterBar({
</div>
) : null}

<FilterPanel
isOpen={!isCategoryLoading && isTagPanelOpen}
focusedCategory={focusedCategory}
filterCategories={filterCategories}
selectedTagKeysByCategory={selectedTagKeysByCategory}
onToggleTagInCategory={onToggleTagInCategory}
onResetFocusedCategoryTags={onResetFocusedCategoryTags}
onClose={onCloseTagPanel}
/>
{!hideTagPanel ? (
<FilterPanel
isOpen={!isCategoryLoading && isTagPanelOpen}
focusedCategory={focusedCategory}
filterCategories={filterCategories}
selectedTagKeysByCategory={selectedTagKeysByCategory}
onToggleTagInCategory={onToggleTagInCategory}
onResetFocusedCategoryTags={onResetFocusedCategoryTags}
onClose={onCloseTagPanel}
/>
) : null}
</div>
);
}
2 changes: 2 additions & 0 deletions src/components/map/filters/map-filter-bar-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { MapCategoryFilterChip, MapPrimaryCategory } from "@/shared/types/m

/** 지도 검색 오버레이 내부에서 사용하는 FilterBar/Panel의 렌더링 상태 Props */
export type MapFilterBarProps = {
/** true면 상세 태그 패널을 렌더하지 않고 카테고리 칩만 표시한다. */
hideTagPanel?: boolean;
categories: MapCategoryFilterChip[];
categoryNameByCode: Record<MapPrimaryCategory, string>;
filterCategories: Category[];
Expand Down
25 changes: 25 additions & 0 deletions src/components/mypage/MyAccountActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type MyAccountActionsProps = {
onLogout?: () => void;
onWithdraw?: () => void;
};

export function MyAccountActions({ onLogout, onWithdraw }: MyAccountActionsProps) {
return (
<div className="mt-6 flex justify-center gap-2 pb-4">
<button
type="button"
onClick={onLogout}
className="h-8 rounded-md border border-[#e5e5e5] bg-white px-4 text-xs font-medium text-[#444444] active:bg-[#f7f7f7]"
>
로그아웃
</button>
<button
type="button"
onClick={onWithdraw}
className="h-8 rounded-md border border-[#e5e5e5] bg-white px-4 text-xs font-medium text-[#444444] active:bg-[#f7f7f7]"
>
회원탈퇴
</button>
</div>
);
}
Loading
Loading