diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 27e7f6a8..427a7842 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-04 Platform Public Code Search matcher / DTO 收口 + +- 背景:`resolvePlatformPublicCodeSearchPlan(...)` 已收口公开搜索顺序,但 `PlatformEntryFlowShellImpl.tsx` 仍内联 RPG by-code DTO 构造,以及拼图、大鱼吃小鱼、跳一跳、敲木鱼、宝贝识物、抓大鹅、方洞挑战、视觉小说和汪汪声浪的 `isSame*PublicWorkCode` 匹配、公开可见性过滤与详情卡映射。 +- 决策:扩展 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,以 `mapRpgPublicCodeSearchDetailToGalleryCard(...)` 和各 `resolve*PublicCodeSearchMatch(...)` 收口 per-play 公开码匹配与 DTO 映射;壳层只保留 gallery 刷新、详情打开、Bark Battle runtime 特例、用户查询和错误归航副作用。`M3D-*` 旧抓大鹅前缀在 `isSameMatch3DPublicWorkCode(...)` 中继续匹配。 +- 影响范围:平台首页搜索框、初始 `publicWorkCode` 恢复、各玩法公开作品号命中、RPG 公开作品 by-code 详情映射、Bark Battle runtime 内搜索启动。 +- 验证方式:`npm run test -- src/components/platform-entry/platformPublicCodeSearchModel.test.ts src/services/publicWorkCode.test.ts`、针对搜索 Module / 壳层 / publicWorkCode 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## 2026-06-04 Draft Generation Shelf 草稿打开 intent 收口 - 背景:`openPuzzleDraft` / `openMatch3DDraft` 在平台壳内重复判断已发布作品、缺 session、ready 未读、失败 notice、active / background 生成中、持久化 generating 和普通草稿恢复,导致壳层继续理解拼图稳定 ID、抓大鹅 notice key 与生成状态优先级。 diff --git a/docs/README.md b/docs/README.md index 612614c2..b51c6193 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,7 +49,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 创作入口点击的占位、隐藏模板拦截、未知入口 no-op 与工作台启动目标收口到 `src/components/platform-entry/platformCreationLaunchModel.ts`,壳层只执行启动前准备、错误提示和受保护动作,规则见 [【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md)。 -平台入口公开码搜索的用户 ID、陶泥号、RPG 作品号、各玩法作品号前缀和失败回退顺序收口到 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,壳层只按计划执行网络读取、详情打开和错误归航副作用,规则见 [【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md)。 +平台入口公开码搜索的用户 ID、陶泥号、RPG 作品号、各玩法作品号前缀、per-play 公开码匹配、详情卡 DTO 映射和失败回退顺序收口到 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,壳层只按计划执行网络读取、详情打开和错误归航副作用,规则见 [【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md)。 个人“玩过作品”面板的玩法别名、`worldKey` 前缀兜底、RPG 公开详情 payload 和大鱼吃小鱼 gallery miss fallback 收口到 `src/components/platform-entry/platformPlayedWorkOpenModel.ts`,壳层只执行面板关闭、gallery 读取、详情打开和错误提示副作用,规则见 [【前端架构】PlatformPlayedWorkOpenModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPlayedWorkOpenModel收口计划-2026-06-04.md)。 diff --git a/docs/technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md index 07c9ed2d..602a185a 100644 --- a/docs/technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md +++ b/docs/technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md @@ -12,26 +12,29 @@ - `resolvePlatformPublicCodeSearchPlan(keyword)`:输入用户搜索词,输出 `{ normalizedKeyword, steps }`;空输入返回 `null`。 - `PlatformPublicCodeSearchStep`:枚举壳层可执行的查找步骤,包括 `user-id`、`public-user-code`、`rpg-work`、各玩法公开作品步骤与 `bark-battle-work`。 +- `mapRpgPublicCodeSearchDetailToGalleryCard(entry)`:把 RPG by-code 详情响应映射为公开作品卡,收口 `playCount` / `remixCount` / `likeCount` 的 `0` 兜底。 +- `resolve*PublicCodeSearchMatch(entries, keyword)`:统一各玩法公开作品列表的公开码匹配、公开可见性过滤和详情卡 DTO 映射;拼图、大鱼吃小鱼、跳一跳、敲木鱼、宝贝识物、抓大鹅、方洞挑战、视觉小说和汪汪声浪都走此接口。 -`PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:它保留 `getPublicAuthUserByCode`、各玩法 gallery 刷新 / 详情打开、Bark Battle runtime 特例和 missing work 归航副作用,只按 `steps` 顺序执行,前一步失败才尝试下一步。 +`PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:它保留 `getPublicAuthUserByCode`、各玩法 gallery 刷新 / 详情打开、Bark Battle runtime 特例和 missing work 归航副作用,只按 `steps` 顺序执行,前一步失败才尝试下一步;壳层不再重复维护 per-play `isSame*PublicWorkCode` 匹配和 DTO 映射。 ## Interface 约束 - 空白搜索词返回 `null`,壳层不得进入搜索 loading。 - `user_` / `user-` 开头的内部用户 ID 只执行 `user-id`,不回退作品号。 -- `PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀只进入对应玩法公开作品查找;`M3D-*` 继续归入 `M3` / 抓大鹅。 +- `PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀只进入对应玩法公开作品查找;`M3D-*` 继续归入并匹配 `M3` / 抓大鹅。 - `CW` 与 `1-8` 位纯数字先查 RPG 公开作品,再回退陶泥号。 - 普通关键词与 `SY` 陶泥号保持既有顺序:先查陶泥号,再查 RPG 公开作品,再查汪汪声浪作品,最后再以陶泥号兜底。 ## Depth / Leverage / Locality -- **Depth**:壳层只消费短小的 `steps` Interface,搜索前缀、优先级和回退顺序藏入 Module Implementation。 -- **Leverage**:新增公开作品前缀时,先扩展 Module 的 step union、前缀表和单测,再在壳层 Adapter 绑定对应执行函数。 -- **Locality**:搜索计划规则集中在一个纯 Module;UI、网络、详情打开与 runtime 启动副作用继续留在壳层,避免把副作用 setter 变成浅 Interface。 +- **Depth**:壳层只消费短小的 `steps` 与 match result Interface,搜索前缀、优先级、回退顺序、per-play 匹配和 DTO 映射藏入 Module Implementation。 +- **Leverage**:新增公开作品前缀时,先扩展 Module 的 step union、前缀表、matcher 和单测,再在壳层 Adapter 绑定对应网络读取与打开动作。 +- **Locality**:搜索计划与作品命中规则集中在一个纯 Module;UI、网络、详情打开与 runtime 启动副作用继续留在壳层,避免把副作用 setter 变成浅 Interface。 ## 验收 - `npm run test -- src/components/platform-entry/platformPublicCodeSearchModel.test.ts` +- `npm run test -- src/services/publicWorkCode.test.ts` - `npx eslint src/components/platform-entry/platformPublicCodeSearchModel.ts src/components/platform-entry/platformPublicCodeSearchModel.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet` - `npm run typecheck` - `npm run check:encoding` diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index a7d03114..1394d891 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -73,7 +73,7 @@ RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts` 发现页 / 推荐页公开作品卡的作者行只显示可读公开昵称;不得把手机号掩码、`SY-*` 陶泥号或作品号拼接进卡片作者名。陶泥号搜索、作品号复制和完整作品身份只在搜索、详情页或明确的复制入口展示,避免卡片列表暴露账号标识。 -平台公开搜索的分流顺序统一由 `platformPublicCodeSearchModel.ts` 判定:`user_` / `user-` 内部用户 ID 只查用户 ID;`PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀分别直达对应玩法公开作品;`CW` 与 1-8 位纯数字先查 RPG 公开作品再回退陶泥号;普通关键词和 `SY` 陶泥号保持先查陶泥号、再查 RPG 作品、再查汪汪声浪作品、最后陶泥号兜底的既有顺序。平台壳只按计划执行网络读取、详情打开、Bark Battle runtime 特例和缺失作品归航,不在壳层重复维护前缀布尔链。 +平台公开搜索的分流顺序、per-play 公开码匹配、公开可见性过滤和详情卡 DTO 映射统一由 `platformPublicCodeSearchModel.ts` 判定:`user_` / `user-` 内部用户 ID 只查用户 ID;`PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀分别直达对应玩法公开作品;`M3D-*` 作为抓大鹅旧前缀继续匹配;`CW` 与 1-8 位纯数字先查 RPG 公开作品再回退陶泥号;普通关键词和 `SY` 陶泥号保持先查陶泥号、再查 RPG 作品、再查汪汪声浪作品、最后陶泥号兜底的既有顺序。平台壳只按计划执行网络读取、详情打开、Bark Battle runtime 特例和缺失作品归航,不在壳层重复维护前缀布尔链、`isSame*PublicWorkCode` 或 DTO 映射。 个人“玩过作品”面板点击作品时,玩法别名、`worldKey` 前缀兜底、RPG 公开详情 payload 和大鱼吃小鱼缺 gallery 命中时的 fallback work 统一由 `platformPlayedWorkOpenModel.ts` 判定。平台壳只负责关闭面板、调用对应公开详情打开函数、刷新大鱼 gallery、优先使用真实 gallery 命中项和写入错误提示;不要在壳层重新维护 `worldType` / `worldKey` 分支链。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index f47ac8de..21bb578c 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -226,15 +226,7 @@ import { buildSquareHolePublicWorkCode, buildVisualNovelPublicWorkCode, buildWoodenFishPublicWorkCode, - isSameBabyObjectMatchPublicWorkCode, - isSameBarkBattlePublicWorkCode, - isSameBigFishPublicWorkCode, - isSameJumpHopPublicWorkCode, - isSameMatch3DPublicWorkCode, isSamePuzzlePublicWorkCode, - isSameSquareHolePublicWorkCode, - isSameVisualNovelPublicWorkCode, - isSameWoodenFishPublicWorkCode, } from '../../services/publicWorkCode'; import { createPuzzleAgentSession, @@ -356,8 +348,6 @@ import { } from '../rpg-entry/rpgEntryPublicGalleryViewModel'; import { isEdutainmentGalleryEntry, - mapBabyObjectMatchDraftToPlatformGalleryCard, - mapBarkBattleWorkToPlatformGalleryCard, mapPuzzleWorkToPlatformGalleryCard, type PlatformPublicGalleryCard, resolvePlatformPublicWorkCode, @@ -550,8 +540,18 @@ import { resolveProfileWalletBalance, } from './platformProfileWalletDeltaModel'; import { + mapRpgPublicCodeSearchDetailToGalleryCard, type PlatformPublicCodeSearchStep, + resolveBabyObjectMatchPublicCodeSearchMatch, + resolveBarkBattlePublicCodeSearchMatch, + resolveBigFishPublicCodeSearchMatch, + resolveJumpHopPublicCodeSearchMatch, + resolveMatch3DPublicCodeSearchMatch, resolvePlatformPublicCodeSearchPlan, + resolvePuzzlePublicCodeSearchMatch, + resolveSquareHolePublicCodeSearchMatch, + resolveVisualNovelPublicCodeSearchMatch, + resolveWoodenFishPublicCodeSearchMatch, } from './platformPublicCodeSearchModel'; import { buildPlatformPublicGalleryFeeds, @@ -12281,26 +12281,7 @@ export function PlatformEntryFlowShellImpl({ const tryOpenGalleryEntry = async () => { const entry = await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword); - const card = { - ownerUserId: entry.ownerUserId, - profileId: entry.profileId, - publicWorkCode: entry.publicWorkCode, - authorPublicUserCode: entry.authorPublicUserCode, - visibility: 'published', - publishedAt: entry.publishedAt, - updatedAt: entry.updatedAt, - authorDisplayName: entry.authorDisplayName, - worldName: entry.worldName, - subtitle: entry.subtitle, - summaryText: entry.summaryText, - coverImageSrc: entry.coverImageSrc, - themeMode: entry.themeMode, - playableNpcCount: entry.playableNpcCount, - landmarkCount: entry.landmarkCount, - playCount: entry.playCount ?? 0, - remixCount: entry.remixCount ?? 0, - likeCount: entry.likeCount ?? 0, - } satisfies CustomWorldGalleryCard; + const card = mapRpgPublicCodeSearchDetailToGalleryCard(entry); if (!canExposePublicWork(card)) { throw new Error(EDUTAINMENT_HIDDEN_MESSAGE); } @@ -12313,18 +12294,16 @@ export function PlatformEntryFlowShellImpl({ puzzleGalleryEntries.length > 0 ? puzzleGalleryEntries : await refreshPuzzleGallery(); - const matchedEntry = entries - .map(mapPuzzleWorkToPublicWorkDetail) - .filter(canExposePublicWork) - .find((entry) => - isSamePuzzlePublicWorkCode(normalizedKeyword, entry.profileId), - ); + const matchedEntry = resolvePuzzlePublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到拼图作品。'); } - await openPuzzlePublicWorkDetail(matchedEntry.profileId, { + await openPuzzlePublicWorkDetail(matchedEntry.detail.profileId, { tab: platformBootstrap.platformTab, }); }; @@ -12333,170 +12312,133 @@ export function PlatformEntryFlowShellImpl({ bigFishGalleryEntries.length > 0 ? bigFishGalleryEntries : await refreshBigFishGallery(); - const matchedEntry = entries.find((entry) => { - const detailEntry = mapBigFishWorkToPublicWorkDetail(entry); - return ( - canExposePublicWork(detailEntry) && - isSameBigFishPublicWorkCode( - normalizedKeyword, - entry.sourceSessionId, - ) - ); - }); + const matchedEntry = resolveBigFishPublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到大鱼吃小鱼作品。'); } - openPublicWorkDetail(mapBigFishWorkToPublicWorkDetail(matchedEntry)); + openPublicWorkDetail(matchedEntry.detail); }; const tryOpenJumpHopGalleryEntry = async () => { const entries = jumpHopGalleryEntries.length > 0 ? jumpHopGalleryEntries : await refreshJumpHopGallery(); - const matchedEntry = entries.find((entry) => { - const detailEntry = mapJumpHopWorkToPublicWorkDetail(entry); - return ( - canExposePublicWork(detailEntry) && - isSameJumpHopPublicWorkCode(normalizedKeyword, entry.profileId) - ); - }); + const matchedEntry = resolveJumpHopPublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到跳一跳作品。'); } - openPublicWorkDetail(mapJumpHopWorkToPublicWorkDetail(matchedEntry)); + openPublicWorkDetail(matchedEntry.detail); }; const tryOpenWoodenFishGalleryEntry = async () => { const entries = woodenFishGalleryEntries.length > 0 ? woodenFishGalleryEntries : await refreshWoodenFishGallery(); - const matchedEntry = entries.find((entry) => { - const detailEntry = mapWoodenFishWorkToPublicWorkDetail(entry); - return ( - canExposePublicWork(detailEntry) && - isSameWoodenFishPublicWorkCode(normalizedKeyword, entry.profileId) - ); - }); + const matchedEntry = resolveWoodenFishPublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到敲木鱼作品。'); } - openPublicWorkDetail(mapWoodenFishWorkToPublicWorkDetail(matchedEntry)); + openPublicWorkDetail(matchedEntry.detail); }; const tryOpenMatch3DGalleryEntry = async () => { const entries = match3dGalleryEntries.length > 0 ? match3dGalleryEntries : await refreshMatch3DGallery(); - const matchedEntry = entries.find((entry) => { - const detailEntry = mapMatch3DWorkToPublicWorkDetail(entry); - return ( - canExposePublicWork(detailEntry) && - isSameMatch3DPublicWorkCode(normalizedKeyword, entry.profileId) - ); - }); + const matchedEntry = resolveMatch3DPublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到抓大鹅作品。'); } - openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(matchedEntry)); + openPublicWorkDetail(matchedEntry.detail); }; const tryOpenSquareHoleGalleryEntry = async () => { const entries = squareHoleGalleryEntries.length > 0 ? squareHoleGalleryEntries : await refreshSquareHoleGallery(); - const matchedEntry = entries.find((entry) => { - const detailEntry = mapSquareHoleWorkToPublicWorkDetail(entry); - return ( - canExposePublicWork(detailEntry) && - isSameSquareHolePublicWorkCode(normalizedKeyword, entry.profileId) - ); - }); + const matchedEntry = resolveSquareHolePublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到方洞挑战作品。'); } - openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(matchedEntry)); + openPublicWorkDetail(matchedEntry.detail); }; const tryOpenVisualNovelGalleryEntry = async () => { const entries = visualNovelGalleryEntries.length > 0 ? visualNovelGalleryEntries : await refreshVisualNovelGallery(); - const matchedEntry = entries.find((entry) => { - const detailEntry = mapVisualNovelWorkToPublicWorkDetail(entry); - return ( - canExposePublicWork(detailEntry) && - isSameVisualNovelPublicWorkCode(normalizedKeyword, entry.profileId) - ); - }); + const matchedEntry = resolveVisualNovelPublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到视觉小说作品。'); } - openPublicWorkDetail( - mapVisualNovelWorkToPublicWorkDetail(matchedEntry), - ); + openPublicWorkDetail(matchedEntry.detail); }; const tryOpenBabyObjectMatchGalleryEntry = async () => { const entries = (await listLocalBabyObjectMatchDrafts()).filter( (draft) => draft.publicationStatus === 'published', ); - const matchedDraft = entries.find((draft) => { - const detailEntry = - mapBabyObjectMatchDraftToPlatformGalleryCard(draft); - return ( - canExposePublicWork(detailEntry) && - isSameBabyObjectMatchPublicWorkCode( - normalizedKeyword, - draft.profileId, - ) - ); - }); + const matchedDraft = resolveBabyObjectMatchPublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedDraft) { throw new Error('未找到宝贝识物作品。'); } - const detailEntry = - mapBabyObjectMatchDraftToPlatformGalleryCard(matchedDraft); - setBabyObjectMatchDraft(matchedDraft); - openPublicWorkDetail(detailEntry); + setBabyObjectMatchDraft(matchedDraft.item); + openPublicWorkDetail(matchedDraft.detail); }; const tryOpenBarkBattleGalleryEntry = async () => { const entries = barkBattleGalleryEntries.length > 0 ? barkBattleGalleryEntries : await refreshBarkBattleGallery(); - const matchedEntry = entries.find((entry) => { - const detailEntry = mapBarkBattleWorkToPlatformGalleryCard(entry); - return ( - canExposePublicWork(detailEntry) && - isSameBarkBattlePublicWorkCode(normalizedKeyword, entry.workId) - ); - }); + const matchedEntry = resolveBarkBattlePublicCodeSearchMatch( + entries, + normalizedKeyword, + ); if (!matchedEntry) { throw new Error('未找到汪汪声浪作品。'); } if (selectionStage === 'bark-battle-runtime') { - startBarkBattleRunFromWork(matchedEntry, 'platform'); + startBarkBattleRunFromWork(matchedEntry.item, 'platform'); return; } - openPublicWorkDetail( - mapBarkBattleWorkToPlatformGalleryCard(matchedEntry), - ); + openPublicWorkDetail(matchedEntry.detail); }; const runSearchStep = async (step: PlatformPublicCodeSearchStep) => { diff --git a/src/components/platform-entry/platformPublicCodeSearchModel.test.ts b/src/components/platform-entry/platformPublicCodeSearchModel.test.ts index c4bc04b1..5130d391 100644 --- a/src/components/platform-entry/platformPublicCodeSearchModel.test.ts +++ b/src/components/platform-entry/platformPublicCodeSearchModel.test.ts @@ -1,8 +1,39 @@ import { describe, expect, test } from 'vitest'; +import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; +import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; +import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; +import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime'; +import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; +import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; +import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish'; import { + buildBarkBattlePublicWorkCode, + buildBigFishPublicWorkCode, + buildJumpHopPublicWorkCode, + buildMatch3DPublicWorkCode, + buildPuzzlePublicWorkCode, + buildSquareHolePublicWorkCode, + buildVisualNovelPublicWorkCode, + buildWoodenFishPublicWorkCode, +} from '../../services/publicWorkCode'; +import type { CustomWorldProfile } from '../../types'; +import { + mapRpgPublicCodeSearchDetailToGalleryCard, type PlatformPublicCodeSearchStep, + resolveBabyObjectMatchPublicCodeSearchMatch, + resolveBarkBattlePublicCodeSearchMatch, + resolveBigFishPublicCodeSearchMatch, + resolveJumpHopPublicCodeSearchMatch, + resolveMatch3DPublicCodeSearchMatch, resolvePlatformPublicCodeSearchPlan, + resolvePuzzlePublicCodeSearchMatch, + resolveSquareHolePublicCodeSearchMatch, + resolveVisualNovelPublicCodeSearchMatch, + resolveWoodenFishPublicCodeSearchMatch, } from './platformPublicCodeSearchModel'; function expectSearchSteps( @@ -66,4 +97,386 @@ describe('platformPublicCodeSearchModel', () => { expectSearchSteps('SY-00000001', legacyFallbackSteps); expectSearchSteps('月井守望', legacyFallbackSteps); }); + + test('maps RPG detail responses to gallery cards with count defaults', () => { + expect( + mapRpgPublicCodeSearchDetailToGalleryCard( + buildRpgDetailEntry({ + playCount: undefined, + remixCount: undefined, + likeCount: undefined, + }), + ), + ).toMatchObject({ + profileId: 'rpg-profile-1', + visibility: 'published', + worldName: '潮雾世界', + playCount: 0, + remixCount: 0, + likeCount: 0, + }); + }); + + test('resolves public code matches for every play-specific gallery type', () => { + const puzzle = buildPuzzleWork({ profileId: 'puzzle-profile-12345678' }); + const bigFish = buildBigFishWork({ + sourceSessionId: 'big-fish-session-12345678', + }); + const jumpHop = buildJumpHopCard({ profileId: 'jump-hop-profile-12345678' }); + const woodenFish = buildWoodenFishCard({ + profileId: 'wooden-fish-profile-12345678', + }); + const babyObjectMatch = buildBabyObjectMatchDraft({ + profileId: 'baby-object-profile-12345678', + }); + const match3d = buildMatch3DWork({ profileId: 'match3d-profile-12345678' }); + const squareHole = buildSquareHoleWork({ + profileId: 'square-hole-profile-12345678', + }); + const visualNovel = buildVisualNovelWork({ + profileId: 'visual-novel-profile-12345678', + }); + const barkBattle = buildBarkBattleWork({ + workId: 'bark-battle-work-12345678', + }); + + expect( + resolvePuzzlePublicCodeSearchMatch( + [puzzle], + buildPuzzlePublicWorkCode(puzzle.profileId), + )?.detail, + ).toMatchObject({ sourceType: 'puzzle' }); + expect( + resolveBigFishPublicCodeSearchMatch( + [bigFish], + buildBigFishPublicWorkCode(bigFish.sourceSessionId), + )?.detail, + ).toMatchObject({ sourceType: 'big-fish' }); + expect( + resolveJumpHopPublicCodeSearchMatch( + [jumpHop], + buildJumpHopPublicWorkCode(jumpHop.profileId), + )?.detail, + ).toMatchObject({ sourceType: 'jump-hop' }); + expect( + resolveWoodenFishPublicCodeSearchMatch( + [woodenFish], + buildWoodenFishPublicWorkCode(woodenFish.profileId), + )?.detail, + ).toMatchObject({ sourceType: 'wooden-fish' }); + expect( + resolveBabyObjectMatchPublicCodeSearchMatch( + [babyObjectMatch], + `BO-${babyObjectMatch.profileId.slice(-8)}`, + )?.detail, + ).toMatchObject({ sourceType: 'edutainment' }); + expect( + resolveMatch3DPublicCodeSearchMatch( + [match3d], + buildMatch3DPublicWorkCode(match3d.profileId), + )?.detail, + ).toMatchObject({ sourceType: 'match3d' }); + expect( + resolveSquareHolePublicCodeSearchMatch( + [squareHole], + buildSquareHolePublicWorkCode(squareHole.profileId), + )?.detail, + ).toMatchObject({ sourceType: 'square-hole' }); + expect( + resolveVisualNovelPublicCodeSearchMatch( + [visualNovel], + buildVisualNovelPublicWorkCode(visualNovel.profileId), + )?.detail, + ).toMatchObject({ sourceType: 'visual-novel' }); + expect( + resolveBarkBattlePublicCodeSearchMatch( + [barkBattle], + buildBarkBattlePublicWorkCode(barkBattle.workId), + )?.detail, + ).toMatchObject({ sourceType: 'bark-battle' }); + }); + + test('public code search matchers skip entries hidden by visibility policy', () => { + const hiddenPuzzle = buildPuzzleWork({ + profileId: 'hidden-profile-12345678', + }); + + expect( + resolvePuzzlePublicCodeSearchMatch( + [hiddenPuzzle], + buildPuzzlePublicWorkCode(hiddenPuzzle.profileId), + () => false, + ), + ).toBeNull(); + }); }); + +function buildRpgDetailEntry( + overrides: Partial> = {}, +): CustomWorldLibraryEntry { + return { + ownerUserId: 'rpg-owner-1', + profileId: 'rpg-profile-1', + publicWorkCode: 'CW-00000001', + authorPublicUserCode: 'SY-00000001', + profile: {} as CustomWorldProfile, + visibility: 'published', + publishedAt: '2026-06-04T00:00:00.000Z', + updatedAt: '2026-06-04T00:00:00.000Z', + authorDisplayName: '测试作者', + worldName: '潮雾世界', + subtitle: '潮雾港', + summaryText: '潮雾世界说明。', + coverImageSrc: null, + themeMode: 'tide', + playableNpcCount: 1, + landmarkCount: 1, + playCount: 1, + remixCount: 1, + likeCount: 1, + ...overrides, + }; +} + +function buildPuzzleWork( + overrides: Partial = {}, +): PuzzleWorkSummary { + return { + workId: 'puzzle-work-1', + profileId: 'puzzle-profile-1', + ownerUserId: 'user-1', + sourceSessionId: 'puzzle-session-1', + authorDisplayName: '测试作者', + workTitle: '潮雾拼图', + workDescription: '潮雾拼图说明。', + levelName: '潮雾拼图', + summary: '潮雾拼图说明。', + themeTags: [], + coverImageSrc: null, + coverAssetId: null, + publicationStatus: 'published', + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + playCount: 0, + remixCount: 0, + likeCount: 0, + publishReady: true, + levels: [], + ...overrides, + }; +} + +function buildBigFishWork( + overrides: Partial = {}, +): BigFishWorkSummary { + return { + workId: 'big-fish-work-1', + sourceSessionId: 'big-fish-session-1', + ownerUserId: 'user-1', + authorDisplayName: '测试作者', + title: '潮雾大鱼', + subtitle: '潮雾港', + summary: '潮雾大鱼说明。', + coverImageSrc: null, + status: 'published', + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + playCount: 0, + remixCount: 0, + likeCount: 0, + publishReady: true, + levelCount: 1, + levelMainImageReadyCount: 1, + levelMotionReadyCount: 1, + backgroundReady: true, + ...overrides, + }; +} + +function buildJumpHopCard( + overrides: Partial = {}, +): JumpHopGalleryCardResponse { + const profileId = overrides.profileId ?? 'jump-hop-profile-1'; + return { + publicWorkCode: buildJumpHopPublicWorkCode(profileId), + workId: 'jump-hop-work-1', + profileId, + ownerUserId: 'user-1', + authorDisplayName: '测试作者', + workTitle: '潮雾跳一跳', + workDescription: '潮雾跳一跳说明。', + coverImageSrc: null, + themeTags: [], + difficulty: 'standard', + stylePreset: 'minimal-blocks', + publicationStatus: 'published', + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + generationStatus: 'ready', + ...overrides, + }; +} + +function buildWoodenFishCard( + overrides: Partial = {}, +): WoodenFishGalleryCardResponse { + const profileId = overrides.profileId ?? 'wooden-fish-profile-1'; + return { + publicWorkCode: buildWoodenFishPublicWorkCode(profileId), + workId: 'wooden-fish-work-1', + profileId, + ownerUserId: 'user-1', + authorDisplayName: '测试作者', + workTitle: '潮雾木鱼', + workDescription: '潮雾木鱼说明。', + coverImageSrc: null, + themeTags: ['敲木鱼'], + publicationStatus: 'published', + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + generationStatus: 'ready', + ...overrides, + }; +} + +function buildBabyObjectMatchDraft( + overrides: Partial = {}, +): BabyObjectMatchDraft { + return { + draftId: 'baby-draft-1', + profileId: 'baby-object-profile-1', + templateId: 'baby-object-match', + templateName: '宝贝识物', + workTitle: '潮雾识物', + workDescription: '潮雾识物说明。', + itemNames: ['苹果', '香蕉'], + itemAssets: [ + buildBabyObjectMatchItemAsset('item-a', '苹果'), + buildBabyObjectMatchItemAsset('item-b', '香蕉'), + ], + visualPackage: null, + themeTags: ['寓教于乐'], + publicationStatus: 'published', + createdAt: '2026-06-04T00:00:00.000Z', + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + ...overrides, + }; +} + +function buildBabyObjectMatchItemAsset(itemId: string, itemName: string) { + return { + itemId, + itemName, + imageSrc: `/media/${itemId}.png`, + assetObjectId: null, + generationProvider: 'placeholder' as const, + prompt: itemName, + }; +} + +function buildMatch3DWork( + overrides: Partial = {}, +): Match3DWorkSummary { + return { + workId: 'match3d-work-1', + profileId: 'match3d-profile-1', + ownerUserId: 'user-1', + sourceSessionId: 'match3d-session-1', + gameName: '潮雾抓大鹅', + themeText: '潮雾港', + summary: '潮雾抓大鹅说明。', + tags: [], + coverImageSrc: null, + referenceImageSrc: null, + clearCount: 0, + difficulty: 1, + publicationStatus: 'published', + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + publishReady: true, + generationStatus: 'ready', + generatedItemAssets: [], + ...overrides, + }; +} + +function buildSquareHoleWork( + overrides: Partial = {}, +): SquareHoleWorkSummary { + return { + workId: 'square-hole-work-1', + profileId: 'square-hole-profile-1', + ownerUserId: 'user-1', + sourceSessionId: 'square-hole-session-1', + gameName: '潮雾方洞', + themeText: '潮雾港', + twistRule: '避开雾门', + summary: '潮雾方洞说明。', + tags: [], + coverImageSrc: null, + backgroundPrompt: '潮雾港', + backgroundImageSrc: null, + shapeOptions: [], + holeOptions: [], + shapeCount: 1, + difficulty: 1, + publicationStatus: 'published', + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + publishReady: true, + ...overrides, + }; +} + +function buildVisualNovelWork( + overrides: Partial = {}, +): VisualNovelWorkSummary { + return { + runtimeKind: 'visual-novel', + profileId: 'visual-novel-profile-1', + ownerUserId: 'user-1', + title: '潮雾视觉小说', + description: '潮雾视觉小说说明。', + coverImageSrc: null, + tags: [], + publishStatus: 'published', + publishReady: true, + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + ...overrides, + }; +} + +function buildBarkBattleWork( + overrides: Partial = {}, +): BarkBattleWorkSummary { + return { + workId: 'bark-battle-work-1', + draftId: 'bark-battle-draft-1', + ownerUserId: 'user-1', + authorDisplayName: '测试作者', + title: '潮雾声浪', + summary: '潮雾声浪说明。', + themeDescription: '潮雾港', + playerImageDescription: '小狗', + opponentImageDescription: '对手', + onomatopoeia: ['汪'], + playerCharacterImageSrc: null, + opponentCharacterImageSrc: null, + uiBackgroundImageSrc: null, + difficultyPreset: 'normal', + status: 'published', + generationStatus: 'ready', + publishReady: true, + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: '2026-06-04T00:00:00.000Z', + ...overrides, + }; +} diff --git a/src/components/platform-entry/platformPublicCodeSearchModel.ts b/src/components/platform-entry/platformPublicCodeSearchModel.ts index 616d69aa..1f1b93f0 100644 --- a/src/components/platform-entry/platformPublicCodeSearchModel.ts +++ b/src/components/platform-entry/platformPublicCodeSearchModel.ts @@ -1,3 +1,42 @@ +import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; +import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; +import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; +import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { + CustomWorldGalleryCard, + CustomWorldLibraryEntry, +} from '../../../packages/shared/src/contracts/runtime'; +import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; +import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; +import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish'; +import { + isSameBabyObjectMatchPublicWorkCode, + isSameBarkBattlePublicWorkCode, + isSameBigFishPublicWorkCode, + isSameJumpHopPublicWorkCode, + isSameMatch3DPublicWorkCode, + isSamePuzzlePublicWorkCode, + isSameSquareHolePublicWorkCode, + isSameVisualNovelPublicWorkCode, + isSameWoodenFishPublicWorkCode, +} from '../../services/publicWorkCode'; +import type { CustomWorldProfile } from '../../types'; +import type { PlatformPublicGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation'; +import { mapBabyObjectMatchDraftToPlatformGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation'; +import { canExposePublicWork } from './platformEdutainmentVisibility'; +import { mapMatch3DWorkToPublicWorkDetail } from './platformMatch3DRuntimeProfile'; +import { + mapBarkBattleWorkToPublicWorkDetail, + mapBigFishWorkToPublicWorkDetail, + mapJumpHopWorkToPublicWorkDetail, + mapPuzzleWorkToPublicWorkDetail, + mapSquareHoleWorkToPublicWorkDetail, + mapVisualNovelWorkToPublicWorkDetail, + mapWoodenFishWorkToPublicWorkDetail, +} from './platformPublicWorkDetailFlow'; + export type PlatformPublicCodeSearchStep = | 'user-id' | 'public-user-code' @@ -17,6 +56,19 @@ export type PlatformPublicCodeSearchPlan = { steps: readonly PlatformPublicCodeSearchStep[]; }; +export type PlatformPublicCodeSearchMatch = { + item: TItem; + detail: PlatformPublicGalleryCard; +}; + +type PlatformPublicCodeSearchMatcherInput = { + keyword: string; + entries: readonly TItem[]; + mapEntry: (item: TItem) => PlatformPublicGalleryCard; + matchesEntry: (keyword: string, item: TItem) => boolean; + canExposeEntry?: (entry: PlatformPublicGalleryCard) => boolean; +}; + const PLATFORM_PUBLIC_USER_ID_PATTERN = /^user[_-][a-z0-9_-]+$/iu; const PLATFORM_RPG_WORK_NUMERIC_CODE_PATTERN = /^\d{1,8}$/u; @@ -81,3 +133,181 @@ export function resolvePlatformPublicCodeSearchPlan( ], }; } + +export function mapRpgPublicCodeSearchDetailToGalleryCard( + entry: CustomWorldLibraryEntry, +): CustomWorldGalleryCard { + return { + ownerUserId: entry.ownerUserId, + profileId: entry.profileId, + publicWorkCode: entry.publicWorkCode, + authorPublicUserCode: entry.authorPublicUserCode, + visibility: 'published', + publishedAt: entry.publishedAt, + updatedAt: entry.updatedAt, + authorDisplayName: entry.authorDisplayName, + worldName: entry.worldName, + subtitle: entry.subtitle, + summaryText: entry.summaryText, + coverImageSrc: entry.coverImageSrc, + themeMode: entry.themeMode, + playableNpcCount: entry.playableNpcCount, + landmarkCount: entry.landmarkCount, + playCount: entry.playCount ?? 0, + remixCount: entry.remixCount ?? 0, + likeCount: entry.likeCount ?? 0, + }; +} + +export function resolvePuzzlePublicCodeSearchMatch( + entries: readonly PuzzleWorkSummary[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapPuzzleWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSamePuzzlePublicWorkCode(searchKeyword, item.profileId), + canExposeEntry, + }); +} + +export function resolveBigFishPublicCodeSearchMatch( + entries: readonly BigFishWorkSummary[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapBigFishWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSameBigFishPublicWorkCode(searchKeyword, item.sourceSessionId), + canExposeEntry, + }); +} + +export function resolveJumpHopPublicCodeSearchMatch( + entries: readonly JumpHopGalleryCardResponse[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapJumpHopWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSameJumpHopPublicWorkCode(searchKeyword, item.profileId), + canExposeEntry, + }); +} + +export function resolveWoodenFishPublicCodeSearchMatch( + entries: readonly WoodenFishGalleryCardResponse[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapWoodenFishWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSameWoodenFishPublicWorkCode(searchKeyword, item.profileId), + canExposeEntry, + }); +} + +export function resolveBabyObjectMatchPublicCodeSearchMatch( + entries: readonly BabyObjectMatchDraft[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapBabyObjectMatchDraftToPlatformGalleryCard, + matchesEntry: (searchKeyword, item) => + isSameBabyObjectMatchPublicWorkCode(searchKeyword, item.profileId), + canExposeEntry, + }); +} + +export function resolveMatch3DPublicCodeSearchMatch( + entries: readonly Match3DWorkSummary[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapMatch3DWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSameMatch3DPublicWorkCode(searchKeyword, item.profileId), + canExposeEntry, + }); +} + +export function resolveSquareHolePublicCodeSearchMatch( + entries: readonly SquareHoleWorkSummary[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapSquareHoleWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSameSquareHolePublicWorkCode(searchKeyword, item.profileId), + canExposeEntry, + }); +} + +export function resolveVisualNovelPublicCodeSearchMatch( + entries: readonly VisualNovelWorkSummary[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapVisualNovelWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSameVisualNovelPublicWorkCode(searchKeyword, item.profileId), + canExposeEntry, + }); +} + +export function resolveBarkBattlePublicCodeSearchMatch( + entries: readonly BarkBattleWorkSummary[], + keyword: string, + canExposeEntry = canExposePublicWork, +) { + return resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry: mapBarkBattleWorkToPublicWorkDetail, + matchesEntry: (searchKeyword, item) => + isSameBarkBattlePublicWorkCode(searchKeyword, item.workId), + canExposeEntry, + }); +} + +function resolveMappedPublicCodeSearchMatch({ + keyword, + entries, + mapEntry, + matchesEntry, + canExposeEntry = canExposePublicWork, +}: PlatformPublicCodeSearchMatcherInput): + | PlatformPublicCodeSearchMatch + | null { + for (const item of entries) { + const detail = mapEntry(item); + if (canExposeEntry(detail) && matchesEntry(keyword, item)) { + return { item, detail }; + } + } + return null; +} diff --git a/src/services/publicWorkCode.test.ts b/src/services/publicWorkCode.test.ts index c90d1c48..5b724b17 100644 --- a/src/services/publicWorkCode.test.ts +++ b/src/services/publicWorkCode.test.ts @@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest'; import { buildCustomWorldPublicWorkCode, buildJumpHopPublicWorkCode, + buildMatch3DPublicWorkCode, buildWoodenFishPublicWorkCode, isSameCustomWorldPublicWorkCode, isSameJumpHopPublicWorkCode, + isSameMatch3DPublicWorkCode, isSameWoodenFishPublicWorkCode, } from './publicWorkCode'; @@ -34,6 +36,24 @@ describe('publicWorkCode', () => { ); }); + it('matches current and legacy match3d public work prefixes', () => { + expect(buildMatch3DPublicWorkCode('match3d-profile-12345678')).toBe( + 'M3-12345678', + ); + expect( + isSameMatch3DPublicWorkCode( + 'M3-12345678', + 'match3d-profile-12345678', + ), + ).toBe(true); + expect( + isSameMatch3DPublicWorkCode( + 'M3D-12345678', + 'match3d-profile-12345678', + ), + ).toBe(true); + }); + it('builds and matches custom world public work codes from profile ids', () => { expect(buildCustomWorldPublicWorkCode('world-public-1')).toBe('CW-00000001'); expect(isSameCustomWorldPublicWorkCode('cw-00000001', 'world-public-1')).toBe( diff --git a/src/services/publicWorkCode.ts b/src/services/publicWorkCode.ts index 2d33ae06..4cc1782d 100644 --- a/src/services/publicWorkCode.ts +++ b/src/services/publicWorkCode.ts @@ -29,6 +29,14 @@ export function buildMatch3DPublicWorkCode(profileId: string) { return `M3-${suffix}`; } +function buildLegacyMatch3DPublicWorkCode(profileId: string) { + const normalized = normalizePublicCodeText(profileId); + const fallback = normalized || '00000000'; + const suffix = fallback.slice(-8).padStart(8, '0'); + + return `M3D-${suffix}`; +} + export function buildSquareHolePublicWorkCode(profileId: string) { const normalized = normalizePublicCodeText(profileId); const fallback = normalized || '00000000'; @@ -134,6 +142,8 @@ export function isSameMatch3DPublicWorkCode(keyword: string, profileId: string) return ( normalizedKeyword === normalizePublicCodeText(buildMatch3DPublicWorkCode(profileId)) || + normalizedKeyword === + normalizePublicCodeText(buildLegacyMatch3DPublicWorkCode(profileId)) || normalizedKeyword === normalizePublicCodeText(profileId) ); }