[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CBFP4V12AHTDRGEKA3K6D4":3,"skill-same:01M6CBFP4V12AHTDRGEKA3K6D4":60},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":22,"publishedAt":25,"linkOnly":26,"markdownContent":27,"markdownBody":27,"frontMatter":28,"refName":29,"commitSha":30,"skillDirectory":31,"forksCount":13,"repositoryHtmlUrl":32,"repositoryHomepageUrl":6,"ownerHtmlUrl":33,"ownerBlogUrl":6,"ownerDescription":34,"updatedAt":35,"install":36,"resources":49},"01M6CBFP4V12AHTDRGEKA3K6D4","11-ui-scroll-frame-grid",null,"devwinsoft\u002Fdevian","devwinsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F22570160?v=4","01M6C6RR8W05NH8NPKQNCE3Q72","maoshy","01M6C6RRDJ5TJJCZXRTY801YDH",0,"MIT","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian-unity\u002F23-ui-package\u002F21-ui-scroll-system\u002F11-ui-scroll-frame-grid\u002FSKILL.md",[17],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",[23,24],"ui","11 ui scroll frame grid","2026-11-10 10:12:30",false,"# 11-ui-scroll-frame-grid\n\nStatus: ACTIVE\nAppliesTo: v3\n\n---\n\n## Overview\n\n`UIScrollGridFrame`는 `UIScrollContainer`가 계산한 logical row enter\u002Fexit 요청만 받아\nrow 내부 cell을 spawn\u002Fshow\u002Fhide\u002Fdespawn 하는 grid section renderer다.\nvirtualization owner는 아니다.\n\n현재 구현 원칙:\n\n- `UIScrollGridFrame`는 layout, pool, lifecycle bridge만 담당한다.\n- 실제 데이터 bind\u002Freset은 `UIScrollGridCell.onShow(int cellIndex)` \u002F `onHide()`가 담당한다.\n- scroll 쪽은 별도 init semantics를 만들지 않고 `UIBaseFrame._Init()` \u002F `_InitComplete()`를 재사용한다.\n\n---\n\n## SSOT\n\n### Code Path\n\n```text\nframework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FUIPackage\u002FRuntime\u002FScroll\u002F\n├── UIScrollGridFrame.cs\n└── UIScrollGridCell.cs\n```\n\n### 3-Path Mirror\n\n| 경로 | 역할 |\n|------|------|\n| `framework-cs\u002Fupm\u002Fcom.devian.foundation\u002FSamples~\u002FUIPackage\u002F` | UPM 정본 |\n| `framework-cs\u002Fapps\u002FUnityExample\u002FPackages\u002Fcom.devian.foundation\u002FSamples~\u002FUIPackage\u002F` | Packages mirror |\n| `framework-cs\u002Fapps\u002FUnityExample\u002FAssets\u002FSamples\u002FDevian Foundation\u002F{version}\u002FUIPackage\u002F` | Assets\u002FSamples 구현 |\n\n---\n\n## Public Model\n\n### UIScrollGridCell\n\n```csharp\nnamespace Devian\n{\n    [RequireComponent(typeof(RectTransform))]\n    public class UIScrollGridCell : UIBaseFrame, IPoolable\n    {\n        public int CellIndex { get; }\n\n        public void Show(int cellIndex);\n        public void Hide();\n\n        public void OnPoolSpawned();\n        public void OnPoolDespawned();\n\n        protected virtual void onInit();\n        protected virtual void onInitComplete();\n        protected virtual void onShow(int cellIndex);\n        protected virtual void onHide();\n        protected virtual void onPoolSpawned();\n        protected virtual void onPoolDespawned();\n    }\n}\n```\n\n의미:\n\n- `onInit()` \u002F `onInitComplete()` = semantic init\n- `onShow(int cellIndex)` = 현재 index 기준 full bind\n- `onHide()` = reset \u002F unbind\n- `CellIndex`는 현재 bind index다\n\n### UIScrollGridFrame\n\n```csharp\nnamespace Devian\n{\n    public class UIScrollGridFrame : UIBaseFrame, IUIScrollSection\n    {\n        public string CellPrefabName { get; set; }\n        public int ColumnCount { get; set; }\n        public int MinimumLineCount { get; set; }\n        public Vector2 CellSize { get; set; }\n        public float RowSpacing { get; set; }\n        public RectOffset Padding { get; set; }\n        public bool FillEmptyCells { get; set; }  \u002F\u002F 빈 칸 cell spawn 여부 (기본: false)\n\n        public int CellCount { get; }\n        public int DataRowCount { get; }\n        public int RowCount { get; }\n        public int RenderCellCount { get; }\n\n        public void SetCellCount(int count);\n        public void SetMinimumLineCount(int lineCount);\n        public bool HasDataAt(int cellIndex);\n\n        public UIScrollGridCell Spawn(...);\n        public T Spawn\u003CT>(...) where T : UIScrollGridCell;\n        public void Despawn\u003CT>(T cell) where T : UIScrollGridCell;\n\n        protected virtual UIScrollGridCell createCell(Transform parent);\n    }\n}\n```\n\n### Typed Alias\n\n```csharp\nnamespace Devian\n{\n    public abstract class UIScrollGridFrame\u003CTCell> : UIScrollGridFrame\n        where TCell : UIScrollGridCell\n    {\n    }\n}\n```\n\n`UIScrollGridFrame\u003CTCell>`는 타입 의미만 주는 thin wrapper다.\nbind hook을 추가하지 않는다.\n\n---\n\n## Init Semantics\n\n`UIScrollGridFrame`와 `UIScrollGridCell` 모두 별도 scroll init 시스템을 만들지 않는다.\n기존 `UIBaseFrame._Init()` \u002F `_InitComplete()`가 semantic boundary다.\n\n해석 규칙:\n\n- owner tree가 이미 initialized 상태면 cell spawn 직후 `cell._Init(canvas)`가 즉시 수행될 수 있다\n- owner frame이 이미 init complete 상태면 같은 경로에서 `cell._InitComplete()`도 즉시 수행될 수 있다\n- frame이 아직 init complete 전이면 cell은 `onInit()`까지만 먼저 수행되고, frame `onInitComplete()` 시점에 active cell들이 `_InitComplete()` 된다\n\n실무 규칙:\n\n- child UI 구조 생성 \u002F ref wiring은 `cell.onInit()`에 둔다\n- `cell.onInit()` 안에서 새 `UIComponentBase` \u002F `UIBaseFrame` subtree를 만들었다면 `InitDynamicSubtree(root)`를 호출해 기존 init 체계에 편입한다\n- 현재 데이터 bind는 `cell.onShow(int cellIndex)`에 둔다\n- 이전 bind 정리는 `cell.onHide()`에 둔다\n\n---\n\n## Runtime Flow\n\n### Row Bind\n\ninternal `IUIScrollSection.BindRow(...)` 흐름:\n\n1. row index 유효성 검사\n2. 이미 active면 기존 row unbind\n3. row마다 `ColumnCount`개 cell 생성\n4. `createCell(transform)` 호출 — cell은 frame의 child로 생성된다\n5. spawn 직후 `ensureCellInitialized()`로 `cell._Init(canvas)` \u002F 필요 시 `cell._InitComplete()` 연결\n6. anchor\u002Fpivot\u002Fsize 적용\n7. position은 frame 로컬좌표로 계산: `localMainPos = mainPos - sectionMainAxisPosition`\n8. `cell.Show(cellIndex)` 호출\n\n### Row Unbind\n\ninternal `IUIScrollSection.UnbindRow(...)` 흐름:\n\n1. active row lookup\n2. 각 cell에 대해 `Hide()` 후 `BundlePool.Despawn(cell)`\n\n### Row Refresh\n\nactive row만 처리한다.\n\n- `cell.Hide()`\n- `cell.Show(cellIndex)`\n\n즉 refresh에서도 `onHide()`가 reset 경계다.\n\n### Section Clear\n\n- 모든 active row cell을 hide\u002Fdespawn\n- `_activeRows.Clear()`\n- holder는 active 상태를 유지한다\n\n---\n\n## Layout Rules\n\n- `GetWidth()`는 `UIBaseFrame` base 구현 사용\n- `GetHeight()` =\n  - `padding.top + rowCount * cellSize.y + (rowCount - 1) * rowSpacing + padding.bottom`\n- X축 spacing은 frame width 기준 자동 계산:\n  - `(frameWidth - padding.left - padding.right - columnCount * cellSize.x) \u002F max(1, columnCount - 1)`\n- `left\u002Fright`를 제외한 내부 영역에서 cell 간격을 균등 분배한다\n- row 0은 `padding.top`, 마지막 row는 `padding.bottom`을 흡수한다\n- 마지막 row도 partial row가 아니라 full row renderer다\n- 데이터가 없는 slot은 placeholder로 남고, consumer는 `HasDataAt(cellIndex)`로 판정한다\n\n---\n\n## Pool Rules\n\n- `Spawn()`은 `_cellPrefabId` prefab의 실제 pool type을 해석한 뒤 그 concrete type으로 spawn한다\n- prefab 대표 `IPoolable` component는 반드시 `UIScrollGridCell` 하위 타입이어야 한다\n- `Spawn\u003CT>()`는 typed facade다. 실제 생성은 항상 prefab concrete type 기준으로 수행된다\n- `OnPoolSpawned()`는 `CellIndex = -1` 후 `_HandlePoolSpawned()` bridge를 호출한다\n- `OnPoolDespawned()`는 `CellIndex = -1` 후 `_HandlePoolDespawned()` bridge를 호출한다\n- pooled cell은 despawn 시 `UIBaseFrame._ResetForPool()`로 init state가 reset된다\n\n---\n\n## Recommended Usage\n\n### Frame\n\nframe subclass는 보통 layout과 count만 설정한다.\n\n```csharp\npublic sealed class HeroEquipGridFrame : UIScrollGridFrame\u003CHeroEquipGridCell>\n{\n    protected override void onInit()\n    {\n        SetCellCount(InventoryManager.Instance.EquippedItems.Count);\n    }\n}\n```\n\n### Cell\n\n실제 데이터 bind는 cell이 `cellIndex`로 직접 조회한다.\n\n```csharp\npublic sealed class HeroEquipGridCell : UIScrollGridCell\n{\n    protected override void onInit()\n    {\n        \u002F\u002F child UI 생성 \u002F ref wiring\n    }\n\n    protected override void onShow(int cellIndex)\n    {\n        var items = InventoryManager.Instance.EquippedItems;\n        if (cellIndex \u003C 0 || cellIndex >= items.Count)\n            return;\n\n        var item = items[cellIndex];\n        \u002F\u002F bind\n    }\n\n    protected override void onHide()\n    {\n        \u002F\u002F reset\n    }\n}\n```\n\n즉:\n\n- frame은 데이터 전달하지 않는다\n- cell이 `cellIndex`로 직접 조회한다\n- `onShow(int cellIndex)`만으로 bind를 해결한다\n\n---\n\n## Reference\n\n- Parent: `skills\u002Fdevian-unity\u002F23-ui-package\u002FSKILL.md`\n- Scroll container: `skills\u002Fdevian-unity\u002F23-ui-package\u002F21-ui-scroll-system\u002F10-ui-scroll-container\u002FSKILL.md`\n- Frame lifecycle base: `skills\u002Fdevian-unity\u002F23-ui-package\u002F10-base-system\u002F11-ui-canvas-system\u002FSKILL.md`\n",{},"main","8cb5bd877a3a7004867d2f55697cb9e4ff2f1734","skills\u002Fdevian-unity\u002F23-ui-package\u002F21-ui-scroll-system\u002F11-ui-scroll-frame-grid","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian","https:\u002F\u002Fgithub.com\u002Fdevwinsoft","Kim Hyong Joon","2026-04-12 18:44:10",{"source":7,"skillName":5,"commands":37,"prompt":47,"zipAvailable":48},[38,41,44],{"tool":39,"command":40},"npx","npx skills add devwinsoft\u002Fdevian --skill 11-ui-scroll-frame-grid",{"tool":42,"command":43},"bunx","bunx skills add devwinsoft\u002Fdevian --skill 11-ui-scroll-frame-grid",{"tool":45,"command":46},"pnpm","pnpm dlx skills add devwinsoft\u002Fdevian --skill 11-ui-scroll-frame-grid","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian-unity\u002F23-ui-package\u002F21-ui-scroll-system\u002F11-ui-scroll-frame-grid\u002FSKILL.md",true,[50,56],{"path":51,"resourceType":52,"resourceTypeDesc":53,"sizeBytes":54,"sourceHtmlUrl":55},"scripts\u002Fedit_music.py","SCRIPT","脚本",10476,"https:\u002F\u002Fgithub.com\u002FBlackBeltTechnology\u002Fpi-agent-dashboard\u002Fblob\u002Fdevelop\u002Fpackages\u002Fmusic-production\u002F.pi\u002Fskills\u002Fmusic-edit-to-length\u002Fscripts\u002Fedit_music.py",{"path":57,"resourceType":52,"resourceTypeDesc":53,"sizeBytes":58,"sourceHtmlUrl":59},"scripts\u002Fscore_joins.py",7019,"https:\u002F\u002Fgithub.com\u002FBlackBeltTechnology\u002Fpi-agent-dashboard\u002Fblob\u002Fdevelop\u002Fpackages\u002Fmusic-production\u002F.pi\u002Fskills\u002Fmusic-edit-to-length\u002Fscripts\u002Fscore_joins.py",[61,67,75,83,92,99],{"skillNo":62,"name":63,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":64,"categories":65,"keywords":66,"publishedAt":25},"01M6CBX3K2JJV38ANHVGCAT8A0","devian","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002FSKILL.md",[],[63],{"skillNo":68,"name":69,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":70,"categories":71,"keywords":72,"publishedAt":25},"01M6CBX210BFMM8AMN770J9WY8","90-project-archive","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F90-project-archive\u002FSKILL.md",[],[73,74],"archive","90 project archive",{"skillNo":76,"name":77,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":25},"01M6CBWZSGRR9R3YJZB3177667","30-env-management","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F30-env-management\u002FSKILL.md",[],[81,82],"management","30 env management",{"skillNo":84,"name":85,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":86,"categories":87,"keywords":88,"publishedAt":25},"01M6CBWYD3TGWBQV7V35551THF","19-page-push-send","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F19-page-push-send\u002FSKILL.md",[],[89,90,91],"push","page","19 page push send",{"skillNo":93,"name":94,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":95,"categories":96,"keywords":97,"publishedAt":25},"01M6CBWWRQ05A7604GHJKTPJTF","18-page-savedata","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F18-page-savedata\u002FSKILL.md",[],[90,98],"18 page savedata",{"skillNo":100,"name":101,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":102,"categories":103,"keywords":104,"publishedAt":25},"01M6CBWVH4S2VF1V7B2GBTESCT","17-page-deobfuscate","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F17-page-deobfuscate\u002FSKILL.md",[],[90,105],"17 page deobfuscate"]