Files
HMDesign/src/center/center-facility.stories.tsx
T
shkim4 ed7e8790ec style(storybook): tidy CENTER, chart, org chart and SW story pages
- CENTER plain page layout and shorter reference demo notes (spec values
  moved to the CENTER README); chart specimen descriptions trimmed
- Org chart page names and previews; SW width rows follow the 30px frame
  margin (1980 / 1065 / 727px) and the SW tests match the current layout
- Story inventory regenerated and WORKING_CONTEXT updated
2026-11-13 17:58:01 +09:00

258 lines
14 KiB
TypeScript

import type { ComponentType, ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
CompactFacilityConnection,
FACILITY_CONFIGS,
LocalAssetFacility,
RoadsideFacilityTag,
TunnelFacility,
type FacilityConfig,
type RoadsideEntity,
} from "@hm-design/center-graphic"
import { LinkedFacilityReferenceDemo } from "@hm-design/center-graphic/reference"
import { StoryPage, StoryPageHeader } from "@/components/story-page"
import { withRepresentation } from "./center-shared"
// Owners: ui-viz docs/visualization/facility.md and facility-assets.md (128 approved SVG references, shipped unchanged).
// The display scale (Q2b) is HOLD and Facility has no Selected / Hover state.
const meta = {
title: "노선도/시설물",
id: "center-graphic-facility",
component: TunnelFacility,
parameters: { layout: "padded" },
} satisfies Meta<typeof TunnelFacility>
export default meta
type Story = StoryObj<typeof meta>
// Common Story page. The CENTER representation wraps only the specimen, so the page title is never scaled with the graphic.
// 2026-11-13: no visible "개요" section title (사업명 page rule); the specimens keep their indent via the common `sb-page-indent`.
const page = (name: string, Demo: ComponentType): Story => ({
name,
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader title={name} />
<div className="sb-page-indent">
{withRepresentation(() => <Demo />)}
</div>
</StoryPage>
),
})
// 터널 page layout (2026-11-13): composed here from the public API (same props as the package TunnelReferenceDemo) so the
// specimens can be grouped — one width-behaviour section with a Position Map column (SEPARATED → SHORT → MEDIUM → LONG) and an
// Actual / Detail column (SEPARATED → ACTUAL SHORT → REF → LONG), same steps on the same row (1 column below a 1180px content
// width), and the compact applied example full width below. The former standalone Position Map / Actual examples are the
// MEDIUM / ACTUAL REF rows (same default name and length). Package classes are reused so the common title row, 12px title gap and the
// 8px local scrollbar still apply; the layout lives in center-stories.css (`.sb-center-tunnel*`).
const TUNNEL_WIDTH_NAMES: [string, string, string][] = [
["SHORT", "터널", "L=80m"],
["MEDIUM", "대산2터널", "L=750m"],
["LONG", "서해안고속도로 대산연결 제2터널", "L=12,480m"],
]
const TUNNEL_ACTUAL_LABELS = ["ACTUAL SHORT", "ACTUAL REF", "ACTUAL LONG"]
function TunnelSample({ title, note, children }: { title: string; note?: string; children: ReactNode }) {
return (
<article className="tunnel-reference__sample">
<header>
<strong>{title}</strong>
{note ? <span>{note}</span> : null}
</header>
<div className="tunnel-reference__stage">{children}</div>
</article>
)
}
function TunnelSpecimens() {
return (
<div className="sb-center-tunnel">
<div className="tunnel-reference">
{/* 2026-11-13: no "너비 동작" title row; each column carries the common specimen title (English + Korean note). */}
<article className="tunnel-reference__sample">
<div className="tunnel-reference__stage sb-center-tunnel__width-stage">
<div className="sb-center-tunnel__width">
{(["positionMap", "actualDetail"] as const).map((variant) => (
<div className="tunnel-reference__width-tests sb-center-tunnel__width-group" key={variant}>
<header>
<strong>{variant === "positionMap" ? "Position Map" : "Actual / Detail"}</strong>
<span>{variant === "positionMap" ? "위치도" : "실사화"}</span>
</header>
<div>
<span>SEPARATED</span>
<TunnelFacility variant={variant} separated />
</div>
{TUNNEL_WIDTH_NAMES.map(([size, name, length], index) => (
<div key={size}>
<span>{variant === "positionMap" ? size : TUNNEL_ACTUAL_LABELS[index]}</span>
<TunnelFacility name={name} secondaryText={length} variant={variant} />
</div>
))}
</div>
))}
</div>
</div>
</article>
<TunnelSample title="간략 적용 예제" note="노선 + 연결 + 터널 + 레이블">
<CompactFacilityConnection className="tunnel-applied-example" routeMode="inline" segmentShape="tunnel" sourceStatus="SOURCE_RECEIVED" label="노선과 터널 시설물의 간략 적용 예제">
<TunnelFacility name="대산2터널" secondaryText="L=750m" variant="positionMap" />
</CompactFacilityConnection>
</TunnelSample>
</div>
</div>
)
}
// 교량 · 교차로 · 분기점 · 나들목 page layout (2026-11-13): the 터널 layout applied to the package FacilityReferenceDemo
// specimens, composed from the public API with the same props. Position Map | Actual / Detail columns (SEPARATED first, then
// SHORT / MEDIUM / LONG and ACTUAL SHORT / REF / LONG on the same rows), the compact applied example full width below. The former
// standalone Position Map / Actual examples are the MEDIUM / ACTUAL REF rows (default name and secondary text) and the former
// Separated example is each column's first row. `data-sb-compare-at` raises the 2-column breakpoint when the widest row needs it
// (교량 LONG 511px artwork: 1210px); without it the 터널 1180px rule applies.
function facilityWidthNames(config: FacilityConfig) {
return [
config.id === "bridge" ? "1교" : "신시",
config.defaultName,
config.id === "bridge" ? "서해안고속도로 대산연결 제2교량" : `서해안고속도로 ${config.defaultName}`,
]
}
function FacilitySpecimens({ config, compareAt }: { config: FacilityConfig; compareAt?: "1210" }) {
const widthNames = facilityWidthNames(config)
return (
<div className="sb-center-tunnel" data-sb-compare-at={compareAt}>
<div className="facility-reference" data-reference-facility={config.id}>
<article className="facility-reference__sample">
<div className="facility-reference__stage sb-center-tunnel__width-stage">
<div className="sb-center-tunnel__width">
{(["positionMap", "actualDetail"] as const).map((variant) => (
<div className="facility-reference__width-tests sb-center-tunnel__width-group" key={variant}>
<header>
<strong>{variant === "positionMap" ? "Position Map" : "Actual / Detail"}</strong>
<span>{variant === "positionMap" ? "위치도" : "실사화"}</span>
</header>
<div>
<span>SEPARATED</span>
<LocalAssetFacility config={config} variant={variant} separated />
</div>
{widthNames.map((name, index) => (
<div key={name}>
<span>{variant === "positionMap" ? ["SHORT", "MEDIUM", "LONG"][index] : `ACTUAL ${["SHORT", "REF", "LONG"][index]}`}</span>
<LocalAssetFacility config={config} name={name} variant={variant} />
</div>
))}
</div>
))}
</div>
</div>
</article>
<TunnelSample
title="간략 적용 예제"
note={config.id === "interchange" ? "SOURCE_PENDING · 기존 Preview 전용; 정확한 IC 구성 추론 안 함" : "SOURCE_RECEIVED · 관계 비교, 정확한 픽셀 정합 아님"}
>
<CompactFacilityConnection
className={`facility-applied facility-applied--${config.routeMode}`}
routeMode={config.routeMode}
segmentShape={config.id === "bridge" ? "bridge" : undefined}
sourceStatus={config.id === "interchange" ? "SOURCE_PENDING" : "SOURCE_RECEIVED"}
label={`${config.label} 간략 적용 예제`}
>
<LocalAssetFacility config={config} />
</CompactFacilityConnection>
</TunnelSample>
</div>
</div>
)
}
// 연계 노선 page layout (2026-11-13): the package RoadsideReferenceDemo specimens in the same 2-column structure and the same
// single-line row labels as the other facility pages (SHORT / MEDIUM / LONG, ACTUAL SHORT / REF / LONG; the former two-line
// "휴게소 · SHORT" meta is replaced — the entity stays in the tag text and its accessible name). Widest row 64px + 16px + 450px
// LONG tag, so the 터널 1180px rule applies. Separated exists for Actual / Detail only, so it is that column's first row; at
// 2 columns the Position Map column keeps an empty first row so the steps stay on the same rows, at 1 column the empty row is
// not rendered. The 연계 / 노선 맥락 example is full width below.
const ROADSIDE_TESTS: { size: string; actual: string; entity: RoadsideEntity; label: string }[] = [
{ size: "SHORT", actual: "ACTUAL SHORT", entity: "rest-area", label: "휴게소" },
{ size: "MEDIUM", actual: "ACTUAL REF", entity: "drowsy-shelter", label: "졸음쉼터" },
{ size: "LONG", actual: "ACTUAL LONG", entity: "drowsy-shelter", label: "서해안고속도로 대산방향 졸음쉼터" },
]
function RoadsideSpecimens() {
return (
<div className="sb-center-tunnel sb-center-roadside">
<div className="facility-reference roadside-reference" data-reference-facility="roadside">
<article className="facility-reference__sample">
<div className="facility-reference__stage sb-center-tunnel__width-stage">
<div className="sb-center-tunnel__width">
<div className="facility-reference__width-tests sb-center-tunnel__width-group sb-center-roadside__position">
<header><strong>Position Map</strong><span>위치도</span></header>
<div className="sb-center-roadside__blank" aria-hidden="true" />
{ROADSIDE_TESTS.map((test) => (
<div key={test.size} data-roadside-test={test.size.toLowerCase()}>
<span>{test.size}</span>
{/* The tag is itself a span: the wrapper keeps the row-label rule (`> div > span`) off the tag text. */}
<div><RoadsideFacilityTag entity={test.entity} label={test.label} /></div>
</div>
))}
</div>
<div className="facility-reference__width-tests sb-center-tunnel__width-group">
<header><strong>Actual / Detail</strong><span>실사화</span></header>
<div data-roadside-actual-test="separated">
<span>SEPARATED</span>
<RoadsideFacilityTag entity="drowsy-shelter" variant="actualDetail" separated />
</div>
{ROADSIDE_TESTS.map((test) => (
<div key={test.size} data-roadside-actual-test={test.size.toLowerCase()}>
<span>{test.actual}</span>
<RoadsideFacilityTag entity={test.entity} label={test.label} variant="actualDetail" />
</div>
))}
</div>
</div>
</div>
</article>
<TunnelSample title="연계 / 노선 맥락" note="노선 + 앵커 + 연결선 + 시설물 태그">
<CompactFacilityConnection className="facility-applied facility-applied--anchored roadside-applied" routeMode="anchored" sourceStatus="SOURCE_PENDING" label="졸음쉼터 연계 노선 간략 적용 예제">
<RoadsideFacilityTag entity="drowsy-shelter" />
</CompactFacilityConnection>
</TunnelSample>
</div>
</div>
)
}
// `name` is repeated as a literal because Storybook's static indexer cannot read it through page(); it drives the sidebar label.
export const Tunnel: Story = { ...page("터널", TunnelSpecimens), name: "터널" }
export const Bridge: Story = { ...page("교량", () => <FacilitySpecimens config={FACILITY_CONFIGS.bridge} compareAt="1210" />), name: "교량" }
export const Intersection: Story = { ...page("교차로", () => <FacilitySpecimens config={FACILITY_CONFIGS.intersection} />), name: "교차로" }
export const Junction: Story = { ...page("분기점", () => <FacilitySpecimens config={FACILITY_CONFIGS.junction} />), name: "분기점" }
export const Interchange: Story = { ...page("나들목", () => <FacilitySpecimens config={FACILITY_CONFIGS.interchange} />), name: "나들목" }
export const Roadside: Story = { ...page("휴게소 · 졸음쉼터", RoadsideSpecimens), name: "연계 노선" }
// Linked facilities are a standalone section heading followed by item cards.
// Keep the reference data/artwork and road-class grouping owned by the package.
// 2026-11-13: same `sb-page-indent` start as the other facility pages; the page class scopes the Linked-only CSS
// (hidden context notice / READY count / duplicate card name, card title and POSITION · ACTUAL label style).
export const Linked: Story = {
...page("연계 시설물", LinkedFacilityReferenceDemo),
name: "연계 시설물",
render: () => (
<StoryPage className="sb-center-linked-page">
<StoryPageHeader title="연계 시설물" />
<div className="sb-page-indent">
{withRepresentation(() => <LinkedFacilityReferenceDemo />)}
</div>
</StoryPage>
),
}
export const TunnelPlayground: Story = {
tags: ["verification"],
name: "직접 테스트 (터널)",
decorators: [withRepresentation],
args: { name: "대산2터널", secondaryText: "L=750m", variant: "positionMap", separated: false },
argTypes: { variant: { control: "inline-radio", options: ["positionMap", "actualDetail"] } },
}