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
61 changes: 61 additions & 0 deletions src/components/estimate/detail/EstimateChatAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { useRouter } from "next/navigation";
import { useState } from "react";

import Button from "@/components/common/Button/Button";
import Toast from "@/components/common/Toast/Toast";
import { useGetOrCreateChatRoom } from "@/hooks/useChatRoom";
import { WriteIcon } from "@/icons";
import { getApiErrorMessage } from "@/lib/api/getApiErrorMessage";
import { APP_ROUTES } from "@/lib/constants/appRoutes";
import { cn } from "@/lib/utils/cn";

interface EstimateChatActionProps {
estimateId: number;
className?: string;
buttonClassName?: string;
}

/**
* 견적 상세 μ±„νŒ…λ°© μ§„μž… CTA
* // 2026.08.06 κΉ€μ„±ν˜„ - [μΆ”κ°€] 고객·기사 상세 곡톡 μ±„νŒ…λ°© 생성 및 이동
*/
export default function EstimateChatAction({
estimateId,
className,
buttonClassName,
}: EstimateChatActionProps) {
const router = useRouter();
const [toastMessage, setToastMessage] = useState<string | null>(null);
const chatRoomMutation = useGetOrCreateChatRoom();

const handleClick = async () => {
try {
const room = await chatRoomMutation.mutateAsync({ estimateId });
router.push(APP_ROUTES.CHATS.ROOM(room.id));
} catch (error) {
setToastMessage(getApiErrorMessage(error, "μ±„νŒ…λ°©μ„ μ€€λΉ„ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."));
}
};

return (
<>
<div className={cn("flex w-full justify-end", className)}>
<Button
variant="outline"
size="cta"
className={cn("w-[200px] whitespace-nowrap", buttonClassName)}
disabled={chatRoomMutation.isPending}
aria-busy={chatRoomMutation.isPending}
onClick={() => void handleClick()}
Comment thread
juengseulki marked this conversation as resolved.
rightIcon={<WriteIcon className="size-20 shrink-0" aria-hidden="true" />}
>
{chatRoomMutation.isPending ? "μ±„νŒ… μ€€λΉ„ 쀑" : "μ±„νŒ…ν•˜κΈ°"}
</Button>
</div>

{toastMessage ? <Toast onClose={() => setToastMessage(null)}>{toastMessage}</Toast> : null}
</>
);
}
2 changes: 1 addition & 1 deletion src/components/estimate/detail/EstimateDetailLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const ESTIMATE_DETAIL_LAYOUT_CLASSES = {
// Desktop λ³Έλ¬Έ 840 + aside 320 + gap 40 = 1200 (container)
// overflow-clip 제거 β€” focus ring이 aside κ²½κ³„μ—μ„œ μž˜λ¦¬μ§€ μ•Šλ„λ‘ 폭만으둜 μ œν•œ
mainClassName: "gap-24 md:gap-30 xl:w-210 xl:shrink-0",
asideClassName: "gap-28 md:gap-40 xl:w-80 xl:shrink-0",
asideClassName: "gap-28 md:gap-40 xl:w-[320px] xl:shrink-0",
Comment thread
juengseulki marked this conversation as resolved.
} as const;

interface EstimateDetailLayoutProps {
Expand Down
34 changes: 21 additions & 13 deletions src/components/estimate/pending/PendingEstimateDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useState } from "react";

import Toast from "@/components/common/Toast/Toast";
import EstimateChatAction from "@/components/estimate/detail/EstimateChatAction";
import EstimateDetailActions from "@/components/estimate/detail/EstimateDetailActions";
import EstimateDetailComment from "@/components/estimate/detail/EstimateDetailComment";
import EstimateDetailDriverSummary from "@/components/estimate/detail/EstimateDetailDriverSummary";
Expand Down Expand Up @@ -42,6 +43,7 @@ function PendingEstimateDetailContent({ estimateId, data }: PendingEstimateDetai
const estimateRequestId = data.estimateRequest.id;
const canCancelRequest = isCancelableEstimateRequestStatus(data.estimateRequest.status);
const displayName = data.mover.nickname || data.mover.name;
const showChatAction = data.status === "SENT";

const confirmMutation = useConfirmEstimate(estimateId, {
onSuccess: () => setConfirmToastMessage("견적이 ν™•μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€."),
Expand Down Expand Up @@ -77,19 +79,24 @@ function PendingEstimateDetailContent({ estimateId, data }: PendingEstimateDetai
</>
}
aside={
<EstimateDetailActions
price={data.price}
buttonSize="detail"
isConfirmed={data.isConfirmed}
canConfirm={data.canConfirm}
confirmDisabledReason={data.confirmDisabledReason}
isConfirming={confirmMutation.isPending}
onConfirm={() => confirmMutation.mutate()}
canCancelRequest={canCancelRequest}
isCanceling={cancelFlow.isCancelPending}
onCancelRequest={cancelFlow.openCancelModal}
cancelButtonRef={cancelFlow.cancelButtonRef}
/>
<div className="flex w-full flex-col gap-16">
<EstimateDetailActions
price={data.price}
buttonSize="detail"
isConfirmed={data.isConfirmed}
canConfirm={data.canConfirm}
confirmDisabledReason={data.confirmDisabledReason}
isConfirming={confirmMutation.isPending}
onConfirm={() => confirmMutation.mutate()}
canCancelRequest={canCancelRequest}
isCanceling={cancelFlow.isCancelPending}
onCancelRequest={cancelFlow.openCancelModal}
cancelButtonRef={cancelFlow.cancelButtonRef}
/>
{showChatAction ? (
<EstimateChatAction estimateId={estimateId} buttonClassName="w-full" />
) : null}
</div>
}
/>

Expand Down Expand Up @@ -119,6 +126,7 @@ function PendingEstimateDetailContent({ estimateId, data }: PendingEstimateDetai
* // 2026.07.25 μ •μŠ¬κΈ° - [μΆ”κ°€]
* // 2026.07.30 μ •μŠ¬κΈ° - [μˆ˜μ •] useEstimateDetailΒ·LayoutΒ·Actions 톡합
* // 2026.08.03 μ •μŠ¬κΈ° - [μˆ˜μ •] λ ˆμ΄μ•„μ›ƒΒ·μ½”λ©˜νŠΈΒ·μš”μ²­ μ·¨μ†Œ μ•‘μ…˜ Β· Header λ’€λ‘œκ°€κΈ°
* // 2026.08.06 κΉ€μ„±ν˜„ - [μˆ˜μ •] λŒ€κΈ° 견적 상세 μ±„νŒ…λ°© μ§„μž… CTA μΆ”κ°€
*/
export default function PendingEstimateDetailView({ estimateId }: PendingEstimateDetailViewProps) {
const { data, isLoading, isError, error, refetch } = useEstimateDetail(estimateId);
Expand Down
13 changes: 13 additions & 0 deletions src/components/estimate/sent/SentEstimateChatAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import EstimateChatAction from "@/components/estimate/detail/EstimateChatAction";

interface SentEstimateChatActionProps {
estimateId: number;
}

/**
* // 2026.08.06 κΉ€μ„±ν˜„ - [μΆ”κ°€] 보낸 견적 상세 μ±„νŒ…λ°© μ§„μž… CTA
* // 2026.08.06 κΉ€μ„±ν˜„ - [μˆ˜μ •] 곡톡 EstimateChatAction μž¬μ‚¬μš©
*/
export default function SentEstimateChatAction({ estimateId }: SentEstimateChatActionProps) {
return <EstimateChatAction estimateId={estimateId} />;
}
10 changes: 8 additions & 2 deletions src/components/estimate/sent/SentEstimateDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import { Text } from "@/components/common/Text";
import EstimateDetailLayout, {
ESTIMATE_DETAIL_LAYOUT_CLASSES,
EstimateDetailQueryState,
} from "@/components/estimate/detail/EstimateDetailLayout";
import { EstimateDetailInfoSection } from "@/components/estimate/detail/EstimateDetailInfoSection";
import EstimateDetailPrice from "@/components/estimate/detail/EstimateDetailPrice";
import SentEstimateChatAction from "@/components/estimate/sent/SentEstimateChatAction";
import { MoveTypeChip } from "@/components/common/Chip/MoveTypeChip";
import DesignatedChip from "@/components/estimate/DesignatedChip";
import { useSentEstimateDetail } from "@/hooks/useSentEstimates";
Expand Down Expand Up @@ -124,14 +126,17 @@ export default function SentEstimateDetailPage({ estimateId }: SentEstimateDetai

const estimate = query.data;
const request = estimate.estimateRequest;
// 2026.08.06 κΉ€μ„±ν˜„ - [μˆ˜μ •] 견적 쑰율 κ°€λŠ₯ν•œ 보낸 κ²¬μ μ—μ„œ μ±„νŒ…λ°© μ§„μž… CTA λ…ΈμΆœ
const showChatAction = estimate.status === "SENT";

return (
<EstimateDetailLayout
showProfile={false}
backFallbackHref={APP_ROUTES.MOVER_ESTIMATES.SENT}
contentClassName="pt-35 pb-64 md:pt-[46px] md:pb-80 lg:pt-[43px] lg:pb-37-5"
rowClassName="gap-20 md:gap-32 lg:gap-0"
mainClassName="gap-20 md:gap-30 lg:w-210"
rowClassName={ESTIMATE_DETAIL_LAYOUT_CLASSES.rowClassName}
mainClassName="gap-20 md:gap-30 xl:w-210 xl:shrink-0"
asideClassName="items-end gap-28 md:gap-40 xl:w-80 xl:shrink-0"
main={
<>
<div className="flex w-full flex-col gap-20 md:gap-26">
Expand Down Expand Up @@ -170,6 +175,7 @@ export default function SentEstimateDetailPage({ estimateId }: SentEstimateDetai
<SentEstimateComment comment={estimate.comment} />
</>
}
aside={showChatAction ? <SentEstimateChatAction estimateId={estimate.id} /> : undefined}
/>
);
}
4 changes: 4 additions & 0 deletions src/lib/constants/appRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ export const APP_ROUTES = {
SENT_DETAIL: (estimateId: number) => `/estimate/sent/${estimateId}`,
REJECTED: "/estimate/rejected",
},
// 2026.08.06 κΉ€μ„±ν˜„ - [μΆ”κ°€] μ±„νŒ…λ°© 상세 νŽ˜μ΄μ§€ 경둜
CHATS: {
ROOM: (roomId: number) => `/chats/${roomId}`,
},
} as const;