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
22 changes: 13 additions & 9 deletions src/components/common/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,24 @@ const Header = ({
const nickname = user?.name ?? displayName ?? initialNickname ?? "닉네임";

return (
<header className="border-border-subtle bg-background-surface w-full border-b">
<div className="h-gnb-height-desktop px-gnb-padding-x-desktop flex items-center justify-between py-26">
<div className="flex items-center gap-80">
<header className="border-border-subtle bg-background-surface w-full max-w-full overflow-x-hidden border-b">
{/* Desktop 고정 GNB padding은 xl+ — Tablet에서 160px×2 padding + nav가 가로 스크롤을 만들던 문제 방지 */}
{/* 2026.08.04 정슬기 - [수정] */}
<div className="h-gnb-height-mobile md:h-gnb-height-tablet xl:h-gnb-height-desktop px-margin-mobile md:px-margin-tablet xl:px-gnb-padding-x-desktop flex w-full max-w-full items-center justify-between gap-12 py-16 xl:py-26">
<div className="flex min-w-0 flex-1 items-center gap-24 overflow-hidden xl:gap-80">
<Link href="/" className="shrink-0">
<Image src="/icons/logo_full.svg" alt="4roro-moving" width={116} height={44} priority />
</Link>

<nav aria-label="주요 메뉴">
<ul className="flex items-center gap-40">
{/* Mobile은 햄버거 전까지 링크 숨김 — 좁은 폭에서 GNB 가로 스크롤 방지 */}
{/* 2026.08.04 정슬기 - [수정] */}
<nav aria-label="주요 메뉴" className="hidden min-w-0 md:block">
<ul className="flex items-center gap-16 md:gap-24 xl:gap-40">
Comment thread
juengseulki marked this conversation as resolved.
{navLinks.map((link) => {
const isActive = isNavLinkActive(pathname, link.href);

return (
<li key={link.label}>
<li key={link.label} className="shrink-0">
<Link
href={link.href}
aria-current={isActive ? "page" : undefined}
Expand All @@ -134,15 +138,15 @@ const Header = ({
</div>

{showAuthSkeleton ? (
<div className="flex items-center gap-32" aria-hidden>
<div className="flex shrink-0 items-center gap-16 xl:gap-32" aria-hidden>
<div className="bg-background-subtle size-36 animate-pulse rounded-full" />
<div className="flex items-center gap-16">
<div className="bg-background-subtle size-36 animate-pulse rounded-full" />
<div className="bg-background-subtle rounded-4 h-20 w-64 animate-pulse" />
</div>
</div>
) : isLogin ? (
<div className="flex items-center gap-32">
<div className="flex shrink-0 items-center gap-16 xl:gap-32">
<NotificationTrigger />
<ProfileMenuTrigger
key={pathname}
Expand All @@ -154,7 +158,7 @@ const Header = ({
) : (
<Link
href={getLoginRedirectPath()}
className="bg-background-brand text-text-inverse hover:bg-background-brand-hover rounded-8 flex h-40 items-center px-20 transition-colors"
className="bg-background-brand text-text-inverse hover:bg-background-brand-hover rounded-8 flex h-40 shrink-0 items-center px-20 transition-colors"
>
<Text variant="md-semibold">로그인</Text>
</Link>
Expand Down
106 changes: 77 additions & 29 deletions src/components/estimate/detail/EstimateDetailActions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Ref } from "react";

import Button from "@/components/common/Button/Button";
import { Text } from "@/components/common/Text";
import { ConfirmedCheckIcon } from "@/icons";
import { TrashIcon } from "@/icons";
import { cn } from "@/lib/utils/cn";
import { formatPrice } from "@/lib/utils/estimateFormat";

interface EstimateDetailActionsProps {
Expand All @@ -11,14 +14,24 @@ interface EstimateDetailActionsProps {
onConfirm: () => void;
/** 대기 상세 Figma: CTA 위 견적가 재표시 */
price?: number;
/** received: sm / pending: detail */
/** 상세 CTA — received/pending 모두 detail(h-64)로 Tablet·Desktop 정렬 */
buttonSize?: "sm" | "detail";
/** 요청 상태가 PENDING|OPEN일 때만 취소 아이콘 노출 */
// 2026.08.03 정슬기 - [추가]
canCancelRequest?: boolean;
isCanceling?: boolean;
onCancelRequest?: () => void;
/** 모달 닫힘 후 포커스 복귀용 */
// 2026.08.04 정슬기 - [추가]
cancelButtonRef?: Ref<HTMLButtonElement>;
}

/**
* 견적 상세 확정 CTA (받았던·대기 상세 공통)
* // 2026.07.24 정슬기 - [추가]
* // 2026.07.30 정슬기 - [수정] PendingEstimateDetailActions 통합 (optional price·buttonSize)
* // 2026.08.03 정슬기 - [수정] 확정 Primary + Trash 아이콘 버튼
* // 2026.08.04 정슬기 - [수정] 미확정 전제 · cancel ref 포커스 복귀
*/
export default function EstimateDetailActions({
isConfirmed,
Expand All @@ -28,9 +41,20 @@ export default function EstimateDetailActions({
onConfirm,
price,
buttonSize = "sm",
canCancelRequest = false,
isCanceling = false,
onCancelRequest,
cancelButtonRef,
}: EstimateDetailActionsProps) {
const showPrice = typeof price === "number";
const showCancel = canCancelRequest && typeof onCancelRequest === "function";
// Primary(sm h-57 / detail h-64)와 Trash 정사각 높이를 맞춤 — Tablet 스택에서도 정렬 유지
// 2026.08.04 정슬기 - [수정]
const trashSizeClass = buttonSize === "detail" ? "size-64" : "size-57";
const trashIconClass = buttonSize === "detail" ? "size-24" : "size-20";

// Desktop(aside, xl+)에서만 CTA 위 견적가 — Mobile/Tablet 본문 Price와 중복 방지
// 2026.08.04 정슬기 - [수정] lg → xl
const priceBlock = showPrice ? (
<div className="flex w-full flex-col gap-0">
<Text as="p" variant="2lg-semibold" className="text-text-weak">
Expand All @@ -43,42 +67,66 @@ export default function EstimateDetailActions({
) : null;

if (isConfirmed) {
return (
<div className="flex w-full flex-col gap-16">
{priceBlock}
<div className="flex items-center justify-center gap-6">
<ConfirmedCheckIcon className="text-icon-brand size-24 shrink-0" aria-hidden="true" />
<Text as="p" variant="2lg-semibold" className="text-text-brand">
견적이 확정되었습니다
</Text>
</div>
</div>
);
// 확정 안내 문구는 DriverSummary의 "확정견적" 배지로 통일 — 중복 텍스트 제거
// 호출부는 미확정일 때만 Actions를 넘김. 방어적으로 견적가만 Desktop 노출.
// 2026.08.04 정슬기 - [수정] CodeRabbit: 확정+취소 조합은 호출부에서 제거
if (!showPrice) {
return null;
}

// Desktop aside(xl+)에서만 견적가 — Tablet 세로 스택과 본문 Price 중복 방지
// 2026.08.04 정슬기 - [수정]
return <div className="hidden w-full flex-col gap-16 xl:flex">{priceBlock}</div>;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const disabled = !canConfirm || isConfirming;
const confirmDisabled = !canConfirm || isConfirming || isCanceling;
const reason =
confirmDisabledReason ??
(!canConfirm ? "이미 확정된 견적이 있어 추가로 확정할 수 없습니다." : null);

return (
<div className={showPrice ? "flex w-full flex-col gap-30" : "flex w-full flex-col gap-12"}>
{priceBlock}
<div className={cn("flex w-full flex-col", showPrice ? "gap-12 xl:gap-30" : "gap-12")}>
{showPrice ? <div className="hidden w-full xl:block">{priceBlock}</div> : null}

<div className="flex w-full flex-col gap-12">
<Button
type="button"
variant="solid"
size={buttonSize}
fullWidth
disabled={disabled}
onClick={onConfirm}
className="max-w-full min-w-0"
aria-busy={isConfirming}
>
{isConfirming ? "확정 중..." : "견적 확정하기"}
</Button>
{disabled && reason ? (
{/* [Trash] [견적 확정하기] — Primary 높이에 맞춘 정사각 Trash */}
{/* 2026.08.04 정슬기 - [수정] Tablet/Desktop Trash·Primary 높이 정렬 */}
<div className="flex w-full flex-row items-center gap-8 md:gap-12">
{showCancel ? (
<button
ref={cancelButtonRef}
type="button"
aria-label="견적 요청 취소"
aria-busy={isCanceling}
disabled={isCanceling || isConfirming}
onClick={onCancelRequest}
className={cn(
"border-border-default text-text-primary bg-background-surface rounded-16 shrink-0 border",
"hover:bg-background-hover",
"focus-visible:ring-border-brand focus-visible:ring-2 focus-visible:outline-none",
"disabled:cursor-not-allowed disabled:opacity-40",
"flex items-center justify-center",
trashSizeClass,
)}
>
<TrashIcon className={trashIconClass} aria-hidden="true" />
</button>
) : null}

<Button
type="button"
variant="solid"
size={buttonSize}
fullWidth
disabled={confirmDisabled}
onClick={onConfirm}
className={cn("min-w-0 whitespace-nowrap", showCancel ? "flex-1" : "max-w-full")}
aria-busy={isConfirming}
>
{isConfirming ? "확정 중..." : "견적 확정하기"}
</Button>
</div>
{confirmDisabled && reason && !isConfirming && !isCanceling ? (
<Text as="p" variant="md-regular" className="text-text-muted text-center">
{reason}
</Text>
Expand Down
42 changes: 42 additions & 0 deletions src/components/estimate/detail/EstimateDetailComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Text } from "@/components/common/Text";

interface EstimateDetailCommentProps {
comment: string;
}

/**
* 견적 상세 — 기사님 코멘트 (상세 정보 블록 최하단)
* SentEstimateComment와 동일 spacing/타이포 (gap-20 md:gap-28)
* // 2026.08.03 정슬기 - [추가] 받았던/대기 견적 상세용
*/
export default function EstimateDetailComment({ comment }: EstimateDetailCommentProps) {
const trimmed = comment.trim();

if (!trimmed) {
return null;
}

return (
<section
className="flex w-full flex-col gap-20 md:gap-28"
aria-labelledby="estimate-detail-mover-comment-title"
>
<h2 id="estimate-detail-mover-comment-title" className="text-text-primary">
<Text as="span" variant="lg-semibold" className="md:hidden">
기사님 코멘트
</Text>
<Text as="span" variant="xl-semibold" className="hidden md:inline">
기사님 코멘트
</Text>
</h2>

<Text
as="p"
variant="lg-medium"
className="text-text-muted wrap-break-word whitespace-pre-wrap"
>
{trimmed}
</Text>
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export default function EstimateDetailDriverSummary({
)}
</div>

{/* md+: 소개 왼쪽 + 상태(확정견적/견적대기) 오른쪽 — 본문 컬럼 안에서 자연스럽게 정렬 */}
{/* 2026.08.03 정슬기 - [수정] 확정 안내 문구 제거 후 배지만 유지, 과도한 중앙 강제 정렬 제거 */}
<div className="flex w-full items-center justify-between gap-12">
<Text
as="p"
Expand All @@ -67,7 +69,6 @@ export default function EstimateDetailDriverSummary({
>
{intro}
</Text>

<div className="hidden shrink-0 md:block">
{isConfirmed ? (
<ConfirmedStatus />
Expand Down
26 changes: 17 additions & 9 deletions src/components/estimate/detail/EstimateDetailHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import type { ReactNode } from "react";

import { Text } from "@/components/common/Text";

interface EstimateDetailHeaderProps {
title?: string;
/** Header 우측 액션 슬롯 (보낸 요청 취소 등). 다른 상세에는 영향 없음 */
// 2026.08.03 정슬기 - [추가] optional actions
actions?: ReactNode;
}

export default function EstimateDetailHeader({ title = "견적 상세" }: EstimateDetailHeaderProps) {
export default function EstimateDetailHeader({
title = "견적 상세",
actions,
}: EstimateDetailHeaderProps) {
Comment thread
juengseulki marked this conversation as resolved.
return (
// 2026.07.24 정슬기 - [수정] Figma Mobile/Tablet 페이지 헤더 높이·여백, Desktop(lg) 기존 유지
// 2026.07.24 정슬기 - [수정] page-header height/padding·container를 디자인 토큰 유틸로 교체
<header className="bg-background-default px-margin-mobile md:px-margin-tablet h-page-header-height-mobile md:h-page-header-height-tablet lg:h-page-header-height-desktop lg:px-page-header-padding-x-desktop flex w-full items-center justify-center shadow-[0_2px_10px_0_rgba(248,248,248,0.1)]">
<div className="max-w-container-desktop flex w-full flex-1 items-center">
{/* 2026.07.24 정슬기 - [수정] 반응형 타이포를 Text variant로 분리 (단일 h1 유지) */}
<h1 className="text-text-primary">
<Text as="span" variant="2lg-semibold" className="lg:hidden">
// 2026.07.24 정슬기 - [수정] Figma Mobile/Tablet 페이지 헤더 높이·여백
// 2026.08.04 정슬기 - [수정] Desktop padding/타이포를 xl로 — Tablet(lg) 과다 padding·가로 스크롤 방지
<header className="bg-background-default px-margin-mobile md:px-margin-tablet h-page-header-height-mobile md:h-page-header-height-tablet xl:h-page-header-height-desktop xl:px-page-header-padding-x-desktop flex w-full max-w-full items-center justify-center overflow-x-hidden shadow-[0_2px_10px_0_rgba(248,248,248,0.1)]">
<div className="max-w-container-desktop flex w-full min-w-0 flex-1 items-center justify-between gap-12">
<h1 className="text-text-primary min-w-0">
<Text as="span" variant="2lg-semibold" className="xl:hidden">
{title}
</Text>
<Text as="span" variant="2xl-semibold" className="hidden lg:inline">
<Text as="span" variant="2xl-semibold" className="hidden xl:inline">
{title}
</Text>
</h1>
{actions ? <div className="flex shrink-0 items-center">{actions}</div> : null}
</div>
</header>
);
Expand Down
28 changes: 19 additions & 9 deletions src/components/estimate/detail/EstimateDetailLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import EstimatesQueryStatus from "@/components/estimate/EstimatesQueryStatus";
import { cn } from "@/lib/utils/cn";

/**
* 받은 견적·견적요청 상세 공통 레이아웃 프리셋 (tokens: pb-37-5 / w-185 / w-xs)
* 받은 견적·견적요청 상세 공통 레이아웃 프리셋 (tokens: pb-37-5 / w-210 / w-80)
* Desktop 좌우 분할은 xl(1280+) — lg(1024)에서 고정폭 1200이 Tablet 가로 스크롤을 만들던 문제 방지
* // 2026.07.30 정슬기 - [추가] 호출부 임의 px·클래스 중복 제거
* // 2026.08.04 정슬기 - [수정] lg → xl (Tablet 가로 스크롤)
*/
export const ESTIMATE_DETAIL_LAYOUT_CLASSES = {
contentClassName: "pt-24 pb-64 md:pt-28 md:pb-80 lg:pb-37-5",
rowClassName: "gap-32 md:gap-40",
mainClassName: "gap-24 md:gap-30 lg:w-185",
asideClassName: "gap-28 md:gap-40 lg:w-xs lg:overflow-clip",
contentClassName: "pt-24 pb-64 md:pt-28 md:pb-80 xl:pb-37-5",
// 본문+aside 블록을 컨테이너 안에서 가운데로 모아 좌측 치우침을 줄인다
rowClassName: "gap-32 md:gap-40 xl:justify-center xl:gap-40",
// Desktop 본문 840 + aside 320 + gap 40 = 1200 (container)
mainClassName: "gap-24 md:gap-30 xl:w-210 xl:shrink-0",
asideClassName: "gap-28 md:gap-40 xl:w-80 xl:shrink-0 xl:overflow-clip",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} as const;

interface EstimateDetailLayoutProps {
Expand All @@ -22,6 +26,9 @@ interface EstimateDetailLayoutProps {
heroName?: string;
/** 히어로에 프로필 이미지 표시 여부 (요청 상세는 false) */
showProfile?: boolean;
/** Header 우측 액션 (optional — 다른 상세 화면 무영향) */
// 2026.08.03 정슬기 - [추가]
headerActions?: ReactNode;
main: ReactNode;
aside?: ReactNode;
/** main+aside 바깥 여백·폭. Figma 화면별로 다를 수 있음 */
Expand All @@ -45,6 +52,7 @@ export default function EstimateDetailLayout({
heroImageUrl = null,
heroName = "",
showProfile = true,
headerActions,
main,
aside,
contentClassName,
Expand All @@ -55,18 +63,20 @@ export default function EstimateDetailLayout({
}: EstimateDetailLayoutProps) {
return (
<div className="bg-background-default flex w-full max-w-full flex-col items-start overflow-x-hidden">
<EstimateDetailHeader title={title} />
<EstimateDetailHeader title={title} actions={headerActions} />
<DetailHeroBanner imageUrl={heroImageUrl} name={heroName} showProfile={showProfile} />

<div
className={cn(
"px-margin-mobile md:px-margin-tablet flex w-full flex-col items-center lg:px-0",
"px-margin-mobile md:px-margin-tablet flex w-full flex-col items-center xl:px-0",
contentClassName,
)}
>
<div
className={cn(
"max-w-container-desktop flex w-full flex-col items-stretch lg:flex-row lg:items-start lg:justify-between",
// Desktop(xl+)만 좌우 분할 — Tablet(lg 포함)은 세로 스택으로 가로 스크롤 방지
// 2026.08.04 정슬기 - [수정]
"max-w-container-desktop flex w-full flex-col items-stretch xl:flex-row xl:items-start xl:justify-center",
rowClassName,
)}
>
Expand Down Expand Up @@ -103,7 +113,7 @@ export function EstimateDetailQueryState({
return (
<div className="bg-background-default flex w-full max-w-full flex-col overflow-x-hidden">
<EstimateDetailHeader title={title} />
<div className="px-margin-mobile md:px-margin-tablet flex w-full flex-col items-center lg:px-0">
<div className="px-margin-mobile md:px-margin-tablet flex w-full flex-col items-center xl:px-0">
<div className="max-w-container-desktop w-full">
<EstimatesQueryStatus message={message} actionLabel={actionLabel} onAction={onAction} />
{secondaryAction}
Expand Down
Loading