본문으로 건너뛰기
SheetForge

기능 및 제한

이 페이지는 SheetForge가 하지 않는 것, 아직 할 수 없는 것, 또는 예상과 다르게 동작하는 것 전부를 나열한다. 이유, 우회 방법, 그리고 향후 개선 여지가 있는지를 함께 적는다.

항목별 형식: 무엇을(What) / 왜(Why) / 우회 방법(Workaround)(의미가 있는 경우 + 향후 개선 여지).


1. 플랫폼 및 의존성

Addressables는 필수다 — 없으면 파이프라인이 잠긴다

  • 무엇을: 주소 기반 로드가 런타임 경로이며 AssetRef@Group 타입에는 com.unity.addressables가 필요하므로, 에셋을 사용하려면 이 패키지를 설치해야 한다. 에셋 자체는 이 패키지 없이도 컴파일된다 — Addressables를 사용하는 모든 코드는 패키지가 있을 때만 켜지는 SHEETFORGE_ADDRESSABLES 버전 정의 뒤에 가드되어 있다.
  • 없을 때의 동작: 파이프라인 전체(임포트 · export · push · 작성 반영)는 잠기며, 저하되지 않는다 — 어떤 진입점을 실행하든 설치 안내를 표시하고 멈춘다. 부분적이거나 조용한 경로는 없다("에셋 키 검증 건너뛰기" 같은 폴백은 없다). Editor가 패키지 없이도 컴파일되므로, 결코 세이프 모드로 빠지지 않는다: 시작하기 창은 정상적으로 열리며, 그 Addressables 행은 ✗와 함께 Open Package Manager 버튼을 보여준다. 의존성 없는 SheetForge.Setup 부트스트랩은 다른, 무관한 컴파일 실패에 대한 안내 안전장치로 남아 있다.
  • 패키지 없이 데이터 스튜디오에서는: 에셋 셀의 ⊙ 피커 버튼과 드래그 앤 드롭 대상이 사유를 툴팁으로 단 채 비활성화되지만, 셀에 주소를 직접 입력하는 것은 여전히 동작한다.
  • 왜 Resources/Addressables 이중 추상화가 아닌가: 과도한 엔지니어링으로 판단되어 의도적으로 만들지 않았다.
  • 우회 방법: Addressables를 설치한다. Asset Store 임포트 프롬프트가 컴파일 이전에 이를 처리한다. 건너뛰었다면, 컴파일되는 Editor가 설치를 안내한다.
  • 검증: 두 분기 모두 실제로 실행해 확인한다 — 버전 정의를 제거한 상태("Addressables 부재" 시뮬레이션)에서 제품과 테스트 어셈블리는 에러 0건으로 컴파일된다. 복원한 상태에서는 에러 0건, 경고 0건이다. Addressables가 설치되지 않은 새 프로젝트에 에셋을 실제로 임포트하는 방식으로 종단 간 독립적으로도 검증했다: 프로젝트는 컴파일되며, 설계한 대로 설치 안내 창이 나타난다.

Unity Localization은 선택 사항이다 — StringTable 동기화만 기다린다

  • 무엇을: 현지화 시트(@loc), LocRef 참조, 키 상수, 커버리지 리포트, Export, Push, xlsx, 웹 앱 모두 com.unity.localization 없이도 동작한다. 기다리는 것은 오직 StringTable 동기화 출구뿐이다: 설치 안내(세션당 한 번)를 표시하고 멈춘다. 패키지를 건드리는 모든 코드는 SHEETFORGE_LOCALIZATION 버전 정의 뒤에 있으므로, 패키지 없이도 모든 어셈블리와 생성 코드 한 줄 한 줄이 컴파일된다 — 생성된 필드는 패키지 타입이 아니라 언제나 평범한 LocRef 구조체다.
  • 없을 때의 동작: 다른 어디에서도 저하되거나 조용히 건너뛰는 것이 없다 — 시트는 완전한 시트로 남는다; 오직 동기화 출구만 잠기며, 그 이유가 표시된다.
  • 지원 버전: 1.5 이상.
  • 우회 방법: 테이블을 원할 때 패키지를 설치하면, 그 이전에 작성된 모든 것이 다음 완료된 임포트에서 동기화된다. 현지화 시트 참고.

원클릭 프로그래밍 방식 설치 없음

  • 무엇을: 안전장치 창은 사용자를 안내할 뿐, 패키지 자체를 설치하지 않는다. Unity Localization 설치 안내에도 같은 규칙이 적용된다.
  • 왜: Asset Store 제출 규정이 프로그래밍 방식의 패키지 수정을 제한한다. 안내 창이 안전하고 규정을 준수하는 선택이다.

SHEETFORGE 제품 감지 define은 자동으로 제거되지 않는다

  • 무엇을: Editor 어셈블리는 모든 빌드 타깃에 SHEETFORGE 스크립팅 define 심볼을 자체 등록하여, 다른 에셋이 컴파일 타임에 SheetForge가 설치되어 있음을 감지할 수 있게 한다(플러그인 작성 ▸ 다른 에셋에서 SheetForge 감지하기 참고). 이 등록은 멱등적이다(없을 때만 추가되며, 이미 있는 경우 재컴파일이 발생하지 않는다).
  • 한계: 이후 에셋을 삭제하더라도 그 define은 남아 있는다 — 제거를 감지했을 코드가 함께 사라지기 때문이다.
  • 우회 방법: Project Settings ▸ Player ▸ Scripting Define Symbols(플랫폼별)에서 수동으로 제거한다. 심볼 하나를 정리하기 위해서만 백그라운드 감시자를 계속 실행하지는 않기로 의도적으로 선택했다. 이는 SHEETFORGE_ADDRESSABLES와는 별개다 — 후자는 Addressables 패키지가 있는지만 반영하는 내부 버전 정의다.

2. Google 시트 소스

ExportUrl 모드는 읽기 전용이다

  • 무엇을: ExportUrl 모드에서는 Push, 반영, 구조 편집, 삭제가 모두 비활성화된다.
  • 왜: 이는 인증되지 않은, 링크로 공유되는 export 경로이며 — 본질적으로 읽기 전용이다. Push는 항상 SheetsApi 자격 증명을 요구하며, 이는 어떤 네트워크 호출보다도 먼저 강제된다.
  • 우회 방법: 어떤 형태의 쓰기 반영에도 SheetsApi 모드(서비스 계정 — Google 시트 설정 참고)를 사용한다.

ExportUrl은 gid 맵이 필요하다

  • 무엇을: 빈 gid 맵은 임포트를 실패시킨다. 중복된 gid는 거부된다.
  • 왜: gid가 없는 export URL은 조용히 첫 번째 탭만 반환한다 — 조용한 손상 함정이므로, 임포트가 이를 거부한다. SheetsApi는 탭을 자동으로 발견한다.
  • 우회 방법: 각 탭의 #gid= 값을 등록하거나, SheetsApi를 사용한다.

Push는 키로 행을 삭제하며, 검증된 것만 삭제한다

  • 무엇을: 로컬에서 삭제된 레코드는 push 시 — 전송 전 재조회가 그 키가 임포트 당시 본 행에 여전히 놓여 있음을 확인한 뒤에 — 라이브 시트에서 제거된다. 이미 사라진 행은 완료로 친다(멱등적 재푸시); 다른 행에서 키가 발견되면 알림과 함께 건너뛰어지며, 결코 위치로 삭제되지 않는다. 삭제는 승인 요약의 자기 구획에 나열되며, 각 탭 안에서 아래에서 위로 마지막에 전송된다.
  • 왜: 라이브 시트를 상대로 키로 대조하는 것이 표류했을 수 있는 시트에서 삭제를 안전하게 만드는 방법이다; 대조가 확인할 수 없는 것은 그대로 둔다.
  • 한계: 행 삭제 능력이 없는 소스(그 능력을 갖춘 적 없는 커스텀 프로바이더)는 예전 방식으로 되돌아간다 — 삭제는 보고되고, 라이브 행은 사용자가 직접 제거하도록 남겨진다.

Push는 충돌하는 셀을 건너뛴다(설계상)

  • 무엇을: 임포트 이후 제삼자가 편집한 셀, 키가 모호하게 이동한 행, 사라진 행, 또는 중복된 라이브 키는 경고와 함께 건너뛰어지며 — 덮어써지지 않는다.
  • 왜: 이것이 바로 안전장치가 작동하는 방식이다: 전송되는 셀은 유효하며, 건너뛰기는 다른 사람의 변경 사항을 보호하고 잘못된 행에 쓰는 것을 방지한다.
  • 우회 방법: 리포트에서 적용/건너뜀 개수를 확인한다. 재임포트로 조정한 다음 다시 Push한다. (충돌 해결 UI는 별도의 기능이 될 것이며 — 계획되어 있지 않다.)

Push는 키 컬럼이 필요하다

  • 무엇을: RecordId 키 컬럼이 없는 변경된 탭이 있으면 Push될 수 없다 — 이는 계획 오류로 Push 전체를 차단한다(모든 탭에 대해 전송되는 것이 없음 — 부분 전송은 없다).
  • 왜: Push는 라이브 시트에서 행을 키로 재탐색한다. 키가 없으면 잘못된 행 가드가 성립할 수 없다.
  • 우회 방법: 키 컬럼을 추가하거나, 파일로 Export하여 붙여넣는다.

3. xlsx 소스

  • 탭 이름 변경은 xlsx 원본 탭을 제외한다 — 다중 시트 워크북 보호. 워크북에서 이름을 변경하고 재임포트한다.

  • xlsx 원본 탭으로의 키 이름 변경 전파는 전체 배치를 차단한다 — xlsx 경로는 정교한 셀 업데이트를 안전하게 수행할 수 없으며, 부분 반영은 결코 허용되지 않는다. 해당 탭을 직접 편집하고 재임포트한다.

  • 표현 불가능한 셀은 거부된다 — 캐시된 값이 없는 수식 셀, 에러 셀, 셀 안의 탭/개행. 내장 OOXML 리더는 의도적으로 최소한으로 만들어졌다(서드파티 코드 전혀 없음). 수식을 구체화하고, 리스트에는 ;를 사용하라.

  • 값만 다룬다 — 수식·날짜·서식은 정직하게 해석된다 — 수식 셀은 캐시된 값을 내놓으며(결코 재계산되지 않는다), 날짜 서식 셀은 yyyy-MM-dd 표시 텍스트로 읽히고, 그 외의 숫자 서식·병합된 셀·차트는 임포트되지 않는다. 웹 앱의 가져오기 대화상자는 실제로 일어난 일을 "이 워크북이 어떻게 읽혔는지" 메모에 명시한다; 에디터에서는 같은 정책이 셀마다 조용히 적용된다(위의 거부 사례들은 여전히 셀마다 보고된다).

  • 일부 export된 드롭다운 규칙은 담을 수 없다 — export는 하나의 워크북이므로, 참조 컬럼의 드롭다운은 대상 시트의 키 컬럼에 걸친 진짜 범위로 기록된다 — Google 규칙이 갖는 것과 같은 의미다. 그래도 세 가지 경우는 여전히 빠지며, 하나의 DropdownNotSupportedByFormat 경고에 함께 이름이 실린다:

    • 멤버에 콤마가 포함된 목록(인라인 구분자가 그것을 갈라놓을 것이다);
    • 형식의 255자 한도(따옴표 포함)를 넘는 인라인 목록;
    • 대상 탭이 워크북 안에 없는 범위.

    값 자체는 어느 쪽이든 온전히 export된다. 소스, 내보내기 및 Push 참고.

4. 작성 — 데이터 스튜디오

키 컬럼이 없는 탭은 신규 레코드를 받을 수 없으며, 값 편집이 앵커를 잃는다

  • 무엇을: RecordId 키 컬럼이 없는 탭도 정상적으로 임포트되고 표시되며, 구조 편집은 완전히 동작한다 — 컬럼과 마커의 추가·제거·이름 변경·순서 변경, 그리고 시트 수준의 이름 변경과 삭제까지. 얻을 수 없는 것은 신규 레코드뿐이다 — 키가 없는 레코드는 이름을 붙이거나 참조할 수 없기 때문이다:

    • 행 추가 컨트롤이 비활성화된다;
    • 참조 피커는 그곳에 생성하기를 거부한다("… 키 컬럼이 없어 신규 레코드를 생성할 수 없습니다");
    • 그곳에 쓰려는 캔버스나 인스펙터 액션은 아무 일도 하지 않는다.

    값 셀은 편집 가능하다 — 하지만 행을 주소화할 키가 없으므로, 그 편집은 오직 행의 위치에 대해서만 스테이징된다.

  • 왜: 스테이징된 편집은 보통 (tab, record key, field)로 논리적으로 주소화되며, 기록되기 직전에 시트에 대해 다시 해석된다. 이것이 편집이 재임포트, 행 순서 변경, 또는 누군가가 위에 행을 삽입하는 것을 견뎌내게 하는 힘이다. 키 컬럼이 없으면 그런 주소가 없으므로, 그 편집은 대신 행 번호에 고정된 채로 통과한다 — 이 안전망 밖에서다.

    그래서 다시 기록하기 전에 시트의 행이 아래에서 움직이면(재임포트, 또는 누군가 소스를 직접 편집), 위치로 고정된 편집이 잘못된 행에 떨어질 수 있다. 이런 편집은 짧은 배치로 스테이징하고 반영하라.

  • 우회 방법: RecordId 컬럼을 추가하고(구조 편집이 가능하므로 같은 창에서 할 수 있다) 반영하면, 탭은 논리적 앵커를 되찾아 완전히 작성 가능해진다. 키 없는 탭도 임포트하기에는 완전히 유효하다 — 이는 작성상의 한계이지, 스키마상의 한계가 아니다.

컬럼 이름 변경 / @type 변경은 참조하는 게임 코드를 깨뜨린다; 반영 후에는 되돌릴 수 없다

  • 무엇을: 생성된 필드의 이름/타입이 변경된다. 이를 참조하는 게임 코드는 수동으로 업데이트해야 한다. Ctrl+Z는 반영 이전에만 동작한다.
  • 왜: 강타입 — 필드는 생성된 스키마의 일부다. 컴파일 에러는 자동 체인의 안전 중단이 조치 가능한 문장과 함께 잡아낸다. 컬럼 은 완전히 보존된다(오직 마커 셀만 변경된다).
  • 우회 방법: 확인 대화상자가 먼저 경고한다. 코드를 업데이트하고 다음 임포트가 재개되도록 한다.

탭 이름 변경은 참조하는 게임 코드를 깨뜨린다; 반영 후에는 되돌릴 수 없다

위와 동일한 메커니즘이다 — 생성된 클래스 이름이 변경된다(FooDatabaseBarDatabase). 재임포트가 에셋 측 정리(이전 클래스, SO, 주소)를 모두 자동으로 처리한다.

상호(교환) 및 순환 탭 이름 변경이 지원된다

  • 무엇을: Alpha→Beta + Beta→Alpha(교환), 그리고 더 긴 순환(A→B→C→A)은 한 배치 안에서 스테이징되고 반영될 수 있다 — 어느 쪽을 먼저 스테이징해도 동작하며, 탭 바는 교환된 이름을 즉시 보여준다(WYSIWYG, undo 가능). UI 게이트는 최종 이름 집합의 고유성을 사용한다(오직 진짜 충돌 — 두 개의 이름 변경이 같은 이름을 대상으로 하는 경우 — 만 거부된다). 반영은 이를 엄격하게 강제한다.
  • 참조는 이름이 아니라 데이터(탭 정체성)를 따라간다: A↔B 교환 이후, RecordId@ARecordId@B로 원자적으로 재작성된다(단일 패스 — 결코 두 번 적용되지 않음). 그래서 이는 B로 이동한 동일한 데이터를 계속 가리킨다.
  • 로컬: 교환은 한 번의 쓰기 작업으로 두 파일의 내용을 교환한다. 다른 확장자에 걸쳐 이름을 재사용하는 체인은 오래된 확장자의 파일을 삭제하므로(경로 기반 삭제 가드), 재임포트는 결코 중복된 탭을 보지 않는다.
  • Google: 제목 변경은 위상학적으로 정렬되며 임시 제목(A→tmp, B→A, tmp→B)으로 어떤 순환이든 끊으므로, 라이브 시트는 결코 순간적인 중복 제목을 갖지 않는다. 시퀀스 중간에 제목 변경이 실패하면, 임시 이름으로 남은 탭은 복구 안내와 함께 보고된다.

Google 전용 한계: 서로를 참조하는 교환된 탭은 다시 연결되지 않는다

  • 무엇을: 교환된 두 탭이 서로를 참조할 때(탭 ARecordId@B 컬럼을 갖고 탭 BRecordId@A 컬럼을 갖는 경우), Google 경로는 그 자리에서의 제목 변경을 통해 내용을 보존하며 자신의 @type 셀은 재작성하지 않는다 — 따라서 그 상호 자기 참조는 Google에서는 다시 연결되지 않는다.
  • 왜: Google은 제목을 변경하여 탭 이름을 바꾼다(설계상 내용은 그대로다). 이름이 변경된 탭 자신의 그리드를 재작성하면 이것이 무너진다. 로컬 소스는 이름이 변경된 탭의 프로젝션을 재작성하므로, 로컬은 이를 완전히 처리한다. 제삼의 탭으로부터의 참조는 두 경로 모두에서 다시 연결된다.
  • 우회 방법: Google에서는, 상호 참조를 제삼의 탭을 경유하도록 하거나, 중간 이름을 거쳐 교환을 반영한다.

스테이징된 값 SO 오버레이에는 더 이상 버튼이 없다

  • 무엇을: "SO에 스테이징된 값 미리보기" 오버레이(EphemeralSoApply)는 워크벤치의 버튼이 구동했으며, 그 창은 사라졌다. 이 타입은 이를 원하는 도구를 위해 여전히 공개 API로 남아 있다. SO 인스펙터의 테스트 편집 토글이 런타임 수치를 시험해보는 일상적인 경우를 감당한다.
  • 직접 호출한다면 알아둘 한계: 이 오버레이는 — 배지와 함께 — (a) 대기 중/신규 컬럼과 (b) 파싱 실패 셀을 거부한다. 신규 은 지원된다. 실제 파싱+베이크 경로를 재사용하므로, 진실하게 계산할 수 없는 것은 거짓으로 꾸미는 대신 거부한다.
  • 우회 방법: 이는 어차피 미리보기일 뿐이었다. 실제 변경을 위해서는 정상적으로 반영한다. 재임포트는 항상 진실을 복원한다.

행이 외부에서 이름 변경되거나 삭제되거나 키가 충돌하면 스테이징된 편집이 격리된다

  • 무엇을: 스테이징과 반영 사이에 행이 외부에서 이름 변경되었거나, 외부에서 삭제되었거나, 키가 충돌한 스테이징된 편집은 반영에서 제외되고 "isolated"로 배지가 표시된다.
  • 왜: 그 논리적 주소는 다시 해석될 수 없다 — 하지만 조용히 버려지지도, 세션을 막도록 허용되지도 않는다.
  • 우회 방법: (확인 후) 개별적으로 버리고 다시 스테이징한다.

키 이름 변경 전파는 baseline 셀만을 대상으로 한다

  • 무엇을: 같은 배치에서 이전 키를 참조하며 방금 입력한 텍스트는 자동으로 재작성되지 않는다.
  • 왜: 사용자의 새 입력을 조용히 재작성하는 것은 금지되어 있다. 대신 사전 검증이 매달린 참조를 잡아낸다.
  • 우회 방법: 스테이징된 참조를 직접 고치거나, 이름 변경을 먼저 반영한다.

나머지 항목들은 모두 유일한 작성 창인 **데이터 스튜디오**에 관한 것이다. 더 오래된 워크벤치 창은 제거되었으며, 그것만이 제공하던 세 가지 기능은 먼저 스튜디오와 설정 인스펙터로 옮겨졌다 — 워크벤치에는 무슨 일이 있었나 참고.

검증에 실패한 시트는 편집을 위해 열린다 — 하지만 Export, Push, 빌드는 계속 차단된다

  • 무엇을: 소스를 완전히 읽었다면, 검증이 실패했더라도 그 시트는 baseline으로 저장되므로 스튜디오가 이를 열 수 있고 사용자가 그 자리에서 에러를 고칠 수 있다. 코드 생성과 베이크는 에러 개수가 완전히 사라질 때까지 실행되지 않으며, 그 상태인 동안에는 Export, 실제 시트로의 Push, 플레이어 빌드가 모두 거부되고 각각 그 이유를 알려준다.
  • 왜: 이 세 출구는 모두 마지막으로 성공적으로 베이크된 값과 더 새로운 시트를 결합한다. 지금 이를 실행하면 이미 누군가가 고친 셀 위에 오래된 값이 덧씌워질 것이다 — 조용한 롤백이다. 출구를 막는 것이 입구를 계속 열어 둘 수 있게 하는 방법이다.
  • 수정을 반영하면 한 번 묻는다: 격리된 시트에서는 쓰기 반영에 추가 확인이 표시된다. 사전 검증이 그곳에서는 강한 게이트일 수 없기 때문이다(그 시트는 이미 에러를 갖고 있다). 발견된 모든 것은 그 반영에서 경고로 보고되며, 자동 재임포트가 시트 전체를 다시 검증한다. 정상적인 시트는 영향받지 않는다 — 사전 검증은 여전히 기록을 거부한다.
  • 우회 방법: 보고된 모든 에러를 고치고 다시 pull한다. 이를 해제하는 그 한 곳 — 베이크까지 완주하는 실행 — 에서 차단이 스스로 풀린다.

데이터 스튜디오의 정렬과 필터는 표시 전용이다 — 활성화된 동안 행 순서 변경을 비활성화한다

  • 무엇을: 스튜디오의 시트별 정렬(모든 컬럼, 오름차순/내림차순, 프로젝트별로 유지됨)과 텍스트 필터는 표시 순서만 바꾼다. 거터는 실제 시트 행 번호를 유지하며, 둘 다 스테이징, 반영, push, export에 영향을 주지 않는다. 정렬이나 필터가 활성화된 동안에는 행 ▲▼ 순서 변경 도구가 툴팁과 함께 비활성화된다.
  • 왜: 뷰가 정렬되거나 필터링된 상태에서 "화면상의 이웃"으로 순서를 바꾸면 사용자가 볼 수 없는 줄 옆으로 행이 조용히 이동하게 된다. 실제 행 순서 변경은 구조 작업이다 — 먼저 정렬/필터를 해제하라.
  • 참고: "최신순 정렬"은 시트에 이를 인코딩하는 컬럼(예: IntId나 날짜 형태의 문자열 컬럼)이 있을 때만 존재한다 — 시트 자체는 타임스탬프를 저장하지 않는다.

키 이름 변경이 스테이징된 동안 데이터 스튜디오의 Problems는 초안 상태다

  • 무엇을: 키(RecordId) 셀에 스테이징된 편집이 있는 동안, Problems 패널은 draft 배지를 달며, 그 안의 미해결 참조 항목은 잘못된 경보일 수 있다.
  • 왜: 메모리 내 미리보기는 키 이름 변경 전파를 적용하지 않는다 — 이는 반영 시점에 모든 탭에 걸쳐 실행된다. 진단 정보를 숨기거나 전파를 거짓으로 꾸미는 대신, 창은 이름 변경이 기록될 때까지 목록이 초안이라고 알려준다.
  • 우회 방법: 이름 변경을 반영한(전파는 자신만의 확인 절차와 함께 실행된다) 다음, 새로고침된 목록을 읽는다.

테이블은 200행을 넘으면 행 가상화된다 — 알아둘 두 가지 엣지 포함

  • 무엇을: 200행을 넘으면 테이블은 화면에 보이는 창(윈도)에 대해서만 행 요소를 만들며(더해 오버스캔 열두 행), 위아래에 스페이서를 두어 진짜 전체 높이를 유지하므로 스크롤바가 거짓말을 하지 않는다. 창 경계를 넘어 스크롤하면 살아남은 행은 재사용하고 새로 들어온 행만 만든다. 브라우저 그리드도 동일한 임계값에서 동일하게 동작한다.

    두 가지 경우는 여전히 모든 것을 만든다. 200행 이하에서는 모든 행이 이전과 정확히 똑같이 만들어진다. 뷰포트 높이를 아예 물어볼 수 없는 테이블도 마찬가지다 — 창 밖에 서 있어서 레이아웃이 결코 도달하지 않는 경우 — 그곳에서는 "모든 것을 만든다"가 정직한 폴백이기 때문이다. 아직 레이아웃되지 않은 큰 테이블은 한 프레임을 기다린 다음, 모든 것을 만들었다가 버리는 대신 첫 페인트부터 창 방식으로 그려진다.

  • 편집 중인 행은 살아남는다 — 스크롤되어 화면 밖으로 나가더라도, 그래서 커서, 포커스, 입력한 내용이 유지된다. 이 유지 기능에는 거리 상한이 있으며, 그 너머에서는 열린 에디터가 무한정 이어지는 대신 커밋되고 blur된다. 이때 잃는 것은 없다 — 그 값은 이미 스테이징 세션 안에 있다.

  • 오직 엘리먼트 생성만 창 방식이다. 컬럼 폭 샘플링, 검색, 정렬, 좌표, 스테이징 오버레이는 여전히 모든 행을 고려한다. 화면에 있는 것만 본다면 각각 다른 답을 낼 것이기 때문이다. 그래서 매우 큰 시트로 전환해도 여전히 그 크기에 비례하는 작업이 이루어진다 — 더 이상 하지 않는 것은 수천 개의 위젯을 만드는 일뿐이다.

  • 브라우저에서는, 창 모드가 레이아웃에 맡기는 대신 컬럼을 직접 측정한다. 자동 레이아웃 폭은 창에 우연히 들어와 있는 행들로부터 계산될 것이므로, 스크롤할 때마다 컬럼이 흔들릴 것이다. 창 모드에서는 폭이 모든 행에 대한 데이터 기반 추정치에서 나온 다음 고정된다. 전체 렌더 모드(200행 이하)는 여전히 자동 레이아웃을 그대로 사용한다.

캔버스는 자신의 스크롤 범위 안에서만 팬(pan)하며, 순환을 나타내는 점선 와이어는 커질수록 더 성기어진다

  • 무엇을:

    • Ctrl/Cmd + 마우스 휠은 레코드 캔버스를 커서 아래 지점을 고정한 채로 25 %에서 200 % 사이로 확대/축소한다(중심 고정 확대/축소라면 보고 있던 카드가 화면 밖으로 밀려날 것이다). 캔버스 헤더의 퍼센트 표시는 100 %로 되돌아가는 버튼이다. 그냥 휠은 여전히 스크롤한다.
    • 가운데 마우스 버튼 — 또는 그것이 없는 하드웨어를 위한 Alt + 왼쪽 버튼 — 을 드래그하면 팬한다 — 누르고 있는 동안 커서가 잡기 동작을 표시한다. 왼쪽 드래그는 선택과 연결의 몫으로 남아 있으므로, "뷰 이동하기"까지 의미할 수는 없었다.
    • 이 창은 스크롤 뷰이므로 팬 범위는 곧 스크롤 범위다: 빈 공간으로 표류하는 대신 콘텐츠의 가장자리에서 멈추며, 콘텐츠가 뷰포트보다 작을 때는 전혀 움직이지 않는다. 이는 무한 캔버스가 아니다.
    • 순환을 표시하는 점선 와이어는 그릴 수 있는 대시 개수에 상한을 두고 긴 경로에서는 대시 주기를 두 배로 늘리므로, 매우 긴 루프는 더 선명해지는 대신 더 성기게 읽힌다.
  • 왜: Unity는 메쉬 정점을 드로우콜당 65,535개라는 확고한 상한으로 할당하며, 이를 넘으면 테셀레이션 비용은 여전히 치르면서 그림 자체는 통째로 사라진다. 대시 상한은 설계상 하나의 Stroke를 그 예산 안에 묶어 둔다.

    배경의 점 격자는 예전에는 같은 벼랑 위에 있었지만 더 이상 그렇지 않다. 이는 작게 반복되는 배경 타일이며, 정점 비용이 전혀 들지 않고 캔버스가 아무리 커져도 일정한 시간에 다시 그려진다. (경로로 그려지는 점 하나는 네 모서리가 아니라 실측 28개의 정점이 든다 — 그리기 폴백의 1,800점 예산 뒤에 있는 산수이며, 이 타일이 출시된 경로인 이유다.)

  • 우회 방법: 격자에는 필요 없다. 넓은 이웃을 볼 때는, 화면 하나에 모든 것을 욱여넣으려 하는 대신 축소하거나, 방향 세그먼트를 좁히거나, 이웃을 새로운 종착점으로 열어라.

아직 테이블이 없는 시트는 임포트되지 않고 건너뛰어진다

  • 무엇을: 세 가지 필수 마커 중 어느 것도 없고 데이터 행도 없는 탭 — 주석이나 @style 줄만 담은 갓 생성된 시트 — 은 세 마커 누락으로 임포트를 실패시키는 대신 EmptyTabSkipped 경고와 함께 건너뛰어진다. 이미 생성된 코드, 베이크된 에셋, 주소는 탭이 삭제된 것처럼 정리되지 않고 보존된다. Export와 Push도 대칭적으로 이를 건너뛴다 — 셋 모두 동일한 조건을 묻기 때문이다.
  • 왜: 미완성 시트 하나가 다른 모든 탭의 임포트를 막을 수 있어서는 안 되며, 작성자는 보통 헤더 행보다 시트를 먼저 만든다.
  • 경계: 절반만 작성된 시트(필수 마커가 하나라도 있음)는 건너뛰어지지 않는다 — 정직하게 실패한다. 조용히 건너뛰면 실제 작업을 숨기게 되기 때문이다. B열이 아니라 A열부터 타이핑된 시트도 마찬가지로 파서에 넘겨지므로, 그 진짜 진단("A열은 마커 컬럼이며 데이터는 B열부터 시작합니다")은 그대로 살아남는다.

플러그인 C#에서 등록된 enum은 시트로부터 멤버를 얻을 수 없다

  • 무엇을: 플러그인이 enums.Register<T>()로 등록한 T를 가진 Enum<T>는 코드가 소유한다. enum 정의 시트는 그 이름을 주장할 수 없으며(DuplicateEnumName), 셀 드롭다운의 "새 멤버 추가…" 행은 그런 컬럼에서는 그냥 없다.
  • 왜: 시트는 시트가 정의하는 것에 대해서만 정본이다. 더 이상 컴파일된 타입을 결정하지 않는 시트에 멤버를 써넣으면 코드에는 결코 나타나지 않는 멤버가 생겨날 것이다 — 제품이 지킬 수 없는 약속이다. 없는 행이 바로 UI가 이를 말하는 방식이다. 실패할 액션을 제공하는 대신.
  • 우회 방법: 시트가 소유해야 한다면 enum을 enum 시트로 옮기거나, 플러그인의 C#에 멤버를 추가하고 다시 컴파일한다.
  • 구조는 동일한 소유 경계를 따른다: enum 정의 시트의 구조는 두 호스트 모두에서 완전히 저작할 수 있다 — 정의, 이름 변경, 삭제, 컬럼 순서 변경, 기반 타입과 설명 편집까지 — 하지만 그 어느 것도 코드가 소유한 enum 이름을 건드릴 수 없으며, 정의 시트가 그 이름을 주장할 수도 없다. 거부에는 그 사유가 붙는다.

Enum 정의 시트: 멤버는 추가만 되며, 정렬은 없다

  • 무엇을: enum 시트의 구조는 에디터와 웹 앱 양쪽에서 그 자리에서 저작할 수 있지만, 멤버 행은 언제나 끝에 추가만 될 뿐 — 빈 자리가 다시 채워지는 일은 없으며 — 뷰는 정렬이나 필터를 제공하지 않는다.
  • 왜: 멤버의 위치가 그 정수 값이다. 빈 자리를 채우거나 멤버 순서를 바꾸면 이미 에셋에 베이크되고 세이브에 저장된 값들이 조용히 재번호화될 것이다. 반면 컬럼 순서는 아무 의미도 갖지 않으므로, 컬럼 순서 변경은 언제나 허용된다.
  • 우회 방법: 값을 명시적으로 고정하려면 Name=value 문법을 쓴다. 그 외의 표시 순서는 시트가 아니라 소비자의 몫이다.

시트에서 정의된 enum은 항상 설정 폴더에 생성된다

  • 무엇을: 생성된 타입은 이미 있는 폴더 어디든 그 자리에서 재생성되지만, enum 파일(SheetForgeEnums.cs)은 앵커로 삼을 탭이 없으므로 항상 설정에 지정된 생성 코드 폴더에 기록된다. 생성된 코드가 자신만의 패키지 폴더에 있는 탭이 시트에서 정의된 enum을 사용하면, 그 패키지 어셈블리는 **CS0246**으로 컴파일에 실패한다.
  • 왜: 시트에서 정의된 모든 enum은 파일 하나에 담긴다 — enum은 탭별 산출물이 아니라 프로젝트 수준의 산출물이기 때문이다 — 그래서 따라갈 단일 탭의 거처가 없다.
  • 우회 방법: 두 생성 폴더를 하나의 어셈블리에 두거나, 그 enum을 대신 플러그인 코드에서 등록한다. 실패는 누락된 타입 이름이 명시된, 눈에 보이는 컴파일 에러이지 결코 조용한 손상이 아니다.

타입 지정 에셋 참조는 로드된 타입을 대상으로 해석된다 — 고유할 때만 짧은 이름, 미리 정의된 어셈블리의 타입은 불가

  • 무엇을: AssetRef@Group<Type>은 프로젝트가 로드할 수 있는 UnityEngine.Object 파생 에셋 타입이면 무엇이든 받아들인다, 엔진 타입이든 직접 만든 타입이든, 허용 목록 없이. 세 가지는 추측 대신 거부된다: 여러 로드된 타입이 공유하는 짧은 이름(AmbiguousAssetType — 설치된 패키지에 따라 TextAsset이 그런 예가 될 수 있다)은 전체 이름(UnityEngine.TextAsset)으로 적어야 한다; 알 수 없는 이름은 최근접 일치 제안이 딸린 UnknownAssetType이다; 그리고 미리 정의된 어셈블리(Assembly-CSharp와 그 형제들 — 어셈블리 정의가 없는 모든 스크립트 폴더)에 있는 타입은 AssetTypeNotReferenceable이다.
  • 왜: 생성된 컴패니언 어셈블리는 어셈블리 정의이며, 어셈블리 정의는 미리 정의된 어셈블리를 참조할 수 없다 — 그런 T에 대한 AssetReferenceT<T>는 컴파일되지 않을 것이다. 모호한 이름을 하나 골라서 해석하면 컬럼을 조용히 잘못된 타입에 묶어 버릴 것이다.
  • 우회 방법: 그 타입을 어셈블리 정의 안으로 옮기거나, <…> 제한을 빼고 제한 없는 AssetRef@Group을 유지한다. 컴포넌트와 에디터 전용 타입은 결코 후보가 아니다.
  • 또한: 브라우저는 타입 이름을 해석하지 않는다(해석할 대상 프로젝트가 없다): 웹 앱은 <Type>을 파싱해 컬럼 툴팁에 보여주지만, 세 가지 타입 이름 진단 중 어느 것도 만들지 않으며 피커나 드롭도 제공하지 않는다. 코드젠은 해석되지 않은 이름을 그대로 내보내는 법이 없다 — 해석할 수 없는 이름은 AssetReference로 폴백하며 AssetTypeUnresolvedFallback 경고가 붙는다.

에셋 피커, 드롭, 스테이징된 등록 — 무엇이 자동이고 무엇이 아닌가

  • 무엇을: 에셋을 드롭하거나 고르면 즉시 셀에 주소가 기록되고 반영을 위해 Addressables 변경(추가 · 이동 · 그룹 생성)이 스테이징된다; 그 변경은 시트 기록이 성공한 뒤에만 실행된다 — 또는, 등록만이 유일하게 스테이징된 것일 때는 단독으로 실행된 다음 자동 재임포트가 뒤따른다; 탭이 워크북 기반이라 기록할 수 없었던 반영은 이를 계속 스테이징된 채로 둔다. 더 이상 아무것도 참조하지 않는 등록은 셀을 다시 입력하면 사라지며, 반영까지 살아남은 등록 중 더 이상 참조되지 않는 것은 건너뛰어진다; 이미 그 그룹에 있는 에셋은 기존 주소를 유지한다; 다른 그룹에 있는 에셋은 그것을 참조하는 다른 셀들의 이름을 알려주는 확인을 거친 뒤에만 이동한다. 자동 주소는 확장자를 뺀 파일 이름이며, 그 그룹의 다른 에셋이 이미 쓰고 있는 주소는 이름을 바꾸는 대신 거부된다. 새 그룹은 기본 BundledAssetGroupSchemaContentUpdateGroupSchema를 받는다. 적용되거나 건너뛰어진 항목은 사유와 함께 콘솔에 기록되며, 로컬 폴더 소스에서는 반영의 완료 대화상자가 그 요약(Addressables: N registered, M skipped)을 반복해서 보여준다.
  • 왜: 시트가 정본이다 — 시트에 도달하지 못한 반영에 대해 프로젝트는 결코 바뀌어서는 안 되며, 어떤 셀도 가리키지 않는 항목은 시트가 설명하지 못하는 고아가 될 것이다.
  • 우회 방법: 등록이 건너뛰어졌다면, 다음 재임포트가 그 셀을 UnknownAssetKey로 보고한다; 원인을 고치고 다시 반영한다. 등록에는 항상 Addressables 패키지가 필요하다.

서브 에셋은 parent[sub]로 주소가 지정되며, Sprite 모드의 텍스처는 <Sprite>를 통과한다

  • 무엇을: 서브 오브젝트 항목(텍스처 안의 스프라이트, 폰트 안의 머티리얼)은 Addressables가 이름 붙이는 방식대로 — parent[sub]로 — 주소가 지정되며, 그 키는 서브 오브젝트 자신의 타입에 대해 검사된다. 부모 주소는 자신의 타입 자신이 담은 모든 서브 에셋 타입을 함께 만족하며, 이것이 Sprite 모드로 임포트된 텍스처가 <Sprite> 컬럼을 통과하게 하는 이유다. 서브 에셋을 드롭하면 등록을 위해 부모를 스테이징하고 셀에는 parent[sub]를 기록한다.
  • 경계: parent[sub]가 검증되려면 서브 오브젝트 항목이 Addressables 카탈로그에 존재해야 한다; 피커는 알고 있는 서브 키를 그 부모 뒤에 나열한다.

색상에는 HDR이 없고, 커브 탄젠트는 자신의 모드를 따르며, 그라디언트는 양자화된다 — 의도된 설계

  • 무엇을: Color는 네 바이트다 — 1을 넘는 채널(HDR)은 Export 시 0…1로 클램프된다. 쪽이 Auto, Linear, Constant, ClampedAutoAnimationCurve 탄젠트는 임포트 시점에 모드로부터 다시 계산되므로, 자신의 모드와 모순되는 손 입력 숫자는 교체된다(모드가 적용될 때 Unity가 수행하는 것과 동일한 재계산이다); OnceClampForever로 읽히며 다시 기록되는 일은 결코 없다; 키가 없는 커브는 텍스트 형태가 없으며 선택적 컬럼의 빈 셀로만 존재한다. Gradient 키의 시간은 임포트 시 16비트로 양자화되며(Unity가 저장하는 방식과 정확히 같다), 키 하나짜리 그라디언트는 Unity로부터 동일한 키 두 개로 돌아오고, 색 공간은 설정되었을 때만 기록된다.
  • 왜: 시트가 보여주는 값은 반드시 엔진이 갖는 값과 같아야 하므로, Unity가 나중에 수행했을 정규화를 입구에서 한 번에 수행한다 — 그래서 모든 표면 — 시트, 에디터 필드, 웹 미리보기, 베이크된 에셋 — 이 하나의 커브와 하나의 그라디언트를 보여준다.
  • 우회 방법: 탄젠트 숫자를 있는 그대로 쓰고 싶다면 Free/Free를 사용한다; HDR 강도는 별도의 float 컬럼에 저장한다.

칩 에디터와 네이티브 필드는 세 가지 시각적 타입에만 존재한다

  • 무엇을: 데이터 스튜디오는 Color, AnimationCurve, Gradient 스칼라를 Unity 자신의 필드로, 그 List<>는 칩 에디터로 보여준다; 웹 앱은 자신의 에디터와 칩 목록이 딸린 미리보기를 보여준다. 그 외의 모든 리스트 컬럼 — List<int>, List<Enum<…>>, 래퍼 리스트 — 은 두 호스트 모두에서 정본 텍스트로 남으며, 셋 중 하나를 담은 래퍼(Pair<Color>)도 텍스트다.
  • 왜: 이 세 타입은 요소마다 그림이 있는 타입들이다; 나머지는 하나의 정본 줄이 이미 가장 정확한 표현이며, 래퍼의 외부 표기법은 그 플러그인이 소유한다.
  • 여지: 세 값 중 하나를 저장하는 플러그인 타입은 해당하는 StudioCellEditorHint 아키타입을 선언해 동일한 에디터를 그대로 쓸 수 있다(플러그인 작성 §4.16 참고).

소유권 색상 구분은 시트 대 코드로만 나뉜다

  • 무엇을: 사이드바/범례는 정확히 두 가지 출처 — 실제 시트 탭과 코드 레지스트리 가상 탭 — 만을 구분한다. 세 번째 "생성됨" 분류는 없으며, 컬럼별 소유권 색상 구분도 없다.
  • 왜: 출처는 탭 자신으로부터 도출되며, 창은 이를 이미 확실하게 알고 있다. 컬럼별 분류는 이를 진실하게 만들기 위한 또 다른 확장 지점을 필요로 할 것이고, 이를 요청한 소비자도 없었다.
  • 시트의 색상과는 다른 것: @style은 시트가 자신의 색상을 스스로 이름 붙이게 하며, 그 색상은 작성자가 선택한 표시 메타데이터다 — 시트가 어디서 왔는지에 대해서는 아무것도 말하지 않는다. 두 색상 구분은 서로 다른 곳에서 읽히며 결코 합쳐지지 않는다.

참조 드롭다운은 소속을 답할 뿐, 순서를 답하지 않는다

  • 무엇을: RecordId@Tab 셀은 이제 검색 가능한 드롭다운을 갖고 있고(List<>에는 체크리스트도), 리스트 셀의 드롭다운은 요소를 추가하고 제거할 뿐 — 하나를 이동시킬 수는 없다. 리스트 순서 변경은 캔버스에서 이루어지며, 그곳에서는 각 요소가 자신만의 행을 갖는다.
  • 왜: 드롭다운은 "여기에 무엇이 있는가"에 답한다. "어느 위치인가"에는 사물을 나란히 줄 세우는 표면이 필요하며, 캔버스가 이미 그것이다. 이를 두 곳에 중복시키면 유지해야 할 답이 두 개가 될 것이다.
  • 또한: 이 드롭다운은 평범한, 래퍼가 아닌 참조 컬럼에만 붙는다. 래퍼 셀의 텍스트는 그 래퍼 자신의 표기법을 담고 있으므로, 그 안에 순수한 키를 붙여넣으면 값이 망가질 것이다 — 래퍼 내부에 손을 대는 것은 등록된 셀 위젯의 몫이다(플러그인 작성 참고).

All 검색은 에디터에서는 베이크된 데이터를, 브라우저에서는 라이브 세션을 읽는다

  • 무엇을: 데이터 스튜디오의 All 항목은 베이크된 데이터베이스를 검색하므로, 먼저 성공한 임포트가 한 번 필요하며 임포트가 완료되거나 활성 설정이 바뀌면 스스로 갱신된다. 웹 앱의 All 항목은 스테이징된 편집을 포함해 세션이 지금 보여주는 값을 검색한다. 매칭, 결과 순서, 페이지당 50행은 둘 다 동일한 코드이며, 둘 다 결과를 더블클릭하거나(또는 선택한 뒤 Enter를 눌러) 그 시트를 일치한 셀이 선택된 채로 연다.
  • 왜: 브라우저에는 베이크된 ScriptableObject가 없다 — 있는 것은 라이브 세션뿐이며, 화면에 있는 값으로 답하는 것이 바로 브라우저 세션이 하는 일이다. 에디터는 이미 가진 베이크된 진실을 계속 읽는다.
  • 경계: 에디터에서는 스테이징만 되고 아직 반영되지 않은 편집은 임포트가 실행될 때까지 All에서 찾을 수 없으며, 세션이 아직 로드하지 않은 시트에서 나온 결과는 이동하지 않는다 — 목록 아래의 안내가 그 이유를 알려준다. 브라우저에서는 스테이징된 편집이 즉시 찾아지며 모든 결과로 이동할 수 있다.

5. 성능

  • 정상 경로는 선형적이며 빠르다: 50,000행 × 20컬럼 ≈ 628 ms(라이브 에디터, Mono; 헤드리스는 144 ms), 180k개의 참조 셀을 가진 50개 탭 × 2,000행 ≈ 294 ms. 일반적인 프로젝트 규모에서는 문제가 되지 않는다.
  • 메모리는 선형적이지만 박싱 비중이 크다: 셀당 유지되는 메모리는 ≈ 59 bytes다(임포트 중 피크 ≈ 138). Google 시트의 셀 상한(~10M 셀)에서 이를 외삽하면 ~6.3 s 임포트, ~590 MB 유지, ~1.4 GB 피크가 된다 — 극단적인 규모에서는 저사양/32비트 환경에 유의해야 한다. (컬럼 지향 IR은 인정된 백로그 항목이다.)
  • 작성 테이블은 200행을 넘으면 에디터와 브라우저 양쪽에서 행 가상화된다 — 그래서 큰 시트를 열어도 더 이상 행마다 위젯을 만들지 않는다. 창 방식이 아닌 것은 화면에서만 본다면 다른 답을 낼 행별 로직이다 — 폭 샘플링, 검색, 정렬, 좌표, 스테이징 오버레이. 세부 사항과 두 가지 엣지는 §4에 있다.
  • 에러 경로도 선형적이다: 참조가 대량으로 깨지는 경우에도, 최근접 제안 계산은 범위 내에 머문다 — 필드당 제안 예산과 길이 사전 필터링을 적용한, 조기 종료되는 편집 거리 계산이 깨진 참조 개수에 대략 선형으로 유지되게 한다(깨진 참조 4,000건 기준 헤드리스에서 ≈ 45 ms; 동일 규모의 유효한 데이터는 ≈ 2.7 ms). 참조된 탭의 이름을 바꾸는 것은 애초에 참조를 대량으로 깨뜨리지 않는다 — 이름 변경이 참조하는 @type 셀을 재작성한다.

6. 데모 씬

  • 샘플들은 선택적 임포트다 — 두 데모 샘플과 그 씬은 기본적으로 존재하지 않는다. 이들은 Unity 패키지(Assets/SheetForge/Examples/SheetForgePluginDemo.unitypackageSheetForgeCoreDemo.unitypackage)로 제공된다. 이 중 하나를 임포트하면(더블클릭, 또는 시작하기 창의 Import Plugin/Core Demo 버튼) Assets/SheetForge.PluginDemo/… 또는 Assets/SheetForge.CoreDemo/…가 복원된다. 그전까지 샘플은 사용자의 프로젝트 안에 아예 존재하지 않는다 — 이들은 오직 그 패키지들로만 제공된다 — 그래서 사용자의 프로젝트와 결코 충돌할 수 없다 — 핵심 제품은 이것들 없이도 완전히 자급자족한다.
  • 먼저 임포트 한 번이 필요하다(머신당) — 이것이 로드하는 Addressables 주소는 커밋되지 않는 캐시다. 그전에는 안내 메시지를 보여준다.
  • 데모에는 네임스페이스 설정이 필요 없다 — 커밋된 예제 타입은 Example* 클래스 접두사를 가진 기본 SheetForge.Generated 네임스페이스를 사용하므로, generatedNamespace 설정 없이도 데모를 재임포트하면 그 자리에서 재생성된다.

7. 플러그인 확장 — 출시된 이음매(경계 포함)와 여전히 예약된 것

열여섯 가지 확장 계약이 출시되어 있으며, 그 하나하나가 Core 수정 없이 합류한다 — 전체 목록은 플러그인 작성에 있다. 열여섯 가지 모두 발견을 통해(Unity의 TypeCache; 브라우저의 업로드된 어셈블리 스캔) 찾아지며, 어셈블리 참조도 편집할 매니페스트도 필요 없다. 열한 개의 Core 계약은 자신이 추가하는 것을 등록할 레지스트리를 건네받으며, 다섯 개의 Editor 계약 — 그래프 위젯, 인스펙터 액션, 셀 에디터 프로바이더, 패널 프로바이더, 소스 프로바이더 — 는 그저 발견되어 있는 그대로 사용된다.

출시된 이음매 중 다섯 개는 그냥 발견하도록 두기보다 여기서 명시할 가치가 있는 경계를 갖는다:

커스텀 셀 타입 참조하기 — 자신만의 표기법에 대한 완전한 RecordId@Tab 등가성

  • 무엇을: IReferencingCellType도 함께 구현한(그리고 그 값이 IRefBearingValue를 구현한) 등록된 셀 파서는, 자신의 표기법 안에 묻힌 키를 어떻게 읽고 재작성할지 Core에게 알려준다. 그러면 그 컬럼은 내장 참조가 얻는 모든 것을 얻는다:

    • 최근접 일치 제안이 딸린 무결성 검사;
    • 페이로드를 보존하는 키 이름 변경 전파(attack:add:10power:add:10);
    • 그래프 엣지와 포트, 피커, 역참조 개수;
    • 고아 탐지와 export되는 드롭다운 규칙.

    발견은 이미 등록된 파서를 캐스팅하는 것뿐이다 — 새로운 등록 채널은 없으며, 이를 구현하지 않는 커스텀 타입은 비트 단위로 변화가 없다. 플러그인 작성 §4.4a 참고.

  • 경계: 페이로드는 ;를 포함할 수 없다 — Core는 사용자의 파서가 텍스트를 보기도 전에 리스트 셀을 요소로 분리하므로, 값 안의 세미콜론은 두 요소로 조각날 것이다(래퍼 타입이 갖는 것과 동일한 제약). 그리고 @target실제 시트 탭을 가리켜야 한다: 코드 레지스트리의 가상 탭은 RecordId@Tab과 정확히 동일하게 UnknownTargetTab으로 거부된다. 이 제약이 있기에 Core 자신의 미해결 참조 보고와 이름 변경 전파를 수정 없이 그대로 적용할 수 있다.

<> 래퍼 타입(MyWrapper<T>) — 거부 규칙 포함

  • 무엇을: 플러그인은 ICellWrapperType을 통해 제네릭 값 형태(예: Pair<int> = 1~2)를 등록한다. Core는 내부 타입을 재귀적으로 해석한다(시트 문법 참고).

  • 거부 규칙:

    • Pair<List<T>>는 거부된다 — 리스트는 래퍼 안에 들어갈 수 없다. List는 항상 평평하고 최외곽에 있어야 한다.
    • Pair<int>@Tab은 거부된다 — @는 내부 리프에 붙여야 한다: Pair<RecordId@Tab>.
    • Pair<int?> / Pair<int=1>은 거부된다 — 선택성/기본값은 필드 수준이며 내부 타입의 일부가 아니다.

    List<Pair<T>> 허용되지만, 래퍼 자체의 구분자는 ;(리스트 구분자)와 달라야 한다 — 이는 Core가 강제할 수 없는, 플러그인 작성자의 책임이다.

커스텀 구조 마커(@yourMarker) — 컬럼 수준 메타데이터 전용

  • 무엇을: 플러그인은 IStructuralMarkerDefinition / ISheetForgeMarkerPlugin을 통해 @marker 행을 등록하며, 이는 @overlap의 컬럼별 검증을 일반화한 것이다. 그 값은 FieldSchema.MarkerValues 메타데이터로 저장된다.
  • 경계: 마커는 오직 자신의 컬럼별 값 검증만을 소유한다 — 행 전체 데이터 형태의 파싱을 넘겨받지 않는다(정규화가 여전히 데이터 형태를 표현하는 방법이다). 그리고 코드젠은 마커 값을 베이크하지 않는다: @overlap처럼, 이들은 검증/표시용 메타데이터일 뿐이며 스키마 핑거프린트에는 보이지 않는다 — 따라서 마커와 관련된 그 무엇도 생성 코드나 베이크된 SO에 도달하지 않는다.

컬러 프리셋 — 우리가 칠하는 표면이지, Unity의 위젯이 아니다

  • 무엇을: 플러그인은 ISheetForgeThemePlugin / ThemeRegistry를 통해 컬러 프리셋을 등록한다. 이는 Preferences ▸ SheetForge ▸ Theme에서 내장 DefaultHigh contrast 프리셋 옆에 나타나며, 사용자가 그것을 선택했을 때만 적용된다(등록 자체가 화면을 가로채는 일은 결코 없다). 프리셋은 자신이 이름 붙인 슬롯만 오버라이드한다 — 그 외의 모든 슬롯은 제품 기본값을 유지하므로, 슬롯이 추가되어도 프리셋은 계속 유효하다.
  • 경계 — 혼합된 크롬은 예상된 것이다: 테마는 SheetForge 자신이 그리는 것(창 배경, 헤더, 텍스트, 강조색, 그리드와 스테이징 색상)을 다룬다. 그 창 안에 그려지는 네이티브 Unity 위젯 — 버튼 크롬, 필드 테두리, 팝업 화살표 — 은 계속 에디터 스킨을 따르며, Unity는 패키지가 이를 재스타일링하도록 허용하지 않는다. 그래서 에디터가 다크 스킨으로 실행되는 동안 Always light를 고르면, 다크 네이티브 위젯 위에 라이트 SheetForge 표면이 놓이게 된다. 통일된 모습을 원한다면 에디터 스킨을 맞춰라.
  • 경계 — 색상만 해당: 프리셋은 색상(슬롯당 0xRRGGBB)을 담는다. 간격, 글꼴 크기, 레이아웃은 테마 대상이 아니며, 반투명 채우기(배지 배경, 모달 스크림)는 별도로 설정 가능하지 않고 슬롯 색상에 고정된 알파를 더해 파생된다.

선언적 작성 표면 — 의도적으로 한계가 있는 어휘

  • 무엇을: ISheetForgeStudioPlugin은 팩이 동사, 패널, 컬럼 배지, 셀 에디터 형태를 데이터로서 서술하게 하므로, 하나의 등록이 에디터와 브라우저 양쪽에서 똑같이 그려진다. 어휘는 고정되어 있으며 오직 덧붙여서만 자란다: 다섯 가지 액션 배치, 열세 가지 노드 종류, 일곱 가지 셀 에디터 아키타입(가장 최근 둘인 CurveEditorGradientEditor는 내장 커브·그라디언트 타입이 쓰는 것들이다).
  • 경계 — UI 프레임워크가 아니다. 임의의 렌더링, 복합 입력, 다단계 흐름에는 여기 어휘가 없으며, 이를 추가하는 것은 영원히 소형 UI 툴킷을 유지보수하는 셈이 될 것이다. IStudioPanelProvider가 바로 그것을 위한 것이다: 서술형 패널과 동일한 id로 이를 등록하면 에디터는 리치 쪽을 그리고 브라우저는 서술형 쪽을 그린다. 웹 전용 탈출구는 없다 — 브라우저는 UIToolkit 타입을 로드할 수 없으며, 그렇지 않은 척하는 것은 플러그인의 확장을 한쪽 화면에만 놓는 셈이 될 것이다.
  • 경계 — 액션의 힘은 정확히 네 가지다: 셀 하나 스테이징, 여러 셀을 하나의 Undo 단계로 스테이징, 레코드에 초점 맞추기, 다시 그리기 요청. 그래서 플러그인의 동사는 손으로 입력한 것과 동일한 게이트, 사전 검증, push를 거치는 평범한 스테이징된 편집이다. 작성 세션 자체는 의도적으로 노출되지 않는다.
  • 관찰자는 거짓 발생 없이, 무발생만 있다: IPipelineObserver는 명시적 임포트 사이클의 끝에서 발생한다. 그 지점에 결코 도달하지 못하는 두 경로가 있다 — 파이프라인이 시작되기 전에 멈춘 실행(활성 설정 없음; Addressables 미설치)과 컴파일 에러로 중단되는 codegen→컴파일 단계. "임포트가 시도되었다"는 사실이 필요하다면, 이를 에디터 측 ImportEvents 버스와 함께 사용하라.

여전히 설계만 되어 있고 구축되지 않은 것(아직 소비자 없음)

  • 행 전체 데이터 형태 마커(예: 2차원 행렬을 단일 필드로 읽는 마커 하나) — 의도적으로 만들지 않았다: 마커는 행 파싱이 아니라 컬럼별 검증을 소유하며, 정규화(참조 + type 컬럼 + List<T>)는 표현력 면에서 완전하다. MarkerRegistry 등록 이음매 자체는 출시되어 있으며, 오직 이 형태 파싱 해석만이 예약되어 있다.
  • 커스텀 마커 값을 생성 코드로 베이크하는 것 — 소비자가 마커 메타데이터를 코드젠 상수/애트리뷰트로 필요로 하기 전까지는 범위 밖이다.
  • 레코드별 / 지연 로드 SO 컨테이너 — 설계는 완료되었으나 구축되지 않음. 현재 코드젠은 오직 탭별 전체 로드 Database SO만 생성한다.

8. 스키마 진화

  • Export/Push는 스키마 변경 이후 새로운 베이크를 요구한다 — 오래된 베이크는 ExportSchemaMismatch(핑거프린트 불일치)로 실패한다. 이제 거절 화면이 그 임포트를 대신 실행해 주겠다고 제안한다: 확인 한 번이면 시작되며, 그 뒤에 자동으로 export되거나 push되는 것은 없다 — 임포트가 끝나면 원래 하려던 동작을 다시 누르면 된다.
  • 스키마 변경 이후의 첫 임포트는 내부적으로 두 단계다(코드젠 → 컴파일 → 베이크) — 자동이며 사용자 조작은 한 번뿐이다. 오직 컴파일 실패만이 이를 멈춘다(안전 중단, 조치 가능한 문장, 시도 횟수 제한 3회).

9. 현지화 범위

리포트의 세부 조각(문제가 된 값, 제안), 저수준 예외, 개발자 로그는 현지화된 리포트 뼈대 안에서 인라인 영어로 남는다 — 런타임에 보간되는 내용은 언어 테이블 키가 될 수 없다(업계 표준 경계). 에디터가 그리는 모든 것과 리포트 뼈대, 왜/어떻게 문장은 10개 언어 모두에서 완전히 현지화되어 있다.

열 개 언어 전량이 완전히 번역되어 있다. 모든 언어 테이블의 모든 키가 실제 번역을 담고 있다 — 데이터 스튜디오, 테마 설정, 대화상자, 로그 문구까지 포함해서다. 열 개 파일 사이의 키 패리티는 테스트로 강제되므로, 원문 키로 폴백하거나 플레이스홀더가 깨지는 일은 없다.

언어마다 소수의 항목은 영어판과 완전히 똑같이 읽히는데, 이는 대기 중인 번역이 아니라 번역상의 결정이다: 기호와 플레이스홀더 전용 문자열(, +), 고유명사와 포맷 이름(Google Sheets, SHA-256), 그리고 해당 언어에서도 실제로 영어와 철자가 같은 단어(OK, Alpha)가 그것이다.

플러그인 자신의 라벨은 애초에 Core 테이블에 전혀 없다: 사용자의 언어를 따르게 하려면 ISheetForgeStringsPlugin으로 등록하고, 그렇지 않으면 등록되지 않은 채로 남아 그대로 표시된다.

10. 에디터 상호작용

  • Ctrl+Z 범위: 포커스된 텍스트 필드가 Ctrl+Z를 먼저 소비한다(OS 표준). 반영이 성공한 이후에는 스테이징 기록이 지워진다 — undo는 이미 시트에 기록된 것에는 결코 도달하지 않는다(시트가 정본이다).
  • 임포트/Export/Push 동안에는 언어 변경이 잠긴다(메뉴 재생성 재컴파일을 트리거하기 때문이다). 테마 변경은 잠기지 않는다 — 결코 재컴파일을 트리거하지 않으므로, 밝기와 프리셋은 실행 중이라도 언제든 전환할 수 있다.
  • 테마와 언어는 프로젝트별이 아니라 사용자별이다(EditorPrefs) — 팀원은 각자 자신의 것을 유지하며, 어느 쪽도 버전 관리에 나타나지 않는다. 기본이 아닌 컬러 프리셋을 선택하면 Assets/SheetForge/Editor/Generated/ 아래에 생성된 스타일시트 하나가 기록되며(gitignore됨, 자가 치유됨), 기본 프리셋은 아무것도 기록하지 않고 그것을 삭제한다.
  • 생성 코드 + 베이크된 SO + Addressables 그룹은 머신별 gitignore된 캐시다 — 각 머신은 임포트를 한 번 실행한다. 게임 코드는 주소로 로드하며, 결코 씬 직접 참조로 로드하지 않는다.

11. 라이선스

저장소는 LICENSE 고지를 포함하고 있다: Unity Asset Store EULA가 지배 계약이며, 저장소 열람에 대한 고지가 함께 제공된다(소스는 참고용 및 라이선스를 취득한 구매자를 위해 열람 가능하다. 서면 허가 없이 EULA 범위 밖에서의 재배포/재판매는 불가하다). 서드파티 코드: 전혀 없음 — 직접 작성한 OOXML xlsx 리더/라이터를 포함해서.

12. 검증된 상태 (출시 시점)

  • 이중 테스트 하네스: 헤드리스 .NET 테스트 2,150개(2,150개 통과) + EditMode 테스트 3,021개(3,021개 통과, 실패 0건, 스킵 4건). 이 두 수치가 개수를 명시하는 유일한 곳이다 — 다른 모든 페이지는 이곳으로 링크한다.
  • 스킵된 네 건은 라이브 Google 왕복이며, 이는 환경에 서비스 계정 자격 증명이 있을 때만 실행되고 이번 실행에서는 건너뛰어졌다. 자격 증명이 있을 때는 실제 스프레드시트를 대상으로 반복적으로 실행되어 왔다 — fetch → push, 행 이동 충돌 탐지, 로케일 독립적인 float 처리, 탭을 넘나드는 혼합 배치, 그리고 하나의 병합된 리포트와 전송당 정확히 한 번의 자동 재임포트를 단언하는 전체 디스패처 실행을 포함한다. 결과: 4/4 green.
  • 웹 앱은 자신만의 게이트를 모두 통과한다: 타입 체크, lint, 유닛 테스트 414개, WebAssembly publish + 스모크 실행(실제 플러그인 DLL을 로드한다), 프로덕션 빌드, 종단 간(end-to-end) 브라우저 테스트 56개, 그리고 언어 간 상수 일치 검사(Unity↔웹 여섯 쌍: 호스트 버전, 플러그인 형식, 레지스트리 스키마 버전, OAuth 스코프, 호스트 어셈블리 이름, 행 창 임계값).
  • 컴파일된 모든 제품 asmdef: 에러 0건, 경고 0건. (샘플 asmdef — 각 데모에 두 개씩 — 는 데모 패키지를 임포트해야만 비로소 존재하며, 그전까지는 컴파일되지 않는다.)
  • 가드 테스트 통과: 제품 소스에 한국어 리터럴 없음, 커널 이음매에 도메인 어휘 없음(둘 다 /Samples~/는 건너뛴다 — 샘플은 도메인 콘텐츠이기 때문이다), 10개 언어 키 패리티.
  • IVT가 없는 소비자 시뮬레이션 어셈블리는 공개 API만을 대상으로 컴파일되며(컴파일러로 강제됨), 그 안의 IVT 없는 미니 플러그인 프로브는 공개 표면만을 사용해 열여섯 가지 중 열다섯 가지 확장 계약(ISheetForgePlugin / validator / edge / marker / template / graph / code registry / theme / studio UI / strings / pipeline / ISheetSourceProvider / Studio widget / Studio inspector action / Studio cell editor)을 구현한다 — 따라서 Plugin Demo 샘플이 컴파일되지 않아도 계약의 공개성은 계속 증명된다. 열여섯 번째인 IStudioPanelProviderVisualElement를 반환하므로 대신 에디터 측 테스트로 검증된다. 동일한 프로브는 IReferencingCellType / IRefBearingValue를 포함한 옵트인 capability 인터페이스도 구현하며, 공개 표면을 통해 이를 실증한다(캐스팅을 통한 발견, 다섯 훅 전부, 잔여물 보존).

Asset Store 제출 시점에만 검증할 수 있는(저장소 안에서는 검증할 수 없는) 항목: .unitypackage 설치 프롬프트 동작, Portal 의존성 선언, 배포 패키지에서 테스트 어셈블리 제외 여부, 그리고 클린 프로젝트에서의 0경고 재확인.

13. 현지화 시트 (게임 텍스트)

현지화 시트와 Unity Localization 브리지의 경계를 정직하게 밝힌다. (패키지 자체는 선택 사항이다 — §1 참고.)

브리지는 단방향이며, 외부 테이블 편집은 물어볼 뿐 결코 병합하지 않는다

  • 무엇을: 동기화는 오직 시트 → StringTable 방향으로만 진행된다. 브리지는 자신이 소유한 테이블에 스탬프를 찍고 매 동기화마다 핑거프린트를 남긴다; 그 이후 다른 무언가가 편집한 테이블 — Localization Tables 창, Unity 자체 Google Sheets 확장, XLIFF 임포트 — 는 다음 동기화를 멈추고 물어보게 만든다: 시트로부터 덮어쓸지, 차이 리포트와 함께 중단할지.
  • 왜: 같은 데이터에 두 개의 쓰기 가능한 조종석이 있으면 조용한 덮어쓰기로 끝난다. 시트가 정본이므로, 다른 조종석은 조용해서는 안 되고 명시적이어야 한다.
  • 우회 방법: 번역을 시트를 경유하게 한다 — 번역 워크북(xlsx export + 로케일 전용 부분 재임포트)이 정확히 그것을 위해 존재한다.

스튜디오 밖에서의 키 이름 변경은 삭제 후 추가와 같다

  • 무엇을: 데이터 스튜디오에서 키를 이름 변경하면 테이블 항목이 제자리에서 이름 변경되며, LocalizedString 참조가 결속하는 내부 id가 보존된다 — 씬 참조가 살아남는다. 시트 소스에서 직접 키를 이름 변경하는 것(Google Sheets, Excel)은 하나의 키를 지우고 다른 키를 추가하는 것과 구별할 수 없다: 브리지는 새 항목을 만들고 옛 항목은 고아가 되며, 씬 참조는 여전히 그 고아를 가리킨다.
  • 왜: 텍스트 수준의 diff는 추측 없이는 이름 변경과 삭제+추가를 구별할 수 없으며, 잘못된 추측은 참조를 조용히 재결속시킬 것이다.
  • 우회 방법: 데이터 스튜디오(둘 중 어느 호스트든)에서 키 이름을 변경한다; 고아 리포트가 외부 이름 변경의 여파를 잡아낸다.

고아 테이블 키는 기본적으로 보존된다

  • 무엇을: 테이블에는 존재하지만 더 이상 시트에는 없는 키는 유지되고, 고아로 보고되며, 명시적인 정리 액션을 거쳐야만 — 또는 동기화 시 삭제 설정을 옵트인했다면 자동으로 — 제거된다. 부작용으로 삭제되는 것은 없다.
  • 왜: 사라진 시트 행은 편집 도중의 실수일 수 있다; 그 위에서 번역을 파괴하면 복구할 수 없다.

문자열 테이블만 해당한다 — 에셋 테이블은 다루지 않는다

  • 무엇을: 브리지는 StringTable 컬렉션을 채운다. Unity Localization의 AssetTable 축(현지화된 스프라이트, 오디오, 프리팹)은 시트로부터 동기화되지 않는다. 인정된 백로그 항목이다.
  • 우회 방법: 에셋 테이블은 패키지 자체 도구로 관리한다; 브리지는 이를 건드리지 않는다.

XLIFF와 pseudo-locale은 재구현하지 않는다

  • 무엇을: 동기화된 테이블은 평범한 Unity Localization 테이블이므로, 패키지 자체의 XLIFF export/import와 pseudo-localization이 변경 없이 그 위에서 동작한다. SheetForge는 두 번째 구현을 더하지 않는다.
  • 경계: 그런 도구가 테이블 안으로 쓰는 출력은 외부 편집으로 취급된다(위의 첫 항목). 시트를 정본으로 유지하고, 번역은 번역 워크북을 경유해 옮긴다.

언어별 Smart Format 헬퍼는 제공되지 않는다

  • 무엇을: smart 컬럼은 항목을 Smart String으로 표시하지만, SheetForge는 자체 문법 포매터를 제공하지 않는다 — 예를 들어 한국어 조사 선택은 의도적으로 포함되지 않는다.
  • 우회 방법: 패키지의 Smart Format 확장 지점은 사용자가 작성하는 포매터를 위해 온전히 열려 있다.

키 상수는 ASCII로 정제된다

  • 무엇을: 생성된 {Tab}Keys 상수는 ASCII 문자·숫자·_ 밖의 모든 문자를 _로 바꾼다(충돌에는 숫자 접미사가 붙는다), 그래서 비-ASCII 키는 읽을 수 없는 상수 이름이 된다 — 키 자체는 어디서나 여전히 동작한다.
  • 우회 방법: 상수를 사용할 것이라면 키를 ASCII로 유지한다(ui.ok, dialog.intro). 시트에서 정의된 enum 파일과 마찬가지로, 상수 파일은 항상 설정 폴더에 생성된다 — 동일한 어셈블리 주의사항이 적용된다.

웹 앱은 현지화 시트를 작성한다; 동기화는 에디터의 몫이다

  • 무엇을: 작성, 검증, 커버리지, 민팅, 로케일 렌즈, 번역 워크북 모두 브라우저에서 동작한다. StringTable 쓰기는 그렇지 않다 — 브라우저에는 써넣을 Unity 프로젝트가 없다.
  • 왜: 정직한 범위이지, 누락된 기능이 아니다: 테이블은 프로젝트 안에 산다.

관련 페이지