diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6a73432..f77c30c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,7 +19,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {children} - + diff --git a/src/components/common/skeletons/ChartCardSkeleton.tsx b/src/components/common/skeletons/ChartCardSkeleton.tsx new file mode 100644 index 0000000..1e9eb92 --- /dev/null +++ b/src/components/common/skeletons/ChartCardSkeleton.tsx @@ -0,0 +1,75 @@ +import { Skeleton } from "./Skeleton"; + +type ChartCardSkeletonProps = { + titleWidth?: string; + subtitleWidth?: string; + controls?: boolean; + variant?: "line" | "donut" | "bars"; +}; + +export function ChartCardSkeleton({ + titleWidth = "w-40", + subtitleWidth = "w-28", + controls = false, + variant = "line", +}: ChartCardSkeletonProps) { + return ( +
+
+
+ + +
+ + {controls ? ( +
+ + +
+ ) : null} +
+ + {variant === "donut" ? ( +
+ +
+ {Array.from({ length: 3 }).map((_, index) => ( +
+ + + +
+ ))} +
+
+ ) : ( +
+
+ {(variant === "bars" ? Array.from({ length: 10 }) : Array.from({ length: 12 })).map( + (_, index) => ( + + ), + )} +
+
+ )} +
+ ); +} diff --git a/src/components/common/skeletons/FeedCardSkeleton.tsx b/src/components/common/skeletons/FeedCardSkeleton.tsx new file mode 100644 index 0000000..b992e9f --- /dev/null +++ b/src/components/common/skeletons/FeedCardSkeleton.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from "./Skeleton"; + +type FeedCardSkeletonProps = { + itemCount?: number; +}; + +export function FeedCardSkeleton({ itemCount = 6 }: FeedCardSkeletonProps) { + return ( +
+ + +
+ + +
+ {Array.from({ length: itemCount }).map((_, index) => ( +
+ + + +
+ ))} +
+
+
+ ); +} diff --git a/src/components/common/skeletons/Skeleton.tsx b/src/components/common/skeletons/Skeleton.tsx new file mode 100644 index 0000000..6033ef8 --- /dev/null +++ b/src/components/common/skeletons/Skeleton.tsx @@ -0,0 +1,9 @@ +import { cn } from "@/lib/utils"; + +type SkeletonProps = { + className?: string; +}; + +export function Skeleton({ className }: SkeletonProps) { + return
; +} diff --git a/src/components/common/skeletons/TableCardSkeleton.tsx b/src/components/common/skeletons/TableCardSkeleton.tsx new file mode 100644 index 0000000..8a53522 --- /dev/null +++ b/src/components/common/skeletons/TableCardSkeleton.tsx @@ -0,0 +1,58 @@ +import { Skeleton } from "./Skeleton"; + +type TableCardSkeletonProps = { + rowCount?: number; + columnCount?: number; +}; + +export function TableCardSkeleton({ rowCount = 10, columnCount = 10 }: TableCardSkeletonProps) { + return ( +
+
+ + +
+ +
+
+
+ {Array.from({ length: columnCount }).map((_, index) => ( +
+ +
+ ))} +
+ +
+ {Array.from({ length: rowCount }).map((_, rowIndex) => ( +
+ {Array.from({ length: columnCount }).map((__, cellIndex) => ( +
+ +
+ ))} +
+ ))} +
+
+
+ +
+ +
+ + + + + +
+
+
+
+ ); +} diff --git a/src/components/domain/churn/ChurnDelta.tsx b/src/components/domain/churn/ChurnDelta.tsx index d300397..1e57daa 100644 --- a/src/components/domain/churn/ChurnDelta.tsx +++ b/src/components/domain/churn/ChurnDelta.tsx @@ -14,6 +14,7 @@ import { YAxis, } from "recharts"; +import { ChartCardSkeleton } from "@/components/common/skeletons/ChartCardSkeleton"; import { useChurnTrend } from "@/lib/tanstack/query/churn/useChurnTrend"; const COLOR_POS = "var(--danger-500)"; @@ -22,22 +23,16 @@ const COLOR_ZERO = "var(--neutral-400)"; export function ChurnDelta() { const { data, isLoading, isError } = useChurnTrend(); - const [range, setRange] = useState<9 | 31>(9); const rawData = data?.data.data ?? []; - - const chartData = rawData.slice(-range).map((d) => ({ - date: d.date, - delta: d.delta, + const chartData = rawData.slice(-range).map((item) => ({ + date: item.date, + delta: item.delta, })); if (isLoading) { - return ( -
-
차트 로딩중...
-
- ); + return ; } if (isError) { @@ -58,7 +53,6 @@ export function ChurnDelta() {

- {/* 기간 선택 */}
- {/* 기간 선택 */}
{!memberId ? (
고객을 선택해주세요
- ) : isLoading ? ( -
불러오는 중...
) : isError || !member ? (
- 상세 정보를 불러오지 못했습니다 + 상세 정보를 불러오지 못했습니다.
) : (
@@ -177,7 +179,7 @@ export function CustomerModal({ open, onOpenChange, memberId }: ModalProps) {
- + 0 - ? `${member.remainingDays} 일` + ? `${member.remainingDays}일` : "-" } /> @@ -220,7 +222,7 @@ export function CustomerModal({ open, onOpenChange, memberId }: ModalProps) {
- +
@@ -236,12 +238,14 @@ export function CustomerModal({ open, onOpenChange, memberId }: ModalProps) { className="bg-secondary-500 cursor-pointer rounded-sm px-4 py-1" onClick={() => { setUpdateOpen(true); - }}> + }} + type="button"> 수정 @@ -250,7 +254,7 @@ export function CustomerModal({ open, onOpenChange, memberId }: ModalProps) { open={updateOpen} onOpenChange={setUpdateOpen} memberId={memberId} - memberName={member?.name ?? ""} + memberName={member.name} currentStatus={currentStatus} currentMembership={currentMembership} pendingStatus={effectiveStatus} diff --git a/src/components/domain/customers/modals/CustomerModalSkeleton.tsx b/src/components/domain/customers/modals/CustomerModalSkeleton.tsx new file mode 100644 index 0000000..9ffadb8 --- /dev/null +++ b/src/components/domain/customers/modals/CustomerModalSkeleton.tsx @@ -0,0 +1,120 @@ +import { X } from "lucide-react"; + +import { Skeleton } from "@/components/common/skeletons/Skeleton"; + +function SkeletonInfoRow({ interactive = false }: { interactive?: boolean }) { + return ( +
+
+ +
+
+ {interactive ? ( + + ) : ( + + )} +
+
+ ); +} + +function SkeletonSection({ + titleWidth = "w-32", + leftRows = 3, + rightRows = 3, + interactiveRows = [], +}: { + titleWidth?: string; + leftRows?: number; + rightRows?: number; + interactiveRows?: Array<"left" | "right">; +}) { + return ( + <> +
+
+
+ +
+
+ +
+ {Array.from({ length: leftRows }).map((_, index) => ( + + ))} +
+ +
+ {Array.from({ length: rightRows }).map((_, index) => ( + + ))} +
+
+ +
+
+
+
+ + ); +} + +export function CustomerModalSkeleton() { + return ( +
+
+ +
+
+ + +
+ +
+ + + +
+
+
+ +
+
+
+ + + +
+
+ + + +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/lib/axios.ts b/src/lib/axios.ts index b90e262..dd215ac 100644 --- a/src/lib/axios.ts +++ b/src/lib/axios.ts @@ -1,15 +1,7 @@ -// import axios from "axios"; - -// export const api = axios.create({ -// baseURL: "", -// timeout: 10000, -// withCredentials: true, -// }); - import axios from "axios"; export const api = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, - timeout: 10000, + baseURL: "", + timeout: 15000, withCredentials: true, });