ShopNex
ShopNex는 관리·결제·스토어 API·웹훅·앱 마켓을 갖춘 즉시 사용 가능한 자체 호스팅 상거래 플랫폼으로, 개발자 없이도 운영 가능합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Open source seat reservation library for your applications
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗An open-source, framework-agnostic JavaScript/TypeScript suite for interactive floorplan management and seat reservations:
@open-seats/editor): A feature-rich visual designer for authoring and shaping buildings, non-seating zones, tables/rows, and seats with live layout invariant enforcement, vertex manipulation, grid snapping, and undo/redo history.@open-seats/viewer): A high-performance canvas rendering engine for seat reservations with instant selection handling, custom status registry, async debounced tooltips, touch pinch-to-zoom/pan, and full screen-reader ARIA live announcements and 2D spatial keyboard navigation.@open-seats/angular): First-class standalone components (<seats-viewer> and <seats-editor>) for modern Angular applications.📖 Documentation: guides and a type-checked example for every feature live in docs/. Run them locally with npm run docs:dev.
The workspace is organized into modular packages using Nx:
libs/
core/ @open-seats/core Schema (Zod), 2D geometry, layout invariants, spatial index, typed event emitter
renderer/ @open-seats/renderer Konva scene builder, hit-testing, zoom/pan (hidden behind Renderer interface)
viewer/ @open-seats/viewer Runtime seat selection, status registry, async tooltip provider, ARIA keyboard flow
editor/ @open-seats/editor Tools (building, zone, row, seat), undo/redo command stack, vertex editing, preview mode
angular/ @open-seats/angular Angular standalone components: <seats-viewer> and <seats-editor>
apps/
demo-vanilla/ Interactive Vanilla TS demonstration (theme switcher, live event feed, reservations)
demo-angular/ Interactive Angular application (ng-demo) demonstrating <seats-viewer> and <seats-editor>
Install only the packages required by your application:
npm install @open-seats/viewer @open-seats/editor
npm install @open-seats/angular @open-seats/viewer @open-seats/editor
(Note: @open-seats/core and @open-seats/renderer are installed automatically as dependencies).
| Term | Meaning |
|---|---|
| Floorplan | Root document JSON containing dimensions, background image configuration, and buildings. |
| Building | A free-form polygon container for zones and rows. Moving a building translates its children. |
| Zone | A non-seating free-form polygon inside a building (stage, bar, dance floor, aisle, sound booth). |
| Row | A table or seating line with seats on one or both opposing sides (tableWidth: 0 for bare seat lines). |
| Seat | An individual seat belonging to a row with a stable ID, label, and coordinates. |
import { createViewer } from '@open-seats/viewer';
import auditoriumPlan from './samples/grand-auditorium.json';
const container = document.getElementById('seat-map')!;
const viewer = createViewer(container, {
floorplan: auditoriumPlan,
statuses: {
's-a-1a': 'reserved',
's-a-1b': 'locked',
},
maxSelectable: 4,
tooltipProvider: async (ctx) => ({
title: `Seat ${ctx.seat.label}`,
subtitle: `Row ${ctx.row.label} • ${ctx.building.label}`,
content: 'Standard Admission - $45.00',
}),
});
// Listen to selection changes
viewer.on('selectionChanged', ({ selectedSeatIds, selectedSeats }) => {
console.log('Selected seat IDs:', selectedSeatIds);
});
// Listen to individual seat selection
viewer.on('seatSelected', (ctx) => {
console.log('Seat selected:', ctx.seat.id);
});
import { createEditor } from '@open-seats/editor';
const container = document.getElementById('editor-map')!;
const editor = createEditor(container, {
gridSnapping: { enabled: true, size: 20 },
});
// Create a building polygon
const building = editor.createBuilding(
[
{ x: 0, y: 0 },
{ x: 800, y: 0 },
{ x: 800, y: 600 },
{ x: 0, y: 600 },
],
'Main Auditorium'
);
// Add a row of seats to the building
const row = editor.createRow(building.id, { x: 100, y: 150 }, 8, {
side: 'both',
seatSpacing: 42,
seatRadius: 14,
tableWidth: 28,
});
// Toggle preview mode (switches into interactive reservation viewer)
editor.togglePreview();
// Export valid floorplan JSON
const floorplanJson = editor.getFloorplan();
Import the standalone components in your Angular module or component:
import { Component } from '@angular/core';
import { SeatsViewerComponent, SeatsEditorComponent } from '@open-seats/angular';
import { SAMPLE_FLOORPLAN, type Floorplan } from '@open-seats/core';
@Component({
selector: 'app-booking',
standalone: true,
imports: [SeatsViewerComponent, SeatsEditorComponent],
template: ` <seats-viewer [floorplan]="floorplan" [statuses]="seatStatuses" [maxSelectable]="4" (selectionChanged)="onSelectionChanged($event)" (seatSelected)="onSeatSelected($event)" /> `,
styles: [
`
:host {
display: block;
width: 100vw;
height: 100vh;
}
`,
],
})
export class BookingComponent {
floorplan: Floorplan = SAMPLE_FLOORPLAN;
seatStatuses: Record<string, string> = {
's-a-1a': 'reserved',
's-a-1b': 'locked',
};
onSelectionChanged(event: { selectedSeatIds: string[] }) {
console.log('Current selection:', event.selectedSeatIds);
}
onSeatSelected(ctx: any) {
console.log('Selected:', ctx.seat.label);
}
}
select, pan, building (draw polygon), zone (draw non-seating area), row (place table/row), add-seat.both), customizable seat spacing, seat radius, table width, label numbering.Ctrl+Z) and Redo (Ctrl+Shift+Z / Ctrl+Y) tracking all mutations.maxSelectable.available, reserved, locked, vip) with custom colors and selectable flags.status and its runtime data, and is called again when either changes.setSeatData / seatData (prices, tickets, holder details), and receive shared data (such as live prices) from the server with live updates. Both live only in the viewer and are never written into the floorplan; getSeatData merges them, with the app's own fields taking precedence.A viewer shows one floorplan and receives status updates for the whole floorplan over Socket.IO. The app creates the socket (URL, auth, path), so the library has no Socket.IO dependency and never handles credentials:
import { io } from 'socket.io-client'; // v4
import { createViewer, socketIoStatusSource } from '@open-seats/viewer';
const socket = io('https://api.example.com', {
auth: { floorplanId: 'grand-hall', token },
});
const viewer = createViewer(container, {
floorplan,
statusSource: socketIoStatusSource(socket),
});
viewer.on('connectionStateChanged', ({ state, error }) => {
// 'connecting' | 'live' | 'offline', or null once the source is removed
});
viewer.on('selectionInvalidated', ({ seats }) => {
// These seats were just taken and removed from the selection.
});
syncTimeoutMs, 10 s by default), the state goes to offline with the error, and patches are ignored until the next reconnect syncs successfully: socketIoStatusSource(socket, { syncTimeoutMs: 5000 }).statusSource passed to createViewer is subscribed right after the constructor returns, so listeners you attach straight after creation receive its first events (such as connecting).connectionStateChanged fires when the state changes, when the error changes while the state stays the same (e.g. a new connect_error), and with state: null when setStatusSource(null) removes the source.selectable are deselected automatically and announced to screen readers. This also applies to setStatus / setStatuses.{ price: 120 }) with snapshots and patches; read it with getSeatData(seatId) or in the tooltip provider's data. Everyone viewing the floorplan receives it, so per-user data such as a ticket should be set locally with setSeatData, which live updates never overwrite.StatusSource (subscribe(sink) => unsubscribe), or call applyStatusSnapshot / applyStatusPatch directly.Reference servers for Node.js and Python, and the wire protocol, are in servers/.
The viewer strictly implements ARIA accessibility standards:
role="application", aria-label="Interactive Floorplan Seat Selection", and tabindex="0".aria-live="polite" region automatically announces seat focus, row details, status, and selection state to screen readers.ArrowUp, ArrowDown, ArrowLeft, or ArrowRight to jump focus to the spatially nearest seat in that direction.+ / -: Zoom in and zoom out.0: Reset zoom level.| Shortcut | Action |
|---|---|
Ctrl+Z / Cmd+Z | Undo |
Ctrl+Shift+Z / Ctrl+Y | Redo |
Delete / Backspace | Delete selected building, zone, row, or seat |
Escape | Clear current selection |
Strict layout invariants are enforced in the editor via canPlace() and validated via validateLayout():
seatSpacing >= 2 * seatRadius).Touching edges within configured tolerance (default 0.01) are permitted.
ViewerOptions| Option | Type | Description |
|---|---|---|
floorplan | Floorplan | The floorplan JSON object to render. |
statuses | Record<string, string> | Map of seat IDs to status names (e.g. 'reserved', 'booked'). |
statusDefinitions | SeatStatusDefinition[] | Custom status configurations (colors, selectability). |
maxSelectable | number | Maximum number of seats selectable simultaneously. |
selectedSeatIds | string[] | Initial pre-selected seat IDs. |
tooltipProvider | TooltipProvider | Async function returning tooltip title, subtitle, or content. |
tooltipRenderer | TooltipRenderer | Renders custom tooltip content into an unstyled host element: (ctx, host) => cleanup. Takes precedence over tooltipProvider. |
tooltipDebounceMs | number | Debounce delay in ms before showing tooltip (default: 150). |
enableKeyboardNavigation | boolean | Enable spatial 2D arrow navigation and accessibility (default: true). |
statusSource | StatusSource | Live status updates, e.g. socketIoStatusSource(socket). Subscribed right after creation. |
seatData | Record<string, SeatData> |
ViewerEventsready: Viewer initialized and ready.seatHovered / seatHoverEnded: Triggered when pointer moves over/off a seat.seatSelected / seatDeselected: Triggered when an individual seat is clicked or selected via keyboard.selectionChanged: Emitted whenever the selection changes, providing selectedSeatIds and selectedSeats.floorplanLoaded: Emitted when a new floorplan JSON is loaded.violationsUpdated: Emitted when layout violations are detected.statusesChanged: Emitted with { changes, replaced } whenever seat statuses change.seatDataChanged: Emitted with { changes, replaced, source } when seat data changes, from setSeatData (source: 'app') or a live update (source: 'live').selectionInvalidated: Emitted with { seatIds, seats, reason } when selected seats became unavailable and were deselected.connectionStateChanged: Emitted with { state, error? } when the live status source connects, reconnects or goes offline, when its error changes, and with state: null when the source is removed.EditorOptions| Option | Type | Description |
|---|---|---|
floorplan | Floorplan | Initial floorplan data to load. |
rules | { enforce: boolean; tolerance: number } | Layout invariant enforcement settings. |
gridSnapping | { enabled: boolean; size: number } | Grid snapping toggle and grid spacing in pixels. |
EditorTools'select': Select, move, and edit elements or vertices.'pan': Pan the canvas without selecting elements.'building': Click to add vertices and create building polygons.'zone': Click to draw non-seating zones inside buildings.'row': Click to place seating rows/tables.'add-seat': Append seats to existing rows.EditorEventschange: Emitted with the updated Floorplan after any modification.toolChange: Emitted when active tool changes.selectionChange: Emitted with an array of selected element IDs.previewToggle: Emitted when preview mode is entered or exited.violationsUpdated: Emitted with current layout violations.<seats-viewer>[floorplan]: Floorplan[statuses]: Record<string, string>[maxSelectable]: number[options]: Partial<ViewerOptions>[statusSource]: StatusSource | null. Takes precedence over options.statusSource. Once this input has been given a value (including null to disconnect), options.statusSource is never used, so a custom source is never swapped for another one. Changes to options.statusSource apply only while this input is unused.[seatData]: Record<string, SeatData>. Replaces the previous seat data on change; never saved in the floorplan.[tooltipComponent]: Type<unknown>. Component rendered as the seat tooltip (see below).(ready)(seatHovered)(seatHoverEnded)(seatSelected)(seatDeselected)(selectionChanged)(floorplanLoaded)(violationsUpdated)(statusesChanged)(seatDataChanged)(selectionInvalidated)(connectionStateChanged)statusesChanged, seatDeselected, selectionInvalidated, selectionChanged) are emitted together in one NgZone.run, so each update costs one change-detection pass.Put an <ng-template seatTooltip> inside <seats-viewer> to render any Angular template or component as the seat tooltip. The viewer handles the hover delay, positioning and showing/hiding, and re-renders the template when the hovered seat's status or data changes. The tooltip box is unstyled, so the template brings its own look.
<seats-viewer [floorplan]="plan" [statusSource]="source">
<ng-template seatTooltip let-seat="seat" let-status="status" let-data="data">
<my-seat-card [label]="seat.label" [status]="status" [price]="data?.price" />
</ng-template>
</seats-viewer>
Template variables: seat, row, building, status, data, and the whole TooltipContext as let-tooltip ($implicit). Alternatively pass a component class with [tooltipComponent]="MyTooltip"; it receives the context through a context = input.required<TooltipContext>() input. Both take precedence over options.tooltipProvider, and are read once, when the viewer is created. Outside Angular, the same hook is the tooltipRenderer(ctx, host) viewer option.
<seats-editor>[floorplan]: Floorplan[tool]: ToolType (default 'select')[options]: Partial<EditorOptions>(change): Emits updated Floorplan(toolChange): Emits active ToolType(selectionChange): Emits selected element IDs(previewToggle): Emits preview boolean flag(violationsUpdated): Emits layout violationsnpm start
# or
npx nx serve ng-demo
Access the application at http://localhost:4200.
npx nx serve demo-vanilla
Access the application at http://localhost:5173.
npm run docs:dev # VitePress dev server with hot reload
npm run docs:build # type-check the examples, then build to docs/.vitepress/dist
Every code example on the site is a real file under docs/snippets/ that npm run check type-checks and lints against the current library source, so examples can't silently go out of date.
npx nx run-many -t test
npx nx run-many -t build
npx nx run-many -t lint
Packages are published to npm from CI when a version tag is pushed. See RELEASING.md.
This project is licensed under the GNU General Public License v3.0 (GPLv3).
| Runtime data per seat (price, ticket, ...), passed to the tooltip provider. Never saved in the floorplan. |