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: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"tailwindCSS.experimental.configFile": "apps/monitor-web/tailwind.config.ts",
"cSpell.words": ["jikwon", "supabase"],
"cSpell.words": ["jikwon", "junyeol", "supabase"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
Expand Down
2 changes: 2 additions & 0 deletions apps/monitor-web/src/assets/icons.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import AlertIcon from "@/assets/icons/alert.svg?react";
import BaseLogo from "@/assets/icons/base-logo.svg?react";
import Check from "@/assets/icons/check.svg?react";
import Clear from "@/assets/icons/clear.svg?react";
import EditPencil from "@/assets/icons/edit-pencil.svg?react";
import LoadingSpinner from "@/assets/icons/loading-spinner.svg?react";

export const ICON_MAP = {
alert: AlertIcon,
baseLogo: BaseLogo,
check: Check,
clear: Clear,
editPencil: EditPencil,
loadingSpinner: LoadingSpinner,
} as const;
3 changes: 3 additions & 0 deletions apps/monitor-web/src/assets/icons/edit-pencil.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 2 additions & 5 deletions apps/monitor-web/src/components/common/display/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { cn } from "@/utils";
import {
API_STATUS_BADGE_STYLES,
BADGE_BASE_STYLE,
type ApiStatus,
} from "./_internal/badge.constants";
import type { ApiStatus } from "@/types";
import { API_STATUS_BADGE_STYLES, BADGE_BASE_STYLE } from "./_internal/badge.constants";

/**
* 공통 Badge 컴포넌트입니다.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
// TODO(준열) : 디자인 시스템 정립에 따라 기본 스타일 및 API 상태에 따른 스타일 변경 예정

/**
* Badge 컴포넌트에서 사용하는 API 상태 타입입니다.
*
* @remarks
* - `healthy`: 정상
* - `degraded`: 지연
* - `outage`: 장애
*
* @author junyeol
*/

export type ApiStatus = "healthy" | "degraded" | "outage";
import type { ApiStatus } from "@/types";

/**
* API 상태별 Badge 표시 정보입니다.
Expand Down
71 changes: 71 additions & 0 deletions apps/monitor-web/src/mock/apiDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {
ApiCheckLog,
ApiDetailData,
ApiSummaryData,
DetailSettingsData,
ImpactedFeature,
} from "../pages/ApiDetail/_types";

export const MOCK_HEADER_DATA: ApiDetailData = {
name: "Kakao Map API",
statusCode: "404",
status: "healthy" as const,
category: "map",
responseTime: "428ms",
lastChecked: "2026-04-24 15:30",
successRate: "99%",
};

export const MOCK_SUMMARY_DATA: ApiSummaryData = {
avgResponseTime: 443,
maxResponseTime: 1230,
minResponseTime: 210,
successRate: 99,
errorCount: 1,
lastErrorAt: "2026-04-24 13:20",
};

export const MOCK_LOGS: ApiCheckLog[] = [
{
id: "1",
status: "healthy",
time: "15:30",
fullDate: "2024-05-13",
message: "정상 작동 중",
statusCode: "HTTP 200",
latency: "428ms",
},
{
id: "2",
status: "outage",
time: "15:20",
fullDate: "2024-05-13",
message: "Connection Timeout",
statusCode: "HTTP 504",
latency: "5000ms",
},
{
id: "3",
status: "healthy",
time: "15:10",
fullDate: "2024-05-13",
message: "정상 작동 중",
statusCode: "HTTP 200",
latency: "312ms",
},
];

export const MOCK_FEATURES: ImpactedFeature[] = [
{ id: "1", name: "지도 표시" },
{ id: "2", name: "위치 선택" },
{ id: "3", name: "게시글 작성 시 주소 검색" },
{ id: "4", name: "내 주변 분실물 조회" },
];

export const MOCK_SETTINGS: DetailSettingsData = {
requestUrl: "https://dapi.kakao.com/...",
httpMethod: "GET",
checkInterval: "10분",
isActive: true,
isNotificationEnabled: true,
};
1 change: 1 addition & 0 deletions apps/monitor-web/src/mock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./apiDetail";
31 changes: 28 additions & 3 deletions apps/monitor-web/src/pages/ApiDetail/ApiDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import { useParams } from "react-router-dom";
import { MOCK_HEADER_DATA, MOCK_SUMMARY_DATA } from "@/mock";
import {
DetailCheckLogs,
DetailHeader,
DetailImpactedFeatures,
DetailIncidentHistory,
DetailResponseChart,
DetailSettings,
DetailSummaryCards,
} from "./_components";

const ApiDetail = () => {
const { apiId } = useParams<{ apiId: string }>();
console.warn(apiId);
Comment thread
wlrnjs marked this conversation as resolved.

return (
<div>
<h1>API 상세 - {apiId}</h1>
</div>
<>
<DetailHeader apiData={MOCK_HEADER_DATA} />
<DetailSummaryCards data={MOCK_SUMMARY_DATA} />

<div className="grid h-[620px] min-h-0 grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1.7fr)_minmax(320px,1fr)]">
<DetailResponseChart />

<div className="grid min-h-0 min-w-0 grid-rows-[minmax(0,2fr)_minmax(0,1fr)] gap-8">
<DetailCheckLogs />
<DetailImpactedFeatures />
</div>
</div>

<DetailSettings />

<DetailIncidentHistory />
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { cn } from "@/utils";
import { MOCK_LOGS } from "@/mock";
import type { ApiStatus } from "@/types";

const STATUS_CONFIG: Record<ApiStatus, { label: string; color: string }> = {
healthy: { label: "정상", color: "bg-fg-primary-normal-default" },
outage: { label: "장애", color: "bg-[#FF4D4F]" },
degraded: { label: "지연", color: "bg-[#FAAD14]" },
} as const;

const DetailCheckLogs = () => {
Comment thread
wlrnjs marked this conversation as resolved.
return (
<section
aria-labelledby="logs-title"
className="flex min-h-0 min-w-0 flex-col gap-10 rounded-xl border border-[#DFDFDF] bg-white px-12 py-8"
>
<div className="flex items-center justify-between">
<h2 id="logs-title" className="text-header1-semibold">
최근 체크 로그
</h2>
<span className="text-body1-medium block text-[#1D1D1D]/40">10분 주기</span>
</div>

<div
aria-label="로그 목록"
role="region"
tabIndex={0}
className="flex flex-col gap-4 overflow-y-auto"
>
<ul className="flex flex-col gap-4">
{MOCK_LOGS.map((log) => (
<li
key={log.id}
className="flex w-full items-center justify-between border-b border-[#F5F5F5] pb-4 last:border-0"
>
<div className="flex min-w-[100px] items-center gap-[6px]">
<div
aria-label={STATUS_CONFIG[log.status].label}
role="img"
className={cn("size-3 rounded-full", STATUS_CONFIG[log.status].color)}
/>
<time dateTime={`${log.fullDate}T${log.time}`}>{log.time}</time>
</div>

<span className="flex-1 truncate px-4 text-left text-[#1D1D1D]" title={log.message}>
{log.message}
</span>

<div className="text-body1-regular flex items-center gap-8 text-[#1D1D1D]/60">
<span aria-label="상태 코드">{log.statusCode}</span>
<span aria-label="응답 시간">{log.latency}</span>
</div>
</li>
))}
</ul>
</div>
</section>
);
};

export default DetailCheckLogs;
105 changes: 105 additions & 0 deletions apps/monitor-web/src/pages/ApiDetail/_components/DetailHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Badge, BasicButton, Icon } from "@/components";
import { cn } from "@/utils";
import type { ApiStatus } from "@/types";
import type { ApiDetailData } from "../_types";

interface DetailHeaderProps {
apiData: ApiDetailData;
}

const DetailHeader = ({ apiData }: DetailHeaderProps) => {
const headerInfoList = [
{ label: "카테고리", value: apiData.category },
{ label: "응답", value: apiData.responseTime },
{
label: "마지막 체크",
value: apiData.lastChecked,
dateTime: apiData.lastChecked,
},
{ label: "최근 24시간 성공률", value: apiData.successRate },
];

return (
<section
aria-labelledby="api-detail-title"
className="-mx-8 -mt-8 flex items-center justify-between border border-[#E2E8F0] bg-white p-10"
>
<div className="flex flex-col gap-6">
<div className="flex items-center gap-2">
<h1 id="api-detail-title" className="text-header1-bold">
{apiData.name}
</h1>
<Badge aria-label={`HTTP 상태 코드: ${apiData.statusCode}`} label={apiData.statusCode} />
</div>
<dl className="flex gap-6">
<StatusItem status={apiData.status} />
{headerInfoList.map((info) => (
<InfoItem key={info.label} {...info} />
))}
</dl>
</div>

<BasicButton className="h-[80px] min-w-[121px] rounded-xl border border-[#DFDFDF] bg-white px-7 py-8 text-[#1D1D1D]">
<span className="flex items-center gap-1">
<Icon name="editPencil" size={24} />
<span className="text-body1-medium">수정</span>
</span>
</BasicButton>
</section>
);
};

export default DetailHeader;

const STATUS_CONFIG = {
Comment thread
wlrnjs marked this conversation as resolved.
healthy: {
label: "정상",
color: "bg-fg-primary-normal-default",
textColor: "text-fg-primary-normal-default",
},
outage: {
label: "장애",
color: "bg-[#FF4D4F]",
textColor: "text-[#FF4D4F]",
},
degraded: {
label: "지연",
color: "bg-[#FAAD14]",
textColor: "text-[#FAAD14]",
},
} as const;

const StatusItem = ({ status }: { status: ApiStatus }) => {
const { label, color, textColor } = STATUS_CONFIG[status];

return (
<div className="flex flex-col gap-2">
<dt className="text-body1-regular text-[#1D1D1D]/40">상태</dt>
<dd className="flex items-center gap-2">
<div aria-hidden="true" className={cn("size-3 rounded-full", color)} />
<span className={cn("text-body1-regular", textColor)}>{label}</span>
</dd>
</div>
);
};

interface InfoItemProps {
label: string;
value: string;
dateTime?: string;
}

const InfoItem = ({ label, value, dateTime }: InfoItemProps) => {
const Tag = dateTime ? "time" : "span";

return (
<div className="flex flex-col gap-2">
<dt className="text-body1-regular text-[#1D1D1D]/40">{label}</dt>
<dd>
<Tag className="text-body1-regular text-[#1D1D1D]" {...(dateTime && { dateTime })}>
{value}
</Tag>
</dd>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Badge } from "@/components";
import { MOCK_FEATURES } from "@/mock";

const DetailImpactedFeatures = () => {
return (
<section
aria-describedby="features-description"
aria-labelledby="features-title"
className="flex min-h-0 min-w-0 flex-col justify-between rounded-xl border border-[#DFDFDF] bg-white px-12 py-8"
>
<div className="flex flex-col gap-[13px]">
<h2 id="features-title" className="text-header1-semibold">
영향 받는 기능
</h2>
<span id="features-description" className="text-[#1D1D1D]/40">
이 API에 장애 시 영향을 받는 사용자 기능
</span>
</div>

<div aria-label="영향 받는 기능 목록" role="region" tabIndex={0} className="overflow-x-auto">
<ul className="flex items-center gap-3">
{MOCK_FEATURES.map((feature) => (
<li key={feature.id} className="shrink-0">
<Badge className="min-h-[40px] px-4" label={feature.name} />
</li>
))}
</ul>
</div>
</section>
);
};

export default DetailImpactedFeatures;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const DetailIncidentHistory = () => {
return (
<section
aria-labelledby="incident-title"
className="rounded-xl border border-[#DFDFDF] bg-white px-12 py-8"
>
<h2 id="incident-title" className="text-header1-bold">
최근 장애 / 에러 상세 목록
</h2>
</section>
);
};

export default DetailIncidentHistory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const DetailResponseChart = () => {
return <div className="min-w-0 bg-gray-300" title="표 들어갈 자리" />;
};

export default DetailResponseChart;
Loading