npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iyulab/enterprise

v0.35.1

Published

Enterprise utilities and components for iyulab framework

Readme

@iyulab/enterprise

iyulab 프레임워크의 엔터프라이즈 통합 패키지. 폼 레이아웃 컴포넌트·API 설정·도메인 헬퍼를 제공합니다.

Installation

npm install @iyulab/enterprise @iyulab/components

@iyulab/components 는 필수 peer 다(로케일·포맷·디자인 토큰). React 폼 레이아웃(/react)을 쓰면 react 도 필요하다.

| 진입점 | 내용 | |--------|------| | @iyulab/enterprise | 아래 표의 고유 export | | @iyulab/enterprise/react | FormSection · FormRow | | @iyulab/enterprise/icons | house 아이콘 라이브러리 등록(부수효과 import) |

Quick Start

React 앱의 첫 페이지 — 폼 섹션 하나(Vite 기준, index.html 이 src/main.tsx 를 모듈로 싣는다).

npm install @iyulab/enterprise @iyulab/components @lit/react react react-dom
// src/main.tsx
import '@iyulab/components/styles/tokens.css';
import { createRoot } from 'react-dom/client';
import { UInput, UTextarea, UButton } from '@iyulab/components/react';
import { FormSection, FormRow } from '@iyulab/enterprise/react';
import { DateHelper } from '@iyulab/enterprise';

function OrderForm() {
  return (
    <form style={{ maxWidth: 640, margin: 32 }} onSubmit={(e) => e.preventDefault()}>
      <FormSection title="Order">
        <FormRow>
          <UInput name="customer" label="Customer" required />
          <UInput name="due" label="Due date" value={DateHelper.formatDate(new Date())} />
        </FormRow>
        <FormRow full>
          <UTextarea name="note" label="Note" />
        </FormRow>
      </FormSection>
      <UButton type="submit" color="primary">Save</UButton>
    </form>
  );
}

createRoot(document.body.appendChild(document.createElement('div'))).render(<OrderForm />);

데이터 서비스(createODataService)·인증(createAuthClient)·권한 store 는 아래 Usage 에 있습니다.

무엇을 제공하나

1. 고유 export (enterprise에서만 제공)

| Export | 종류 | 용도 | |--------|------|------| | FormSection | React (/react) | 제목 + 세로 스택 폼 섹션 | | FormRow | React (/react) | 2컬럼 그리드 폼 행(full로 1컬럼) | | u-list-page | 커스텀 엘리먼트 (/list-page) | 목록 화면 골격 — 머리·필터·툴바·뷰·페이저·빈/오류 슬롯, 소스를 뷰·페이저에 묶고 view 로 표↔카드 전환. 표는 그리지 않는다. 상세: skills/iyulab-enterprise/references/list.md | | ListPage | React (/react) | u-list-page 의 얇은 래퍼(@lit/react — 선택 peer) | | ApiConfig | class | baseUrl/OData·API prefix·dev 판별 중앙 설정 | | createODataService | factory | OData v4 + custom REST CRUD 서비스(401·토스트·에러파싱) | | ApiError | class | HTTP status + OData error.details(필드별 검증 상세)를 실은 API 호출 실패 에러 | | applyFieldErrors · clearFieldErrors | function | 서버 거절의 필드 상세(ApiError.details 의 target)를 폼 안 그 name 의 컨트롤에 — 칸 옆 오류 · 고치면 걷힘 · 못 내려간 항목은 폼 단위로 돌려준다 | | wasNotified | function | 이 실패를 서비스가 notify.error 로 이미 알렸는가 — 경계의 이중 토스트 방지 | | createAuthClient | factory | 쿠키 세션 인증(fetchMe/login/logout) — 제네릭 user/자격증명 | | createPermissionStore · hasPermission 외 | store | 권한 스냅샷 store + 판정 free 함수 | | bindSource · SourceBinding | function · Lit controller | 데이터 소스(@iyulab/flex-table 의 createODataSource·createArraySource)를 «뷰 어휘» 를 말하는 요소(표 · 카드 뷰 · 페이저 · 검색 칸)에 묶는다 — 상태를 속성에 쓰고 sort-change·page-change·search 를 소스 조작으로. 상세: skills/iyulab-enterprise/references/list.md | | DateHelper | class | 날짜 포맷·날짜 계산(로컬 달력 — 'YYYY-MM-DD' 는 그 날로 읽는다) | | ProgressHelper | class | 진행률 계산 | | UrgencyHelper | class | 긴급도 계산 |

2. 심볼 출처 (v0.3.0부터 하위 패키지 재노출 제거됨)

@iyulab/enterprise는 자기 고유 export만 제공합니다(위 표). @iyulab/components(컴포넌트), @iyulab/data-components(데이터 그리드/시트), @iyulab/modern-app(앱 프레임워크, 라우팅)은 각 패키지에서 직접 import하세요.

v0.2.x까지는 위 3개 패키지를 export *로 재노출했으나, 심볼 출처 혼란과 exact-pin(v0.2.2 이전) 조합 시 이중 인스턴스 위험 때문에 v0.3.0에서 제거했습니다. 재노출에 의존하던 코드는 @iyulab/enterprise가 아닌 각 하위 패키지에서 직접 import하도록 수정해야 합니다.

Usage

폼 레이아웃 (React)

React 컴포넌트는 @iyulab/enterprise/react 서브패스에 있습니다. react 는 선택 peer 의존이라, React 를 쓰지 않는 앱(프리셋·데이터 서비스·인증 클라이언트만 쓰는 앱)에는 설치되지 않습니다.

import { FormSection, FormRow } from '@iyulab/enterprise/react'
import { UInput, UTextarea } from '@iyulab/components/react'

<FormSection title="기본 정보">
  <FormRow>
    <UInput label="이름" />
    <UInput label="코드" />
  </FormRow>
  <FormRow full>
    <UTextarea label="비고" />
  </FormRow>
</FormSection>
  • FormRow는 기본 2컬럼 그리드입니다. 한 칸을 차지하려면 full을, 다른 열 수가 필요하면 columns를 씁니다.
  • 열 폭 — 선언한 열은 실제로 균등합니다. 트랙은 1fr이 아니라 minmax(0, 1fr)입니다. 그리드 아이템의 기본 min-width: auto는 내용의 min-content 아래로 줄어들기를 거부해서, 순수 1fr로 두면 긴 내용이 든 칸만 부풀고 나머지가 찌그러집니다(400px 컨테이너 실측: 2열 425/8, 3열 425/8/8). minmax(0, 1fr)은 트랙 최소를 0으로 만들어 같은 경우를 196/196·128/128/128로 만듭니다.
  • ⚠긴 불가분 문자열 자체의 넘침은 별개 축입니다. 칸이 균등해져도 그 안의 긴 문자열은 자기 칸을 넘습니다 — 셀 쪽에서 overflow-wrap으로 다룹니다(@iyulab/components의 엘리먼트는 이미 그 값을 갖습니다).
  • 두 컴포넌트 모두 className·style을 받아 기본값 뒤에 병합합니다 — 블록을 복제하지 않고 조정하는 정규 경로입니다. FormSection은 제목 줄만 바꾸는 titleStyle도 받습니다.
<FormRow columns={3}>…</FormRow>
<FormSection title="기본 정보" style={{ marginBottom: 32 }}>…</FormSection>

이 계층이 무엇을 소유하고 무엇을 소유하지 않는지, 새 패턴이 언제 추가되는지는 LOB 계층 헌장 이 정합니다. 패턴을 제안하기 전에 읽어 주세요.

API 설정

import { ApiConfig } from '@iyulab/enterprise'

ApiConfig.initialize({ baseUrl: '/', odataPrefix: '$data', apiPrefix: 'api' })

ApiConfig.getODataUrl('Orders')          // → /$data/Orders
ApiConfig.getApiUrl('auth/me')           // → /api/auth/me
ApiConfig.getUrlWithParams('report', { year: 2026, active: true })
ApiConfig.isDevelopment                   // 환경 판별

OData 서비스 (createODataService)

OData v4 + custom REST CRUD 를 한 번에 구성한다. 401 세션 처리·에러 메시지 추출·성공 토스트·204 빈 바디 안전 파싱이 내장돼 있고, 도메인/로케일 요소는 전부 주입으로 앱 adapter 에 남긴다.

import { createODataService } from '@iyulab/enterprise'
import { app } from '@iyulab/modern-app'

// 앱 adapter (예: src/lib/odata.ts) — 라이브러리는 이 배선만 받는다.
export const svc = createODataService({
  baseUrl: window.location.origin,
  onUnauthorized: () => { window.location.href = '/' },        // 세션 만료 리다이렉트는 앱이 결정
  notify: { success: (m) => app.success(m), error: (m) => app.error(m) },
  messages: {                                                   // 기본은 영어 — 로케일 오버라이드
    saved: '저장되었습니다', updated: '수정되었습니다', deleted: '삭제되었습니다',
    sessionExpired: '세션이 만료되었습니다. 다시 로그인하세요.',
  },
})

await svc.odataGet<Order>('Orders', { $top: '20' })   // value 배열 언랩

// 서버가 페이지를 자르면(@odata.nextLink) odataGet 은 끝까지 따라가 전량을 모은다.
// maxRows 를 주면 그 수를 넘는 순간 남은 페이지를 읽지 않고 RangeError 를 던진다(자르지 않는다).
await svc.odataGet<Order>('Orders', undefined, { maxRows: 5000 })
// 페이지 단위로 읽어야 하는 화면은 페이지 API 를 쓴다 — nextLink 는 조립하지 말고 그대로 넘긴다.
const page = await svc.odataGetPage<Order>('Orders', { $count: 'true' })  // { value, nextLink?, count? }
if (page.nextLink) await svc.odataGetNextPage<Order>(page.nextLink)
await svc.odataPost<Order>('Orders', { name: 'A', note: '' })  // '' → null 정규화 + 성공 토스트
svc.odataUrl('Orders')                                 // flex-table useODataSource 엔드포인트
svc.sourceDefaults                                     // { baseUrl, onUnauthorized } 주입용

// custom REST — GET/POST/PUT/PATCH/DELETE 전부, 204 빈 바디 안전 파싱 포함
await svc.apiGet<Order>('reports/summary')
await svc.apiPost<Order>('orders', { name: 'A' })
await svc.apiPut<Order>('orders/7', { name: 'A (revised)' })    // 리소스 전체 교체
await svc.apiPatch<Order>('orders/7', { note: 'urgent' })
await svc.apiDelete('orders/7')                                 // 204 안전

// 로그인 — 401 이 「세션 만료」가 아니라 「자격 증명 틀림」인 유일한 호출이다.
// 전역 onUnauthorized 를 이 호출에서만 끄면, 서버가 준 사유가 그대로 ApiError.message 로 온다.
await svc.apiPost('auth/login', creds, { onUnauthorized: false })

// 연결 프로브 — 「응답이 왔는가」만 묻는다. fetchRaw 는 던지지도, 401 훅을 부르지도 않는다.
const probe = await svc.fetchRaw(svc.apiUrl('ping'))
const reachable = true          // 여기 닿았다는 것 자체가 답이다(probe.status 는 볼 필요 없다)

// body가 FormData 인스턴스면 그대로(직렬화 없이) 멀티파트로 전송된다 —
// Content-Type은 브라우저가 boundary와 함께 자동 설정한다. apiPost/apiPut/apiPatch 전부 동일.
const form = new FormData()
form.append('file', file)
await svc.apiPost<Order>('orders/7/attachments', form)

주입 항목:

| config | 용도 | |--------|------| | baseUrl | 모든 요청의 오리진 (필수) | | odataPrefix / apiPrefix | 엔드포인트 prefix (기본 $data / api) | | onUnauthorized(status) | 401 시 호출 — 리다이렉트/재진입 가드는 앱이 처리 | | notify.success/error | 토스트 훅 (생략 시 토스트 없음 — 순수). 메서드마다 걸리는 방식이 다르다 — 바로 아래 표 참조 | | onMutated(mutation) | 쓰기가 성공(2xx)한 뒤 한 번 — 캐시 무효화 자리. 아래 절 참조 (0.19.0~) | | messages | 사용자 대면 문구 (기본 영어, 지정 키만 대체) | | formatError(info) | 에러 메시지 포매팅 오버라이드 (앱별 정책) — info 는 status/statusText/rawMessage/code(서버의 거절 코드)/details(검증된 error.details)/body 를 받는다 |

onMutated — 쓰기 뒤 무효화

목록·상세·대시보드가 같은 데이터를 따로 들고 있으면, 한쪽에서 저장한 뒤 다른 쪽이 낡는다 (예: 목록 위 오버레이에서 상세를 저장하고 닫으면 목록은 재마운트되지 않는다). 서비스가 쓰기를 전부 소유하므로 «무엇이 쓰였는가» 도 서비스가 알린다 — 쓰기 메서드를 한 겹씩 감쌀 필요가 없다.

export const svc = createODataService({
  baseUrl: window.location.origin,
  onMutated: ({ method, source, target, id }) => invalidate(source === 'odata' ? target : 'api'),
})
  • 성공한 쓰기마다 한 번 — odataPost·odataPatch·odataDelete, apiPost·apiPut·apiPatch·apiDelete 와 그 *Quiet 변형 전부(odataPut 은 없다). 토스트 여부와 무관하다.
  • 실패한 쓰기·읽기(fetchRaw 포함)에는 부르지 않는다.
  • mutation = { method, source: 'odata' | 'api', target, id? } — target 은 odata 면 엔티티 셋 이름, api 면 넘긴 경로 그대로. id 는 키로 지정한 쓰기(odataPatch·odataDelete)에만.
  • 콜백이 던져도 쓰기는 성공으로 끝난다(예외는 reportError 로 보고) — 관찰자의 실패가 «저장 실패» 로 보이면 안 된다.

notify 가 걸리는 자리 — 축은 «쓰기 ↔ 읽기» 다

| 메서드 | 실패 시 error | 성공 시 success | |---|---|---| | odataPost · odataPatch · odataDelete | ✅ (401 제외) | ✅ saved·updated·deleted | | apiPost · apiPut · apiPatch · apiDelete | ✅ (401 제외) | ❌ | | *Quiet 전부 (odataPostQuiet … apiDeleteQuiet) | ❌ | ❌ | | odataGet · odataGetPage · odataGetNextPage · odataGetById · odataCount · apiGet · fetchRaw | ❌ | ❌ |

  • 조회는 통지하지 않는다. 빈 화면 자체가 신호이고, 목록을 열 때마다 토스트가 뜨면 읽을 수 없다.
  • 쓰기는 통지한다. 결과가 화면에 안 보일 수 있기 때문이다 — 서버가 409 와 사유를 돌려줘도, 통지가 없으면 아무 일도 일어나지 않은 화면과 구별되지 않는다.
  • 401 은 통지하지 않는다 — onUnauthorized 가 이미 안내하므로 겹친다.
  • api* 에는 성공 토스트가 없다. 임의의 RPC(상태 전이·발행·업로드)를 태우는 경로라 «저장되었습니다» 같은 문구를 라이브러리가 지어낼 수 없다. 실패 메시지는 서버가 주므로 어느 엔드포인트에서나 뜻이 통하지만, 성공 문구는 그렇지 않다 — 필요하면 호출한 쪽이 띄운다.
  • 이미 자기 래퍼로 통지하고 있다면 *Quiet 로 바꾼다. 이중 토스트를 막는 탈출구이고, odata*Quiet 와 같은 관용구다(한 사용자 액션이 여러 요청을 낼 때도 같은 것을 쓴다).

401 축 — 전역 정책과 그 호출 단위 예외

onUnauthorized 는 서비스 전역 정책이다: 401 이 오면 훅을 부르고 messages.sessionExpired 로 단락한다. 이 기본값은 거의 항상 옳지만 두 자리에서 틀린다.

| 자리 | 왜 틀리는가 | 해법 | |---|---|---| | 로그인 자체 | 401 이 「세션이 끊겼다」가 아니라 「자격 증명이 틀렸다」를 뜻한다. 전역 훅이 발화하면 로그인 화면에서 로그인 화면으로 리다이렉트되고, 던져지는 메시지가 sessionExpired 라 화면이 실패 사유를 지어내야 한다 | { onUnauthorized: false } | | 연결 프로브 | 「응답이 왔는가」 만 묻는 호출이라, 401 은 서버가 살아서 거절한 것 = 「닿았다」다. 훅+예외를 타면 로그인하지 않은 사용자에게 늘 「서버에 연결할 수 없습니다」가 뜬다 | fetchRaw |

// 401 을 «그 호출의 결과» 로 받는다 — 훅 없음, 메시지 덮어쓰기 없음.
await svc.apiPost('auth/login', creds, { onUnauthorized: false })
  • fetchRaw 를 뺀 모든 요청 메서드가 이 옵션을 받는다(마지막 선택 인자 — fetchRaw 는 애초에 401 훅을 태우지 않는다). 부분집합으로 두면 다음 소비자가 다른 메서드에서 같은 벽을 만난다.
  • 끄는 축만 있다. 새 동작을 켜는 스위치가 아니라 전역 정책의 탈출구이므로, 옵션을 생략한 호출은 이 옵션이 생기기 전과 한 글자도 다르게 동작하지 않는다.
  • authenticate() 같은 이름 있는 프리미티브는 두지 않는다 — 그것은 엔드포인트 경로·바디 모양·토큰 처리 규약을 라이브러리가 안다고 가정하는 도메인 이름이다. 그 규약은 앱마다 다르므로 adapter 에 남기고, 라이브러리는 범용 축만 연다.

fetchRaw — 「raw」 는 정책을 태우지 않는다는 뜻이다

응답을 그대로 돌려준다. 비-2xx 에도 던지지 않고 onUnauthorized 도 부르지 않는다 — 상태 판단은 호출부의 몫이다. 상태에 따라 예외·토스트·세션 처리를 원하면 apiGet 을 쓴다.

⚠0.15.0 이전에는 이 문장이 선언에 적혀 있으면서 실제로는 비-2xx 에 던졌다. 선언이 옳고 구현이 틀렸던 자리라 구현을 고쳤다. fetchRaw 의 throw 에 기대고 있었다면 호출부에서 if (!res.ok) throw … 로 바꾸거나 apiGet 으로 옮긴다.

도메인 액션(상태 전이 등)·엔티티 목록·권한 코드는 라이브러리에 넣지 말고 앱 adapter 에 둔다.

실패 응답 — ApiError

모든 메서드는 실패 시 ApiError(Error 상속)를 던진다. status 로 상태별 분기하고, 서버가 OData v4 오류 봉투에 필드별 검증 상세(error.details)를 실어 보내면 details 로 읽어 폼의 필드별 오류 표시에 바로 연결할 수 있다.

import { applyFieldErrors } from '@iyulab/enterprise'

try {
  await svc.odataPost('Roles', draft)
} catch (e) {
  // [{ code: 'ValidationFailed', message: 'The Name field is required.', target: 'Name' }, …]
  const { applied, formLevel } = applyFieldErrors(form, e)
  // applied: 칸에 내려간 항목(control · message) — 요약에 칸으로 가는 링크로
  // formLevel: target 이 없거나 그 이름의 칸이 없는 항목 — 요약에 링크 없는 줄로
}

applyFieldErrors(root, error, { message? }) 는 target 과 같은 name 의 컨트롤(네이티브 폼 요소 · @iyulab/components 폼 컨트롤)에 setCustomValidity(message) 를 하고, components 컨트롤이면 validate() 로 오류를 보인다. 사용자가 그 칸을 고치면(input·change) 그 메시지를 걷고, 다시 부르면 앞의 것을 먼저 걷는다 (clearFieldErrors(root) 로도 걷는다). message 로 문장을 바꾼다 — detail.code 로 지역화할 때(예: 값을 그 타입으로 읽지 못한 Unconvertible 만 앱의 언어로). ApiError 가 아닌 실패는 빈 결과다.

| 필드 | 설명 | |------|------| | message | 사용자 대면 메시지 (formatError → 서버 raw → status 폴백 순으로 결정) | | status | HTTP status | | code | 서버가 정한 거절 코드 — OData error.code(봉투가 없으면 최상위 code → Code). 같은 상태 안의 실패를 가른다(예: 403 중 «비밀번호 변경 필요»). 없거나 빈 문자열이면 undefined | | details | error.details 항목 배열 — 규격상 code/message 는 필수, target(속성 이름)은 선택. 상세가 없거나 규격 형태가 아니면 undefined | | body | 실패 응답의 본문 — JSON 이면 파싱한 값, 아니면 텍스트, 비었으면 undefined. 거절이 싣는 «충돌의 현재 상태»(409 의 새 판정, 412 의 현재 표현)를 같은 요청을 다시 보내지 않고 읽는다. 모양은 서버 계약이라 unknown — 호출부가 좁힌다. formatError 가 받는 body 와 같은 값. 표준 cause 처럼 열거되지 않는다(직렬화·로그에 실리지 않음) | | cause | 표준 Error.cause — 응답 없이 실패했을 때(인증 클라이언트의 네트워크 오류, status 0) 전송이 던진 예외. 진단용 | | notified | 서비스가 이 실패를 notify.error 로 이미 사용자에게 알렸는가(읽기 전용). 쓰기 실패를 알렸으면 true, 읽기·*Quiet·401 이나 notify.error 미설정이면 false |

경계에서 «알리지 않은 것만» 알리기 — notified · wasNotified

쓰기 실패는 서비스가 알린 뒤 같은 에러를 다시 던진다(호출부 흐름을 멈추게 하려고). 그 에러가 전역 unhandledrejection 핸들러나 error boundary 까지 올라오면, 거기서 다시 토스트를 띄우면 이중 통지가 되고 안 띄우면 읽기 실패가 조용해진다. 어느 쪽인지는 에러가 들고 온다:

import { ApiError, wasNotified } from '@iyulab/enterprise'

window.addEventListener('unhandledrejection', (ev) => {
  const err = ev.reason
  if (err instanceof ApiError || wasNotified(err)) ev.preventDefault()
  if (err instanceof ApiError && !err.notified) app.error(err.message)   // 읽기 실패 등
})

wasNotified(err) 는 ApiError 가 아닌 실패(네트워크 오류 등)에도 같은 답을 준다 — 쓰기 경로에서 알렸다면 true. 표식은 서비스만 세울 수 있다(에러 객체의 필드가 아니다).

OData v4 적합성 (conformance)

서비스가 OData v4 의 어느 조항을 따르는지 — «구현» 행은 전부 tests/odata-conformance.test.ts 가 명세의 예시 응답 모양으로 잰다. 서버가 새 조항을 쓰기 시작하는 날(서버 주도 페이징 등) 소비자가 먼저 깨지지 않게 하려는 표다.

| 조항 | 상태 | 메모 | |---|---|---| | 서버 주도 페이징 — @odata.nextLink(상대·절대) | 구현 | odataGet 은 끝까지 따라가고, odataGetPage 는 nextLink 를 준다. 서비스 오리진 밖 · 이미 읽은 링크로의 순환은 던진다 | | nextLink 를 불투명하게 따라가기(Protocol §11.2.6.7) | 구현 | 서버가 쓴 쿼리가 바이트 그대로 나간다(@iyulab/http-client 0.10.3 이상 — 범위 ^0.10.0 이면 재설치로 받는다) | | $count=true → @odata.count · odataCount | 구현 | odataCount 는 $top=0&$count=true 로 행 없이 건수만 | | 오류 봉투 — error.code·message·details[](code·message·target) | 구현 | ApiError.message·ApiError.code·ApiError.details · innererror 는 해석하지 않는다(서버의 디버깅용이지 계약이 아니다) — 원문 body 에만 있고, body 는 열거되지 않아 에러를 직렬화해도 따라 나가지 않는다 | | 생성 201 · 수정/삭제 204 No Content | 구현 | 빈 바디를 파싱하지 않는다 | | 키 리터럴(URL Conventions §4.3.1) | 호출자 | id 는 URL 에 쓰이는 리터럴 그대로다 — GUID·숫자는 그대로, 문자열 키는 호출자가 '…' 로 감싸고 ' 를 '' 로. 클라이언트는 메타데이터 없이 둘을 가를 수 없다 | | 낙관적 동시성 — ETag · If-Match · 412 | 미구현 | 서버 지원 확인 뒤에 연다 | | Prefer(return=… · odata.maxpagesize) · $batch · @odata.deltaLink | 미구현 | | | 429 · Retry-After | 미구현 | 다른 오류와 같이 ApiError 로 던진다(재시도하지 않는다) |

인증 + 권한 (createAuthClient · 권한 store)

쿠키 세션 인증 흐름과 권한 스냅샷을 승격. 사용자·자격증명 형태는 앱이 제네릭으로 정의하고, 권한 코드는 불투명 문자열로만 다룬다. getPermissions 를 주면 로그인/세션 조회 성공 시 권한 store 가 자동 갱신된다.

import { createAuthClient, hasPermission, setPermissions } from '@iyulab/enterprise'

interface User { Id: string; Permissions: string[] }   // 도메인 형태 = 앱 소유

export const auth = createAuthClient<User, { Username: string; Password: string }>({
  meUrl: '/api/auth/me', loginUrl: '/api/auth/login', logoutUrl: '/api/auth/logout',
  getPermissions: (u) => u.Permissions,                 // 성공 시 권한 store 자동 set
  messages: { invalidCredentials: '사용자명 또는 비밀번호가 올바르지 않습니다.' },
})

// 부팅 게이트 — 답은 셋이다
const session = await auth.fetchMe()
if (session.status === 'anonymous') showLogin()                    // 401 — 로그인 화면
else if (session.status === 'unknown') showOffline(session.error)  // 답을 못 받음 — 로그인으로 보내지 않는다

// @iyulab/modern-app 의 부팅 게이트는 이 답을 그대로 받는다
await app.load({ auth: { me: () => auth.fetchMe(), renderLogin, renderUnavailable }, /* … */ })

// 로그아웃도 결과를 본다 — 서버가 세션을 끊지 못했으면(2xx·401 이 아닌 답 · 응답 없음) 쿠키가 살아 있다
const out = await auth.logout()
if (out.ok) showLogin()
else showError(out.message)                                        // 화면을 지키고 다시 시도하게 한다

// 어디서나 권한 판정(부팅 스냅샷)
if (hasPermission('orders.write')) { /* 저장 버튼 노출 */ }

주입 항목(위 예시가 쓴 것 외 나머지):

| config | 용도 | |--------|------| | baseUrl | 상대 URL 앞에 붙일 오리진 (기본 '' = same-origin) | | credentials | fetch credentials 모드 (기본 'same-origin' — 쿠키 세션) | | extractLoginError | 로그인 실패(non-401) 응답 바디에서 사용자 대면 메시지 추출 오버라이드 (기본: 봉투 error.message → message → Message) | | permissionStore | 권한 자동 갱신 대상 store (기본 defaultPermissionStore) — 격리가 필요하면 createPermissionStore()로 별도 store를 만들어 주입 | | loginUser | 로그인 성공 뒤 세션 사용자를 어디서 얻는가 — 'body'(기본: 로그인 응답 본문) · 'me'(meUrl 재조회 — 로그인 응답이 { Success, User, Token } 같은 봉투인 서버). LoginResult 성공에는 응답 원문 body 도 실린다 |

  • fetchMe() 는 던지지 않고 { status: 'authenticated', user } · { status: 'anonymous' }(401) · { status: 'unknown', error }(그 밖의 비-2xx · 네트워크 · 읽을 수 없는 본문) 중 하나를 준다. 로그인 게이트는 anonymous 에서만 연다 — unknown 을 미인증으로 읽으면 서버가 잠깐 503 을 낸 순간 모두가 로그인 화면으로 간다. 라이브러리는 리다이렉트하지 않는다.
  • login() 실패는 { ok: false, message, error } — error 는 ApiError(status · code · details, 응답이 없으면 status: 0)라 429 · 403 거절 코드 같은 갈래로 화면을 나눌 수 있다.
  • 권한 store 는 «모름» 과 «없음» 을 가른다(isKnown() · permissionsKnown()). unknown 세션은 마지막으로 알던 권한을 건드리지 않는다.
  • 도메인 판정(isPortalUser 등)·권한 코드 상수는 라이브러리가 아니라 앱 adapter 에 둔다.

도메인 헬퍼

import { DateHelper } from '@iyulab/enterprise'
import { formatCurrency } from '@iyulab/components/dist/utilities/format.js'   // 통화·숫자 포맷은 components 의 것(배럴은 컴포넌트를 전부 등록한다)

DateHelper.formatDate('2026-10-09')   // 그 날(로컬 달력)로 읽는다
formatCurrency(1234000, 'KRW')        // ₩1,234,000

Development

npm run build

License

MIT