From 8db151470ce1f0aac128e1756a46795365b22830 Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 06:26:57 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[HSC-412]=20feat:=20=EC=8A=A4=EC=BC=88?= =?UTF-8?q?=EB=A0=88=ED=86=A4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/skeletons/ChartCardSkeleton.tsx | 75 +++++++++++++++++++ .../common/skeletons/FeedCardSkeleton.tsx | 27 +++++++ src/components/common/skeletons/Skeleton.tsx | 9 +++ .../common/skeletons/TableCardSkeleton.tsx | 58 ++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 src/components/common/skeletons/ChartCardSkeleton.tsx create mode 100644 src/components/common/skeletons/FeedCardSkeleton.tsx create mode 100644 src/components/common/skeletons/Skeleton.tsx create mode 100644 src/components/common/skeletons/TableCardSkeleton.tsx 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) => ( +
+ +
+ ))} +
+ ))} +
+
+
+ +
+ +
+ + + + + +
+
+
+
+ ); +} From 2d8ca76f9dcc729077f2088b7cc868d9824a2edc Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 07:02:42 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[HSC-412]=20feat:=20=EA=B3=A0=EA=B0=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC/=EC=9D=B4=ED=83=88=20=EA=B0=90=EC=A7=80=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=8A=A4=EC=BC=88?= =?UTF-8?q?=EB=A0=88=ED=86=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/domain/churn/ChurnDelta.tsx | 37 +++---- src/components/domain/churn/ChurnTotal.tsx | 25 ++--- .../domain/churn/feed/ChurnFeed.tsx | 9 +- .../domain/churn/list/ChurnList.tsx | 54 ++++------ .../domain/customers/CustomersList.tsx | 98 +++++++------------ .../domain/customers/MembershipChart.tsx | 16 ++- .../domain/customers/MonthlyMembersChart.tsx | 29 ++---- 7 files changed, 103 insertions(+), 165 deletions(-) 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 ( +
+
+ +
+
+ + +
+ +
+ + + +
+
+
+ +
+
+
+ + + +
+
+ + + +
+
+ +
+ + +
+
+
+
+ ); +} From 9716fb370d800e2e9482d89c394c3f39ec39864b Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 15:58:14 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[HSC-412]=20fix:=20console.log=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/domain/customers/CustomersList.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/domain/customers/CustomersList.tsx b/src/components/domain/customers/CustomersList.tsx index 88a7630..2d8152d 100644 --- a/src/components/domain/customers/CustomersList.tsx +++ b/src/components/domain/customers/CustomersList.tsx @@ -106,14 +106,12 @@ export function CustomersList({ const queryClient = useQueryClient(); const statusMutation = useAdminMembersStatus({ - onSuccess: (response) => { - console.log("[STATUS PATCH SUCCESS]", response); + onSuccess: () => { toast.success("상태를 변경하였습니다."); onRowSelectionChange({}); queryClient.invalidateQueries({ queryKey: ["adminMembers"] }); }, - onError: (error) => { - console.log("[STATUS PATCH ERROR]", error); + onError: () => { toast.error("상태 변경에 실패했습니다."); }, }); From 79852b021eeb6b04b34d3349c711f30c578eb55a Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 15:59:18 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[HSC-412]=20fix:=20=ED=86=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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} - + From 51408c463d36bb689fe01f963c3a8aece419ed0b Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 16:01:08 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[HSC-412]=20fix:=20=EA=B3=A0=EA=B0=9D=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=EC=97=90=20=EC=BB=AC=EB=9F=BC=20=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/domain/customers/CustomersList.tsx | 2 +- src/lib/axios.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/domain/customers/CustomersList.tsx b/src/components/domain/customers/CustomersList.tsx index 2d8152d..4249bc6 100644 --- a/src/components/domain/customers/CustomersList.tsx +++ b/src/components/domain/customers/CustomersList.tsx @@ -132,7 +132,7 @@ export function CustomersList({ const totalCount = data?.pagination.totalCount ?? 0; if (isLoading) { - return ; + return ; } if (isError) { diff --git a/src/lib/axios.ts b/src/lib/axios.ts index b90e262..fcd8067 100644 --- a/src/lib/axios.ts +++ b/src/lib/axios.ts @@ -10,6 +10,6 @@ import axios from "axios"; export const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, - timeout: 10000, + timeout: 30000, withCredentials: true, }); From 86a9f5acc823148bc3b1f6540e8f65bd542850d6 Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 16:01:51 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[HSC-412]=20fix:=20axios.ts=20=EC=9B=90?= =?UTF-8?q?=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/axios.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/lib/axios.ts b/src/lib/axios.ts index fcd8067..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: 30000, + baseURL: "", + timeout: 15000, withCredentials: true, }); From 6373e678522e4b2b0b7b3c93007c271bda6d0dfb Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 18:57:00 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[HSC-414]=20feat:=20=EC=BF=A0=ED=8F=B0=20?= =?UTF-8?q?=EC=A2=85=EB=A5=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/domain/churn/list/CouponSelect.tsx | 6 +++--- src/constants/coupons.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/domain/churn/list/CouponSelect.tsx b/src/components/domain/churn/list/CouponSelect.tsx index 974eba2..715ad6e 100644 --- a/src/components/domain/churn/list/CouponSelect.tsx +++ b/src/components/domain/churn/list/CouponSelect.tsx @@ -24,13 +24,13 @@ export function CouponSelect({ - +
    {coupons.map((coupon) => (
  • @@ -38,7 +38,7 @@ export function CouponSelect({ type="button" onClick={() => onChange(coupon.id)} className={cn( - "flex w-full items-center justify-between rounded-md px-3 py-2 text-sm", + "flex w-full items-center justify-between rounded-md px-3 py-2 text-start text-sm", value === coupon.id ? "bg-primary-500 text-neutral-0" : "hover:bg-primary-100 text-neutral-900", diff --git a/src/constants/coupons.ts b/src/constants/coupons.ts index bf3855b..f3133d9 100644 --- a/src/constants/coupons.ts +++ b/src/constants/coupons.ts @@ -1,4 +1,5 @@ export const CHURN_COUPONS = [ - { id: 1, name: "이탈 방지 쿠폰" }, - { id: 2, name: "요금제 할인 쿠폰" }, + { id: 1, name: "요금 5000원 할인 쿠폰" }, + { id: 2, name: "생일 축하 데이터 쿠폰" }, + { id: 3, name: "신규 가입자 웰컴 데이터 쿠폰" }, ]; From 2252af305cf414d9d5d9785afde5edd14c5646e9 Mon Sep 17 00:00:00 2001 From: joonhyong Date: Mon, 23 Mar 2026 19:07:29 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[HSC-414]=20fix:=20axios.ts=20timeout=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/axios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/axios.ts b/src/lib/axios.ts index dd215ac..7c6dd7e 100644 --- a/src/lib/axios.ts +++ b/src/lib/axios.ts @@ -2,6 +2,6 @@ import axios from "axios"; export const api = axios.create({ baseURL: "", - timeout: 15000, + timeout: 18000, withCredentials: true, });