diff --git a/docs/분석/2026/06/24/102127_manual_selectfield_저장_및_scoutnormal_읽기_분석.md b/docs/분석/2026/06/24/102127_manual_selectfield_저장_및_scoutnormal_읽기_분석.md new file mode 100644 index 0000000..4e06242 --- /dev/null +++ b/docs/분석/2026/06/24/102127_manual_selectfield_저장_및_scoutnormal_읽기_분석.md @@ -0,0 +1,191 @@ +# Manual "Select Field" 저장 위치 / Recipe "Run Scout Normal" 읽기 경로 분석 + +- 작성일: 2026-06-24 +- 대상: Manual 화면의 **Select Field(Scout Field 선택)** 저장 흐름과, Recipe 화면의 **Run Scout Normal** 실행 시 선택 필드 영역을 어디서 읽어오는지 + +--- + +## 0. 결론 요약 + +| 항목 | 값/위치 | +|------|---------| +| 저장되는 값 | `List` (각 셀의 `RowIdx`, `ColIdx`, `Enabled`, `PosIdx`, `MotorPos`, `MotorPosOffset`, `Rect` 등) | +| 저장 키 | 페이지별로 `selectedIndex{N}` (예: `selectedIndex0`) PageName 으로 구분 | +| 저장 파일 | **현재 작업 레시피 폴더의 `FieldHelper.json`** (레시피 파일과 같은 디렉터리) | +| 부가 저장 | CAM1 `CamConfig.ChildImage.ChildBufRowCols` → `SaveDeviceConfig`(디바이스 config) | +| Run Scout Normal 읽기 | 같은 `FieldHelper.json` 의 싱글톤 캐시(`RecipeFieldScan.Instance`)에서 **PageIndex** 로 조회 | + +핵심: 저장도 읽기도 **`FieldHelper.json` 한 파일**이며, `RecipeFieldScan.Instance`(싱글톤 인메모리 캐시)를 매개로 페이지 인덱스(`selectedIndex{N}`)를 키로 주고받는다. `ScannerObjectTeaching.json` 은 사용되지 않는다. ([[scout-field-storage]] 메모리와 정합) + +--- + +## 1. Manual "Select Field" — 저장 흐름 + +### 1.1 화면 진입 +- `UCManualScannerOperationViewModel.PerformCmdShowSelectFieldWindow` → `WDSelectScoutField` 창 오픈 + - 파일: `WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCManualScannerOperationViewModel.cs:52` +- 창의 ViewModel = `WDSelectScoutFieldViewModel` + +### 1.2 초기 로드(편집 대상 채우기) +- `WDSelectScoutFieldViewModel.LoadFieldScanParams()` (`...WDSelectScoutFieldViewModel.cs:78`) + ```csharp + FieldScanParamList = CommonHelper.GetMainPage().LoadScoutField(); + ``` +- `MainPage.LoadScoutField()` (`WeldingComponents/LASA/GUI/Page/MainPage.cs:512`) + → `RecipeFieldScan.Instance.GetPageFieldScan(GetAutoPageName(selectedIndexNo))` + → 현재 페이지(`selectedIndex{N}`)의 `FieldScans` 리스트를 싱글톤 캐시에서 가져옴. + +### 1.3 저장(PerformCmdSave) +파일: `...WDSelectScoutFieldViewModel.cs:91` +```csharp +public void PerformCmdSave() +{ + var camConfig = ...GetDeviceConfig(eCamId.CAM1) as CamConfig; + camConfig.ChildImage.ChildBufRowCols = FieldScanParamList.ConvertAll(o => o.Clone()); + + CommonHelper.GetMainPage().UpdateScoutField(camConfig.ChildImage.ChildBufRowCols); // (A) + VisionEnv.AuroraEnv.DeviceManager.SaveDeviceConfig(eCamId.CAM1); // (B) + ... +} +``` + +**(A) FieldHelper.json 경로 저장** — `MainPage.UpdateScoutField()` (`MainPage.cs:499`) +```csharp +SaveAndLoadFieldScanFromLasaDb(); // 기존 캐시 → 파일 동기화 +int selectedNo = toolStripComboBoxPage.SelectedIndex; // 현재 페이지 인덱스 +DB.RecipeFieldScan.Instance.UpdateScoutField(a, selectedNo); // 캐시의 해당 페이지 교체 +SaveAndLoadFieldScanFromLasaDb(); // 캐시 → 파일 재저장+재로드 +``` +- `SaveAndLoadFieldScanFromLasaDb()` (`MainPage.cs:535`) + - `fullpath = DB.Dao.SystemRecipe.WorkRecipeFile` (현재 작업 레시피 경로) + - `newpath = fullpath` 의 파일명을 **`FieldHelper.json`** 으로 치환 → 레시피와 같은 폴더 + - `RecipeFieldScan.Instance.SaveFieldHelper(newpath)` → JSON 직렬화 후 `File.WriteAllText` + (`RecipeFieldScan.cs:96`) + +**(B) 디바이스 config 저장** — `SaveDeviceConfig(CAM1)` +- CAM1 `CamConfig.ChildImage.ChildBufRowCols` 를 Aurora 디바이스 config 로 별도 저장(부수적, 실행 읽기에는 미사용). + +### 1.4 저장되는 실제 값(데이터 모델) +`RecipeFieldScan.FieldScanData` (`RecipeFieldScan.cs:175`) +```json +[ + { + "PageName": "selectedIndex0", + "FileName": "...FieldScanImage 경로...", + "FieldScans": [ + { "RowIdx":0, "ColIdx":0, "Enabled":true, "PosIdx":0, + "MotorPos":{...}, "MotorPosOffset":{...}, "Rect":{...} }, + ... + ] + }, + { "PageName": "selectedIndex1", ... } +] +``` +- 페이지별로 한 엔트리, 각 엔트리에 그 페이지의 모든 셀(`ChildBufRowCol`) 영역 정보가 담긴다. +- 페이지 키는 `GetAutoPageName(N) = "selectedIndex{N}"` (`RecipeFieldScan.cs:130`). + +--- + +## 2. Recipe "Run Scout Normal" — 읽기 경로 + +### 2.1 실행 트리거 +- `UCScannerObjectTeachingControlBoxViewModel.PerformCmdRunScoutNormal` (`...Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs:229`) + ```csharp + var param = new SeqScoutNormal_InitParam() { + IsRealTimeScout = false, + ScoutSeqParams = scoutSeqParams, + PageIndex = CommonHelper.GetMainPage().GetCurrentPageIndex(), // 현재 표시 페이지 명시 주입 + }; + _SeqScoutNormal.Start(param, OnCompleteScoutNormal); + ``` +- `GetCurrentPageIndex()` (`MainPage.cs:526`): 콤보박스 `SelectedIndex` 우선, 없으면 `ActiveLayer.No`. + +### 2.2 시퀀스 내부 로드 +- `SeqScoutNormal.InitSequenceVariable()` (`...Sequences/Main/SeqScoutNormal.cs:129`) + ```csharp + _ChildBufRowCols = mainPage.LoadScoutField(_InitParam.PageIndex) // ← 명시 페이지 로드 + ?? mainPage.LoadScoutField() // 폴백: selectedIndexNo + ?? new List(); // 최종 폴백: 빈 리스트(NRE 방지) + _ScoutNoList = GenerateSortedScoutNoList(_ChildBufRowCols); + ``` +- `MainPage.LoadScoutField(int pageIndex)` (`MainPage.cs:519`) + → `RecipeFieldScan.Instance.GetPageFieldScan(GetAutoPageName(pageIndex))` + → 싱글톤 캐시에서 `selectedIndex{pageIndex}` 엔트리의 `FieldScans` 반환 (`RecipeFieldScan.cs:119`) + +### 2.3 캡처에 사용 +- `CaptureNextPosition()` (`SeqScoutNormal.cs:230`)에서 `Enabled` 셀만 순회하며 + `MotorPos + MotorPosOffset` 으로 스캐너 Jump → Grab → `Rect.X/Y` 위치에 부모 이미지로 복사. + +--- + +## 3. 두 경로의 연결 고리 (Select Field 저장 ↔ Scout Normal 읽기) + +``` +[Manual] WDSelectScoutFieldViewModel.PerformCmdSave + └─ MainPage.UpdateScoutField(page=SelectedIndex) + └─ RecipeFieldScan.Instance.UpdateScoutField (인메모리 캐시 갱신) + └─ SaveFieldHelper(newpath) ──┐ 같은 파일 + │ + ┌────────────────────────────────────────────────────── FieldHelper.json + │ (레시피 폴더 내, 페이지별 selectedIndex{N}) + ▼ +[Recipe] RunScoutNormal(PageIndex=GetCurrentPageIndex) + └─ SeqScoutNormal.LoadScoutField(PageIndex) + └─ RecipeFieldScan.Instance.GetPageFieldScan("selectedIndex{N}") (같은 싱글톤 캐시) +``` + +- **매개 저장소**: `RecipeFieldScan.Instance`(싱글톤 인메모리) + 디스크 `FieldHelper.json`. +- **키**: 페이지 인덱스 → `selectedIndex{N}`. +- 레시피 전환 시에는 `MainPage.ReloadRecipe()`(`MainPage.cs:460`)가 `LoadFieldScanFromLasaDb()`(로드 전용)로 새 레시피의 `FieldHelper.json` 만 싱글톤에 반영 → 이전 레시피 값(stale) 덮어쓰기 방지. (2026-06-20 수정, [[recipe-page-handshake-redesign]] 맥락) + +--- + +## 3.5 FieldHelper.json 엔트리 해석 — `FileName`(표시용) vs `FieldScans`(스캔 입력용) + +레시피에서 `FieldHelper.json` 을 열면, `PageName(selectedIndex{N})` 별 엔트리에 다음 두 가지가 들어 있고 **역할이 서로 다르다.** + +| 필드 | 내용 | 실제 쓰임 | +|------|------|-----------| +| `FileName` | 그 페이지의 스카우트 필드 **이미지 경로** | **화면 표시(배경)용** — Teaching/AutoRun 캔버스에 띄우는 용도 | +| `FieldScans` | 셀(`ChildBufRowCol`) 배열: `RowIdx`/`ColIdx`, `Enabled`, `PosIdx`, `MotorPos`/`MotorPosOffset`, `Rect` | **Run Scout Normal 의 실제 입력** — "어디로 가서 찍을지" 결정 | + +### `FileName` 은 표시용이지 스캔 입력이 아님 +- 사용처: `MainPage.ChangeScoutImage`, `ReloadSetting` → `ChangeFieldString(..., ScannerFieldImageFile)` 로 캔버스 배경 이미지에 반영. +- **Run Scout Normal 실행 시 `FileName` 이미지를 불러와 쓰지 않는다.** + +### Run Scout Normal 이 실제로 하는 일 (`FieldScans` 기반) +1. `FieldScans` 중 **`Enabled == true` 셀만** 순회 (`SeqScoutNormal.cs:234`) +2. 각 셀의 `MotorPos + MotorPosOffset` 좌표로 스캐너 Jump → **카메라로 새로 Grab** (`GrabSync`, `SeqScoutNormal.cs:260`) +3. 각 셀의 `Rect.X/Y` 위치에 부모 이미지로 복사 (`MbufCopyClip`, `SeqScoutNormal.cs:268`) +4. 완성된 필드 이미지를 **새 경로에 저장** (`{DATA_PATH}/_vision/FieldScanImage/{날짜}/{n}_{시각}.PNG`, `SeqScoutNormal.cs:284` `SaveImageFile`) + +### 한 줄 정리 +- `FieldScans`(Enable + 좌표/Rect) = Scout Normal 이 읽어 "어디로 가서 찍을지" 정하는 입력 데이터. +- `FileName` = 이전에 스캔해 둔 결과 이미지를 화면에 보여주는 표시용(스캔 입력 아님). 실행 시에는 매번 새로 촬영해 새 PNG 로 저장한다. + +--- + +## 4. 주의/참고 + +- `selectedIndexNo`(GUI 전역 캐시) 의존 버그를 피하기 위해, Scout Normal 은 `PageIndex` 를 **명시 주입**한다(2026-06-19 변경). `LoadScoutField()`(무인자)는 하위호환 폴백으로만 남아 있다. ([[scout-field-storage]]) +- 배경 이미지(편집 화면 배경)는 별개로 `LASARecipeHelper.GetValue("ScannerFieldImageFile", default)` 경로의 Jpeg 를 사용(`WDSelectScoutFieldViewModel.SetBackgroundImage`). +- `ScannerObjectTeaching.json` 은 이 흐름에서 사용되지 않음(다른 컨트롤박스 관련). + +--- + +## 부록 — 핵심 파일/라인 + +| 역할 | 파일:라인 | +|------|-----------| +| Select Field 창 오픈 | `WeldingCommon/Views/Manual/Scanner/UCManualScannerOperationViewModel.cs:52` | +| 초기 로드 | `.../WDSelectScoutFieldViewModel.cs:78` (`LoadScoutField`) | +| 저장 진입 | `.../WDSelectScoutFieldViewModel.cs:91` (`PerformCmdSave`) | +| 페이지별 캐시 갱신 | `LASA/GUI/Page/MainPage.cs:499` (`UpdateScoutField`) | +| 파일 동기화 경로(FieldHelper.json) | `MainPage.cs:535` (`SaveAndLoadFieldScanFromLasaDb`) | +| JSON 직렬화 저장 | `LASA/GUI/DB/RecipeFieldScan.cs:96` (`SaveFieldHelper`) | +| 페이지 키 규칙 | `RecipeFieldScan.cs:130` (`GetAutoPageName`) | +| Run Scout Normal 트리거 | `WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs:229` | +| PageIndex 산출 | `MainPage.cs:526` (`GetCurrentPageIndex`) | +| 시퀀스 내 로드 | `WeldingCommon/Sequences/Main/SeqScoutNormal.cs:155` | +| 페이지 명시 로드 | `MainPage.cs:519` (`LoadScoutField(int)`) → `RecipeFieldScan.cs:119` (`GetPageFieldScan`) | +| 캡처 사용 | `SeqScoutNormal.cs:230` (`CaptureNextPosition`) |