Skip to content
Open
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: 2 additions & 0 deletions apps/server/src/content.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { seedFocusedConcepts } from "./seed-focused-concepts.js";
import type { DB } from "./db.js";
import { seedCurriculum } from "./curriculum.js";
import { seedPractice } from "./practice.js";
Expand Down Expand Up @@ -268,5 +269,6 @@ export async function seed(db: DB) {
}
await seedPractice(tx);
await seedExtendedPractice(tx);
await seedFocusedConcepts(tx);
});
}
7 changes: 7 additions & 0 deletions apps/server/src/event-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const screen = z.enum([
"history",
"support",
"admin",
"plans",
"checkout",
]);
export const eventProperties = {
USER_REGISTERED: z.object({ client: z.enum(["web", "android"]) }),
Expand Down Expand Up @@ -51,6 +53,10 @@ export const eventProperties = {
CONTENT_PUBLISHED: z.object({ topics: small, problems: small }),
SCREEN_VIEWED: z.object({ screen, topic: code.optional() }),
EXAMPLE_SELECTED: z.object({ topic: code, index: small }),
EXAMPLE_MODE_SELECTED: z.object({
topic: code,
mode: z.enum(["animation", "manual"]),
}),
EXAMPLE_APPLIED: z.object({
topic: code,
count: small,
Expand Down Expand Up @@ -103,6 +109,7 @@ export type BusinessEventType = keyof typeof eventProperties;
export const clientEventTypes = [
"SCREEN_VIEWED",
"EXAMPLE_SELECTED",
"EXAMPLE_MODE_SELECTED",
"EXAMPLE_APPLIED",
"VISUALIZATION_CONTROL",
"PREDICTION_CHECKED",
Expand Down
166 changes: 166 additions & 0 deletions apps/server/src/focused-concepts.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion apps/server/src/quiz-bank.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { practiceTopic } from "./focused-concepts.js";
// Authored, versioned questions. Answers stay on the server until submission.
type Entry = [string, string, string, string, string];
const bank: Record<string, Entry[]> = {
Expand Down Expand Up @@ -757,7 +758,7 @@ const bank: Record<string, Entry[]> = {
export function topicQuizzes(topic: { id: string; quiz: any }) {
return [
{ ...topic.quiz, id: "core" },
...(bank[topic.id] ?? []).map(
...(bank[practiceTopic(topic.id)] ?? []).map(
([question, correct, b, c, explanation], i) => {
const choices = [correct, b, c],
offset = (i + topic.id.length) % 3;
Expand Down
46 changes: 46 additions & 0 deletions apps/server/src/seed-focused-concepts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { DB } from "./db.js";
import { focusedConcepts } from "./focused-concepts.js";

export async function seedFocusedConcepts(db: DB) {
const parents = new Map(
(
await db.query("SELECT id,category,priority,complexity FROM topics")
).rows.map((t) => [t.id, t]),
);
const rows = focusedConcepts.map((c) => {
const parent = parents.get(c.parent);
if (!parent) throw new Error(`Missing curriculum parent: ${c.parent}`);
const options = [
c.principle,
"입력 조건이나 경계는 결과에 영향을 주지 않습니다.",
"모든 입력에서 같은 결과를 반환합니다.",
];
const shift = c.id.length % 3;
const choices = [...options.slice(shift), ...options.slice(0, shift)];
return [
c.id,
c.title,
parent.category,
parent.priority,
c.principle,
parent.complexity,
JSON.stringify({
question: `${c.title}에 대한 설명으로 옳은 것은?`,
options: choices,
answer: choices.indexOf(c.principle),
explanation: c.example,
}),
];
});
// Two round trips regardless of lesson count; important for remote DB cold starts.
const placeholders = rows
.map(
(_, i) =>
`(${Array.from({ length: 7 }, (_, j) => `$${i * 7 + j + 1}`).join(",")})`,
)
.join(",");
await db.query(
`INSERT INTO topics(id,title,category,priority,body,complexity,quiz) VALUES ${placeholders} ON CONFLICT DO NOTHING`,
rows.flat(),
);
}
43 changes: 41 additions & 2 deletions apps/server/test/learning-flow.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { safeEventProperties } from "../src/event-catalog.js";
import { conceptCost } from "../../web/src/concept-costs.js";
import { focusedConcepts, practiceTopic } from "../src/focused-concepts.js";
import test from "node:test";
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
Expand All @@ -9,8 +12,10 @@ import {
import { conceptUses } from "../../web/src/concept-uses.js";
import { readRoute } from "../../web/src/router.js";
test("every published concept has answerable prediction checkpoints and animation assets", () => {
assert.equal(Object.keys(conceptUses).length, 47);
assert.equal(Object.keys(conceptUses).length, 200);
for (const topic of Object.keys(conceptUses)) {
assert.equal(conceptCost(topic).length, 3);
assert.ok(conceptCost(topic).every((s) => s.length > 0));
const checks = checkpoints(topic);
assert.ok(checks.length, topic);
for (const check of checks) {
Expand All @@ -25,7 +30,9 @@ test("every published concept has answerable prediction checkpoints and animatio
for (const theme of ["light", "dark"])
for (const ext of ["png", "gif"])
assert.ok(
existsSync(`apps/web/public/learning/${topic}-${theme}.${ext}`),
existsSync(
`apps/web/public/learning/${practiceTopic(topic)}-${theme}.${ext}`,
),
);
}
});
Expand All @@ -50,3 +57,35 @@ test("routes reject malformed steps, extra segments and unknown admin sections",
assert.equal(readRoute("/records/abc").recordId, "abc");
assert.equal(readRoute("/learn/tree/code?record=abc").recordId, "abc");
});

test("200 curriculum entries are distinct and focused lessons have concrete worked cases", () => {
assert.equal(focusedConcepts.length, 153);
assert.equal(new Set(focusedConcepts.map((c) => c.id)).size, 153);
assert.equal(new Set(focusedConcepts.map((c) => c.title)).size, 153);
for (const c of focusedConcepts) {
assert.ok(conceptUses[c.parent], c.id);
assert.ok(c.principle.length > 25, c.id);
assert.ok(c.example.length > 35, c.id);
assert.ok(/[0-9A-Z가-힣]/.test(c.example), c.id);
}
});

test("billing screens and example-mode events fit the closed analytics vocabulary", () => {
for (const screen of ["plans", "checkout"])
assert.deepEqual(safeEventProperties("SCREEN_VIEWED", { screen }), {
screen,
});
assert.deepEqual(
safeEventProperties("EXAMPLE_MODE_SELECTED", {
topic: "array",
mode: "manual",
}),
{ topic: "array", mode: "manual" },
);
assert.throws(() =>
safeEventProperties("EXAMPLE_MODE_SELECTED", {
topic: "array",
mode: "untrusted",
}),
);
});
2 changes: 1 addition & 1 deletion apps/server/test/platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ test("quiz hides answer until submission and repeated request returns original r
test("every topic has three distinct quizzes and extra answers remain server-only", async () => {
const u = await login("expanded-quiz");
const topics = (await request(u, "GET", "/api/topics")).json().topics;
assert.equal(topics.length, 47);
assert.equal(topics.length, 200);
for (const topic of topics) {
assert.equal(topic.quizzes.length, 3, topic.id);
assert.equal(new Set(topic.quizzes.map((q: any) => q.question)).size, 3);
Expand Down
7 changes: 4 additions & 3 deletions apps/server/test/practice-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { practiceTopic } from "../src/focused-concepts.js";
import test from "node:test";
import assert from "node:assert/strict";
import { database } from "../src/db.js";
import { seed } from "../src/content.js";
import { catalog } from "../src/content-admin.js";
test("all 47 concepts have complete coding problems and reseeding preserves published content", async () => {
test("all 200 concepts link to complete coding problems and reseeding preserves published content", async () => {
const db = await database();
try {
await seed(db);
const topics = (await db.query("SELECT * FROM topics")).rows;
const problems = (await db.query("SELECT * FROM problems")).rows;
assert.equal(topics.length, 47);
assert.equal(topics.length, 200);
assert.equal(problems.length, 47);
catalog.parse({ topics, problems });
assert.deepEqual(
new Set(problems.map((p) => p.topic_id)),
new Set(topics.map((t) => t.id)),
new Set(topics.map((t) => practiceTopic(t.id))),
);
for (const p of problems) {
assert.deepEqual(Object.keys(p.references_code).sort(), [
Expand Down
Binary file modified apps/web/public/learning/advanced-dp-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/advanced-dp-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/advanced-dp-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/advanced-dp-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/array-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/array-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/array-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/array-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/backtracking-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/backtracking-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/backtracking-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/backtracking-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/balanced-tree-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/balanced-tree-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bellman-ford-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bellman-ford-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bellman-ford-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bellman-ford-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bfs-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bfs-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bfs-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/bfs-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/binary-search-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/binary-search-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/binary-search-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/learning/binary-search-light.png
Binary file modified apps/web/public/learning/brute-force-dark.gif
Binary file modified apps/web/public/learning/brute-force-dark.png
Binary file modified apps/web/public/learning/brute-force-light.gif
Binary file modified apps/web/public/learning/brute-force-light.png
Binary file modified apps/web/public/learning/bst-dark.gif
Binary file modified apps/web/public/learning/bst-dark.png
Binary file modified apps/web/public/learning/bst-light.gif
Binary file modified apps/web/public/learning/bst-light.png
Binary file modified apps/web/public/learning/bubble-sort-dark.gif
Binary file modified apps/web/public/learning/bubble-sort-dark.png
Binary file modified apps/web/public/learning/bubble-sort-light.gif
Binary file modified apps/web/public/learning/bubble-sort-light.png
Binary file modified apps/web/public/learning/dfs-dark.gif
Binary file modified apps/web/public/learning/dfs-dark.png
Binary file modified apps/web/public/learning/dfs-light.gif
Binary file modified apps/web/public/learning/dfs-light.png
Binary file modified apps/web/public/learning/dijkstra-dark.gif
Binary file modified apps/web/public/learning/dijkstra-dark.png
Binary file modified apps/web/public/learning/dijkstra-light.gif
Binary file modified apps/web/public/learning/dijkstra-light.png
Binary file modified apps/web/public/learning/dp-dark.gif
Binary file modified apps/web/public/learning/dp-dark.png
Binary file modified apps/web/public/learning/dp-light.gif
Binary file modified apps/web/public/learning/dp-light.png
Binary file modified apps/web/public/learning/fenwick-dark.gif
Binary file modified apps/web/public/learning/fenwick-dark.png
Binary file modified apps/web/public/learning/fenwick-light.gif
Binary file modified apps/web/public/learning/fenwick-light.png
Binary file modified apps/web/public/learning/fft-dark.gif
Binary file modified apps/web/public/learning/fft-dark.png
Binary file modified apps/web/public/learning/fft-light.gif
Binary file modified apps/web/public/learning/fft-light.png
Binary file modified apps/web/public/learning/flow-dark.gif
Binary file modified apps/web/public/learning/flow-dark.png
Binary file modified apps/web/public/learning/flow-light.gif
Binary file modified apps/web/public/learning/flow-light.png
Binary file modified apps/web/public/learning/floyd-warshall-dark.gif
Binary file modified apps/web/public/learning/floyd-warshall-dark.png
Binary file modified apps/web/public/learning/floyd-warshall-light.gif
Binary file modified apps/web/public/learning/floyd-warshall-light.png
Binary file modified apps/web/public/learning/geometry-dark.gif
Binary file modified apps/web/public/learning/geometry-dark.png
Binary file modified apps/web/public/learning/geometry-light.gif
Binary file modified apps/web/public/learning/geometry-light.png
Binary file modified apps/web/public/learning/graph-list-dark.gif
Binary file modified apps/web/public/learning/graph-list-dark.png
Binary file modified apps/web/public/learning/graph-list-light.gif
Binary file modified apps/web/public/learning/graph-list-light.png
Binary file modified apps/web/public/learning/graph-matrix-dark.gif
Binary file modified apps/web/public/learning/graph-matrix-dark.png
Binary file modified apps/web/public/learning/graph-matrix-light.gif
Binary file modified apps/web/public/learning/graph-matrix-light.png
Binary file modified apps/web/public/learning/greedy-dark.gif
Binary file modified apps/web/public/learning/greedy-dark.png
Binary file modified apps/web/public/learning/greedy-light.gif
Binary file modified apps/web/public/learning/greedy-light.png
Binary file modified apps/web/public/learning/hash-dark.gif
Binary file modified apps/web/public/learning/hash-dark.png
Binary file modified apps/web/public/learning/hash-light.gif
Binary file modified apps/web/public/learning/hash-light.png
Binary file modified apps/web/public/learning/heap-dark.gif
Binary file modified apps/web/public/learning/heap-dark.png
Binary file modified apps/web/public/learning/heap-light.gif
Binary file modified apps/web/public/learning/heap-light.png
Binary file modified apps/web/public/learning/insertion-sort-dark.gif
Binary file modified apps/web/public/learning/insertion-sort-dark.png
Binary file modified apps/web/public/learning/insertion-sort-light.gif
Binary file modified apps/web/public/learning/insertion-sort-light.png
Binary file modified apps/web/public/learning/kmp-dark.gif
Binary file modified apps/web/public/learning/kmp-dark.png
Binary file modified apps/web/public/learning/kmp-light.gif
Binary file modified apps/web/public/learning/kmp-light.png
Binary file modified apps/web/public/learning/lca-dark.gif
Binary file modified apps/web/public/learning/lca-dark.png
Binary file modified apps/web/public/learning/lca-light.gif
Binary file modified apps/web/public/learning/lca-light.png
Binary file modified apps/web/public/learning/linear-search-dark.gif
Binary file modified apps/web/public/learning/linear-search-dark.png
Binary file modified apps/web/public/learning/linear-search-light.gif
Binary file modified apps/web/public/learning/linear-search-light.png
Binary file modified apps/web/public/learning/linked-list-dark.gif
Binary file modified apps/web/public/learning/linked-list-dark.png
Binary file modified apps/web/public/learning/linked-list-light.gif
Binary file modified apps/web/public/learning/linked-list-light.png
Binary file modified apps/web/public/learning/memoization-dark.gif
Binary file modified apps/web/public/learning/memoization-dark.png
Binary file modified apps/web/public/learning/memoization-light.gif
Binary file modified apps/web/public/learning/memoization-light.png
Binary file modified apps/web/public/learning/merge-sort-dark.gif
Binary file modified apps/web/public/learning/merge-sort-dark.png
Binary file modified apps/web/public/learning/merge-sort-light.gif
Binary file modified apps/web/public/learning/merge-sort-light.png
Binary file modified apps/web/public/learning/mst-dark.gif
Binary file modified apps/web/public/learning/mst-dark.png
Binary file modified apps/web/public/learning/mst-light.gif
Binary file modified apps/web/public/learning/mst-light.png
Binary file modified apps/web/public/learning/prefix-sum-dark.gif
Binary file modified apps/web/public/learning/prefix-sum-dark.png
Binary file modified apps/web/public/learning/prefix-sum-light.gif
Binary file modified apps/web/public/learning/prefix-sum-light.png
Binary file modified apps/web/public/learning/queue-dark.gif
Binary file modified apps/web/public/learning/queue-dark.png
Binary file modified apps/web/public/learning/queue-light.gif
Binary file modified apps/web/public/learning/queue-light.png
Binary file modified apps/web/public/learning/quick-sort-dark.gif
Binary file modified apps/web/public/learning/quick-sort-dark.png
Binary file modified apps/web/public/learning/quick-sort-light.gif
Binary file modified apps/web/public/learning/quick-sort-light.png
Binary file modified apps/web/public/learning/recursion-dark.gif
Binary file modified apps/web/public/learning/recursion-dark.png
Binary file modified apps/web/public/learning/recursion-light.gif
Binary file modified apps/web/public/learning/recursion-light.png
Binary file modified apps/web/public/learning/scc-dark.gif
Binary file modified apps/web/public/learning/scc-dark.png
Binary file modified apps/web/public/learning/scc-light.gif
Binary file modified apps/web/public/learning/scc-light.png
Binary file modified apps/web/public/learning/segment-tree-dark.gif
Binary file modified apps/web/public/learning/segment-tree-dark.png
Binary file modified apps/web/public/learning/segment-tree-light.gif
Binary file modified apps/web/public/learning/segment-tree-light.png
Binary file modified apps/web/public/learning/selection-sort-dark.gif
Binary file modified apps/web/public/learning/selection-sort-dark.png
Binary file modified apps/web/public/learning/selection-sort-light.gif
Binary file modified apps/web/public/learning/selection-sort-light.png
Binary file modified apps/web/public/learning/sliding-window-dark.gif
Binary file modified apps/web/public/learning/sliding-window-dark.png
Binary file modified apps/web/public/learning/sliding-window-light.gif
Binary file modified apps/web/public/learning/sliding-window-light.png
Binary file modified apps/web/public/learning/sorting-dark.gif
Binary file modified apps/web/public/learning/sorting-dark.png
Binary file modified apps/web/public/learning/sorting-light.gif
Binary file modified apps/web/public/learning/sorting-light.png
Binary file modified apps/web/public/learning/stack-dark.gif
Binary file modified apps/web/public/learning/stack-dark.png
Binary file modified apps/web/public/learning/stack-light.gif
Binary file modified apps/web/public/learning/stack-light.png
Binary file modified apps/web/public/learning/tabulation-dark.gif
Binary file modified apps/web/public/learning/tabulation-dark.png
Binary file modified apps/web/public/learning/tabulation-light.gif
Binary file modified apps/web/public/learning/tabulation-light.png
Binary file modified apps/web/public/learning/topological-sort-dark.gif
Binary file modified apps/web/public/learning/topological-sort-dark.png
Binary file modified apps/web/public/learning/topological-sort-light.gif
Binary file modified apps/web/public/learning/topological-sort-light.png
Binary file modified apps/web/public/learning/tree-dark.gif
Binary file modified apps/web/public/learning/tree-dark.png
Binary file modified apps/web/public/learning/tree-light.gif
Binary file modified apps/web/public/learning/tree-light.png
Binary file modified apps/web/public/learning/trie-dark.gif
Binary file modified apps/web/public/learning/trie-dark.png
Binary file modified apps/web/public/learning/trie-light.gif
Binary file modified apps/web/public/learning/trie-light.png
Binary file modified apps/web/public/learning/two-pointer-dark.gif
Binary file modified apps/web/public/learning/two-pointer-dark.png
Binary file modified apps/web/public/learning/two-pointer-light.gif
Binary file modified apps/web/public/learning/two-pointer-light.png
Binary file modified apps/web/public/learning/union-find-dark.gif
Binary file modified apps/web/public/learning/union-find-dark.png
Binary file modified apps/web/public/learning/union-find-light.gif
Binary file modified apps/web/public/learning/union-find-light.png
143 changes: 143 additions & 0 deletions apps/web/src/BillingScreens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React from "react";
import { go } from "./router.js";

/** Presentation only. Never collects payment credentials or grants review credits. */
export function BillingScreens({
checkout,
usage,
}: {
checkout: boolean;
usage: any;
}) {
return (
<div className="billing-page">
<div className="page-heading">
<span className="eyebrow">
{checkout ? "CHECKOUT PREVIEW" : "MEMBERSHIP"}
</span>
<h1>
{checkout ? "구독 결제 안내" : "배움은 무료로, 리뷰는 필요할 때"}
</h1>
<p>
{checkout
? "상품과 결제 조건을 확인하는 화면이에요. 아직 결제할 수 없어요."
: "개념 학습부터 코드 실행·복잡도 분석까지 무료로 이용하세요."}
</p>
</div>
{checkout ? (
<div className="checkout-layout">
<section className="panel">
<h2>1. 구독 상품</h2>
<h3>Algostep 리뷰 구독</h3>
<p>LLM이 논리 오류, 효율 개선, 대안 코드를 설명해요.</p>
<dl className="billing-facts">
<dt>월 구독료</dt>
<dd>확정 전</dd>
<dt>월 리뷰 제공량</dt>
<dd>확정 전</dd>
<dt>정기 결제일</dt>
<dd>상품 출시 시 안내</dd>
<dt>환불·해지 조건</dt>
<dd>상품 출시 시 안내</dd>
</dl>
<hr />
<h2>2. 결제 수단</h2>
<div className="payment-placeholder">결제 수단 준비 중</div>
<p className="caption">
PG 연동 후 지원 수단을 안내할 예정이에요. 지금은 카드 정보나 결제
정보를 입력받지 않아요.
</p>
</section>
<aside className="panel order-summary">
<h2>결제 내역</h2>
<dl className="billing-facts">
<dt>상품</dt>
<dd>리뷰 구독</dd>
<dt>결제 금액</dt>
<dd>가격 미정</dd>
<dt>현재 청구</dt>
<dd>없음</dd>
</dl>
<p role="status">
가격·제공량·약관 확정 후 신청할 수 있어요. 현재 구독 신청이나
청구는 발생하지 않아요.
</p>
<button disabled>결제 준비 중</button>
<button className="secondary" onClick={() => go("/plans")}>
구독 안내로 돌아가기
</button>
</aside>
</div>
) : (
<>
<div className="plans-grid">
<article className="panel plan-card">
<span className="badge">기본 이용</span>
<h2>무료</h2>
<p className="plan-price">
₩0 <small>/ 계속 무료</small>
</p>
<ul>
<li>200개 개념과 시각화 학습</li>
<li>단계별 상태 예측</li>
<li>웹에서 코드 작성·실행</li>
<li>시간·공간 복잡도 분석</li>
<li>LLM 코드 리뷰 월 3건</li>
<li>기록 저장과 PDF 내보내기</li>
</ul>
<button className="secondary" onClick={() => go("/learn")}>
무료로 학습하기
</button>
</article>
<article className="panel plan-card featured-plan">
<span className="badge">출시 준비 중</span>
<h2>리뷰 구독</h2>
<p className="plan-price">가격 준비 중</p>
<p>
무료 리뷰를 모두 사용한 뒤에도 코드에 대한 설명을 받을 수 있는
상품을 준비하고 있어요.
</p>
<ul>
<li>논리 오류와 원인 설명</li>
<li>시간·공간 효율 개선 제안</li>
<li>대안 코드와 접근 방법</li>
</ul>
<p className="caption">
가격, 월 제공량, 정기 결제 및 환불 조건은 아직 정해지지
않았어요.
</p>
<button onClick={() => go("/checkout")}>
결제 화면 미리 보기
</button>
</article>
</div>
<section className="panel billing-policy">
<h2>무료 리뷰 이용 안내</h2>
{usage && (
<p>
이번 달 남은 무료 리뷰:{" "}
<strong>
{Math.max(
0,
3 - (usage.consumed ?? 0) - (usage.reserved ?? 0),
)}
건
</strong>
</p>
)}
<p>
매월 1일 00:00 KST에 3건이 새로 제공되고, 남은 횟수는 이월되지
않아요. 코드 작성·실행과 기본 분석에는 리뷰 횟수가 차감되지
않아요.
</p>
<p>
모바일에서는 개념과 예측 학습을 이용하고, 코드 작성과 실행은
웹에서 진행하세요.
</p>
<p>현재 결제 기능은 준비 중이며 유료 구독이 활성화되지 않아요.</p>
</section>
</>
)}
</div>
);
}
14 changes: 13 additions & 1 deletion apps/web/src/DiagramCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,19 @@ export function DiagramCanvas({
<text
textAnchor="middle"
y={n.detail ? -3 : 5}
style={onSelect ? { fontSize: 14 / zoom } : undefined}
style={{
fontSize: Math.min(
onSelect ? 14 / zoom : 16,
((n.shape === "card" ? (n.width ?? 100) : 48) - 12) /
Math.max(
1,
Array.from(n.label).reduce(
(sum, c) => sum + (c.charCodeAt(0) > 255 ? 1 : 0.62),
0,
),
),
),
}}
>
{n.label.length > 22 ? n.label.slice(0, 21) + "…" : n.label}
</text>
Expand Down
Loading
Loading