Lingara Lingara 문서 학습 가이드 API 라이브러리 앱 만들기 임베드 웹 앱
언어: 한국어

웹사이트 위젯

이 페이지는 영어에서 번역되었습니다. 두 내용이 다를 경우 영어 페이지가 정확합니다. 영어 페이지 읽기

웹사이트에서 Lingara는 사용자 정의 요소 <lingara-embed>로 실행됩니다. 이 요소는 Lingara가 제공하는 프레임 안에 Lingara의 연습을 그리므로, 여러분의 페이지는 학습자의 데이터를 보지 못하고 Lingara의 페이지도 여러분의 데이터를 보지 못합니다.

요소 마운트하기

npm에서 @lingara/embed를 설치하거나, 사이트에 번들러가 없다면 CDN에서 모듈 빌드를 불러옵니다. 그런 다음 페이지에 <lingara-embed>를 넣고 정의하세요. 이 요소는 다음을 받습니다.

  • plan-id: 연습할 수업 계획으로, 플레이어가 소유한 것
  • surface: 보여 줄 것. practice가 기본값이자 유일한 화면입니다
  • theme: light, dark, auto 중 하나
  • locale: 프레임 자체 문구의 언어로, 지정하지 않으면 학습자를 따릅니다
  • title: 프레임의 접근 가능한 이름

스타일 지정은 테마까지입니다. 여러분의 페이지는 프레임 안에 접근할 수 없으며, 프레임이 제공하는 설정은 테마 하나뿐입니다.

토큰 제공하기

요소의 tokenProvider에 여러분 자체의 서버에 토큰을 요청하는 함수를 설정하세요. 서버는 서버 키트를 사용해 페이지의 오리진으로 토큰을 발급하고, 토큰과 그 만료 시각으로 응답합니다. 요소는 토큰을 메시지로 프레임에 넘기며, 토큰이 URL에 담겨 전달되는 일은 없습니다.

위젯이 실행되는 모든 오리진을 그 오리진용 토큰을 발급하기 전에 콘솔의 클라이언트 설정에 등록하세요. 방법은 서버 키트 페이지에서 설명합니다. API는 다른 오리진용으로 발급된 토큰을 거부하며, 프레임은 연습 대신 오류를 표시합니다.

서버 키트는 요소와 토큰 제공자를 함께 담은 스니펫 전체를 대신 작성해 줄 수도 있으며, 콘텐츠 보안 정책에 필요하다면 페이지의 스크립트 nonce도 포함합니다. 마지막 샘플에서 이를 보여 줍니다.

프레임이 전하는 내용 수신하기

프레임은 버전이 지정된 메시지 프로토콜로 요소와 통신하며, 요소는 페이지에 필요한 것을 DOM 이벤트로 바꿉니다.

  • lingara:practice-completed: 학습자가 세트를 마쳤을 때로, planId, setNumber, correct, total을 담습니다
  • lingara:error: 프레임이 멈췄을 때로, code와 retryable을 담습니다. 재시도 가능한 오류는 새로 마운트하면 성공할 수도 있다는 뜻이며, 언제 다시 시도할지는 여러분이 정합니다.

연습 결과는 연습 결과에서 설명하는 대로 여러분의 서버에도 도착합니다.

npm install @lingara/embed@0.1.0-alpha.6
# No bundler? One module script instead:
# <script type="module" src="https://cdn.jsdelivr.net/npm/@lingara/embed@0.1.0-alpha.6/dist/lingara-embed.min.js"></script>
<!-- plan-id is a lesson plan your server made for this player. -->
<lingara-embed surface="practice" plan-id="0b9d4f2e-3c1a-4e8b-9f6d-2a7c5e1b8d40" theme="auto" title="Practice"></lingara-embed>
<script type="module">
  import { defineLingaraEmbed } from '@lingara/embed'
  defineLingaraEmbed()
</script>
// Your own endpoint, on your own server. It mints with this page's origin and
// returns exactly { token, expires_at }: the token (lgr_et_…) never touches a URL.
widget.tokenProvider = async () => {
  const response = await fetch('/lingara/token', { method: 'POST', credentials: 'same-origin' })
  if (!response.ok) throw new Error(`token endpoint answered ${response.status}`)
  return response.json()
}
widget.addEventListener('lingara:practice-completed', (event) => {
  const { planId, setNumber, correct, total } = (event as CustomEvent).detail
  console.log(`set ${setNumber} of ${planId}: ${correct}/${total}`)
})
widget.addEventListener('lingara:error', (event) => {
  const { code, retryable } = (event as CustomEvent).detail
  // `retryable` means a remount may succeed; when to try is yours to decide.
  if (retryable) setTimeout(() => widget.reload(), 30_000)
  else console.warn('Lingara practice stopped:', code)
})
코드 예제 언어

TypeScript

// The page that mounts the widget: the token never appears in it.
export function practicePage(planId: string, cspNonce: string): string {
  const widget = widgetSnippet({ planId, tokenEndpoint: '/lingara/token', theme: 'auto', locale: 'fr', title: 'Pratique', nonce: cspNonce })
  return `<!doctype html><title>Practice</title>${widget}`
}

Rust

// The element, its script build and a token provider that POSTs to your
// token route. The token never appears in the markup.
let html = widget_snippet(&SnippetOptions {
    locale: Some("fr".into()),
    nonce: Some(csp_nonce.into()), // your page's CSP nonce, for a strict script-src
    ..SnippetOptions::new(plan_id, "/lingara/token")
})?;

Go

// The plan your server made for this player, and the token route above.
// Pass the request's CSP nonce when your script-src allows no inline
// script; the token itself never appears in the markup.
theme := "auto"
html, err := lingaraembed.WidgetSnippet(lingaraembed.SnippetOptions{
	PlanID:        planID,
	TokenEndpoint: "/lingara/token",
	Theme:         &theme,
	Nonce:         &cspNonce,
})
if err != nil {
	return "", err // a *lingaraembed.SnippetError names the refused option
}
return template.HTML(html), nil // already escaped by the kit

Java

// planId is the lesson plan your server made for this player. The element
// POSTs to tokenEndpoint with same-origin cookies: the token never appears
// in the markup. Pass this request's CSP nonce if your page sets one.
String html =
    WidgetSnippet.render(
        SnippetOptions.builder(planId, "/lingara/token").locale("fr").nonce(cspNonce).build());

Kotlin

// planId is the lesson plan your server made for this player. The element
// POSTs to tokenEndpoint with same-origin cookies: the token never appears
// in the markup. Pass this request's CSP nonce if your page sets one.
val html =
    widgetSnippet(
        SnippetOptions(planId, tokenEndpoint = "/lingara/token").apply {
            locale = "fr"
            nonce = cspNonce
        },
    )

Ruby

# The plan your server made for this player, rendered into your page.
# nonce: your page's CSP nonce, when its script-src allows no inline script.
def self.widget(plan_id, csp_nonce)
  Lingara::Embed.widget_snippet(plan_id: plan_id, token_endpoint: "/lingara/token", locale: "fr", nonce: csp_nonce)
end

PHP

use Lingara\Embed\SnippetOptions;
use Lingara\Embed\WidgetSnippet;

// In your page's template: the plan your server made for this player, and
// your token route. Pass the request's CSP nonce if your script-src allows
// no inline script. A bad option throws SnippetException naming its field.
function practiceWidget(string $planId, string $cspNonce): string
{
    return WidgetSnippet::render(new SnippetOptions(
        planId: $planId,
        tokenEndpoint: '/lingara/token',
        theme: 'auto',
        nonce: $cspNonce,
    ));
}

C#

app.MapGet("/practice/{planId}", (string planId, HttpContext http) =>
{
    var widget = WidgetSnippet.Render(new SnippetOptions
    {
        PlanId = planId,                              // the plan your server made for this player
        TokenEndpoint = "/lingara/token",             // same origin: the element sends this page's cookies
        Nonce = http.Items["csp-nonce"] as string,     // your CSP's nonce, if script-src has one
    });
    return Results.Content($"<!doctype html><title>Practice</title>\n{widget}", "text/html");
}).RequireAuthorization();