Skip to content

Commit 49ad189

Browse files
authored
Merge pull request #88 from GoCon/feat/timetable-slides
feat: プログラム詳細に登壇資料の埋め込みを追加
2 parents 968f953 + e1a01a9 commit 49ad189

9 files changed

Lines changed: 315 additions & 15 deletions

File tree

docs/timetable-data.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ SESSIONIZE_API_URL='https://sessionize.com/api/v2/<id>/view/All' pnpm fetch:time
7272
- Accepted セッションのみ残す
7373
- スピーカー情報をセッションへ埋め込む
7474
- `categoryItems` から `type` / `difficulty` / `duration` を解決する(欠けていればエラー)
75+
- `questionAnswers` の questionId `138931`(登壇資料 URL)を `slidesUrl` へ変換する
76+
- Speaker Deck / Docswell / Google スライド → 埋め込み用 URL
77+
- それ以外のドメイン → 元 URL のまま(詳細ページでは「登壇資料を見る」リンク)
78+
- Speaker Deck 公開ページなど自動変換不可 → 固定値 `needsManual`(詳細ページでは非表示)
79+
- 既存 `data.json``needsManual` 以外の `slidesUrl` がある場合は上書きしない
7580
- サイト未使用のフィールドを除去する
7681
- 表示用の時刻・部屋は含めない(カード表示時に `sessionGrid` / `schedule` から解決)
7782
- `parseRawData.ts` は整形済み JSON と手動入力を `Program` のマップへ載せる薄い読み込み層
@@ -81,7 +86,8 @@ SESSIONIZE_API_URL='https://sessionize.com/api/v2/<id>/view/All' pnpm fetch:time
8186
- ファイル: `src/components/timetable/data.json`
8287
- 利用時に使いやすい形(スピーカー埋め込み済みなど)で git 追跡する
8388
- タイムテーブル上の配置(枠・開始時刻・部屋)は、データ本体とは別に `sessionGrid.ts` / `schedule.ts` などで管理する
84-
- **`pnpm transform:timetable` で上書きされる**ため、基調講演・スポンサーはここに書かない
89+
- **`pnpm transform:timetable` で再生成される**ため、基調講演・スポンサーはここに書かない
90+
- ただし `slidesUrl` については、既に埋め込み URL や外部リンクが入っている場合は再生成時も保持する(`needsManual` を手動で直した値が消えない)
8591

8692
## 5. 基調講演・スポンサーセッション(手動入力)
8793

@@ -156,5 +162,6 @@ export const manualSessions: SessionProgram[] = [
156162

157163
1. `SESSIONIZE_API_URL` を指定して `pnpm fetch:timetable` を実行する(または GitHub Actions に任せる)
158164
2. `pnpm transform:timetable``data.json` を生成する
159-
3. 基調講演・スポンサーは `manualSessions.ts` を編集する
160-
4. `data.json` / `manualSessions.ts` の変更をコミットする
165+
3. `slidesUrl``needsManual` のセッションは、埋め込み用 URL を `data.json` に手動で書く(次回 transform でも保持される)
166+
4. 基調講演・スポンサーは `manualSessions.ts` を編集する
167+
5. `data.json` / `manualSessions.ts` の変更をコミットする

scripts/transform-timetable-data.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* - Accepted のみ残す
55
* - スピーカー情報をセッションへ埋め込む
66
* - categoryItems から type / difficulty / duration を解決する(欠けていればエラー)
7+
* - 登壇資料 URL は埋め込み形式へ変換し slidesUrl として残す
8+
* (想定外ドメインは元 URL。変換不可は needsManual。既存の URL は上書きしない)
79
* - 部屋・開始時刻は Sessionize に含まれないため、ここでは扱わない
810
* (sessionGrid.ts / schedule.ts で手動紐づけする)
911
*
@@ -13,6 +15,12 @@
1315
import fs from "node:fs";
1416
import path from "node:path";
1517
import { fileURLToPath } from "node:url";
18+
import {
19+
isPreservableSlidesUrl,
20+
parseSlideEmbed,
21+
SLIDES_URL_NEEDS_MANUAL,
22+
toSlidesUrl,
23+
} from "../src/components/timetable/parseSlideEmbed.ts";
1624

1725
const rootDir = path.resolve(
1826
path.dirname(fileURLToPath(import.meta.url)),
@@ -59,6 +67,7 @@ type ShapedSession = {
5967
speaker: Speaker;
6068
description: string;
6169
duration?: WorkshopDuration;
70+
slidesUrl?: string;
6271
};
6372

6473
type ShapedData = {
@@ -80,15 +89,23 @@ type RawSpeaker = {
8089
links?: RawSpeakerLink[];
8190
};
8291

92+
type RawQuestionAnswer = {
93+
questionId: number;
94+
answerValue?: string | null;
95+
};
96+
8397
type RawSession = {
8498
id: string;
8599
title: string;
86100
description?: string;
87101
speakers?: string[];
88102
categoryItems?: number[];
103+
questionAnswers?: RawQuestionAnswer[];
89104
status: string;
90105
};
91106

107+
const SLIDES_QUESTION_ID = 138931;
108+
92109
type RawData = {
93110
sessions?: RawSession[];
94111
speakers?: RawSpeaker[];
@@ -111,6 +128,53 @@ function normalizeDescription(description: string): string {
111128
return description.replace(/\r\n/g, "\n");
112129
}
113130

131+
function extractSlidesUrl(session: RawSession): string | undefined {
132+
const answer = (session.questionAnswers ?? []).find(
133+
(qa) => qa.questionId === SLIDES_QUESTION_ID,
134+
);
135+
const value = answer?.answerValue?.trim();
136+
if (!value) {
137+
return undefined;
138+
}
139+
140+
return toSlidesUrl(parseSlideEmbed(value));
141+
}
142+
143+
function resolveSlidesUrl(
144+
session: RawSession,
145+
existingSlidesUrls: Map<string, string>,
146+
): string | undefined {
147+
const parsed = extractSlidesUrl(session);
148+
const existing = existingSlidesUrls.get(session.id);
149+
150+
if (
151+
existing &&
152+
isPreservableSlidesUrl(existing) &&
153+
(!parsed || parsed === SLIDES_URL_NEEDS_MANUAL)
154+
) {
155+
return existing;
156+
}
157+
158+
return parsed;
159+
}
160+
161+
function loadExistingSlidesUrls(filePath: string): Map<string, string> {
162+
const map = new Map<string, string>();
163+
if (!fs.existsSync(filePath)) {
164+
return map;
165+
}
166+
167+
const existing = JSON.parse(fs.readFileSync(filePath, "utf8")) as {
168+
sessions?: Array<{ id?: string; slidesUrl?: string }>;
169+
};
170+
for (const session of existing.sessions ?? []) {
171+
if (session.id && session.slidesUrl) {
172+
map.set(session.id, session.slidesUrl);
173+
}
174+
}
175+
return map;
176+
}
177+
114178
function requireExactlyOne<T>(
115179
categoryItems: number[],
116180
mapping: ReadonlyArray<readonly [number, T]>,
@@ -153,6 +217,7 @@ function parseSpeaker(rawSpeaker: RawSpeaker, sessionId: string): Speaker {
153217
function transformSession(
154218
session: RawSession,
155219
speakerMap: Map<string, RawSpeaker>,
220+
existingSlidesUrls: Map<string, string>,
156221
): ShapedSession {
157222
const categoryItems = session.categoryItems;
158223
if (!categoryItems || categoryItems.length === 0) {
@@ -208,10 +273,18 @@ function transformSession(
208273
shaped.duration = proposal.duration;
209274
}
210275

276+
const slidesUrl = resolveSlidesUrl(session, existingSlidesUrls);
277+
if (slidesUrl) {
278+
shaped.slidesUrl = slidesUrl;
279+
}
280+
211281
return shaped;
212282
}
213283

214-
function transform(raw: RawData): ShapedData {
284+
function transform(
285+
raw: RawData,
286+
existingSlidesUrls: Map<string, string>,
287+
): ShapedData {
215288
if (!raw.sessions) {
216289
throw new Error("rawData.json に sessions がありません");
217290
}
@@ -225,7 +298,9 @@ function transform(raw: RawData): ShapedData {
225298

226299
const sessions = raw.sessions
227300
.filter((session) => session.status === "Accepted")
228-
.map((session) => transformSession(session, speakerMap));
301+
.map((session) =>
302+
transformSession(session, speakerMap, existingSlidesUrls),
303+
);
229304

230305
return { sessions };
231306
}
@@ -240,7 +315,8 @@ function main(): void {
240315
}
241316

242317
const raw = JSON.parse(fs.readFileSync(inputPath, "utf8")) as RawData;
243-
const shaped = transform(raw);
318+
const existingSlidesUrls = loadExistingSlidesUrls(outputPath);
319+
const shaped = transform(raw, existingSlidesUrls);
244320
fs.writeFileSync(outputPath, `${JSON.stringify(shaped, null, 2)}\n`, "utf8");
245321

246322
console.log(`Wrote ${outputPath} (${shaped.sessions.length} sessions)`);

0 commit comments

Comments
 (0)