# 작업지시서 — ScoutField 페이지별 로드 버그 수정 (시퀀스 PageIndex 주입, 방식 A) - 작성일: 2026-06-19 - 대상 브랜치: `field/ptop-1st` - 작업 분류: 버그 수정 (스카우트 필드 페이지 정합성) - 방식: **A안 — `SeqScoutNormal`에 처리 중인 페이지 번호를 명시적으로 주입** (B안=GUI 콤보박스 동기화는 위험성으로 제외) - ⚠️ 본 문서는 **수정안(검토용)** 입니다. 아직 소스는 변경하지 않았습니다. 다른 PC에서 본 지시서대로 적용하세요. --- ## 1. 증상 (현장 보고) 레시피별·페이지별로 "Selected Scout Field" 팝업에서 선택한 필드는 정상 저장되어 있음에도, - 레시피를 활성화하거나 페이지1 ↔ 페이지2를 이동한 뒤 - 페이지2에서 저장한 필드가 아니라 **페이지1(또는 마지막에 머문 페이지)에서 저장한 필드 형태로** `SeqScoutNormal`(ScoutImageNormal)이 동작/표시됨. - 즉, 페이지를 이동해도 그 페이지의 스카우트 필드를 다시 불러오지 않음. ## 2. 데이터 저장 구조 (분석 결과) - "Selected Scout Field" 팝업 값의 실제 저장처는 `_config_recipe/<레시피>/FieldHelper.json`. - `ScannerObjectTeaching.json` / `LaserScannerTeaching.json` / `PLCRecipe.json` / `VisionInspectionRecipe.json`은 **전부 빈 `{}`(미사용)**. - `FieldHelper.json` = 페이지 배열. 각 원소 `{ PageName:"selectedIndex{N}", FileName:<배경 이미지>, FieldScans:[ ChildBufRowCol × 80 (10행×8열) ] }`. - `ChildBufRowCol` 키: `Enabled`(선택 여부=실제 스카우트 대상), `RowIdx`/`ColIdx`/`PosIdx`, `MotorPos{X,Y}`, `MotorPosOffset{X,Y}`, `Rect`. - 실측: 레시피별 2~5페이지가 distinct 하게 저장됨(저장 자체는 정상). ## 3. 근본 원인 (코드 레벨) ### 3-1. 시퀀스가 페이지 번호를 모름 - `WeldingComponents/WeldingCommon/Sequences/Main/SeqScoutNormal.cs:151` ```csharp _ChildBufRowCols = mainPage.LoadScoutField(); // 인자 없음 ``` - `SeqScoutNormal_InitParam`(SeqScoutNormal.cs:373)에는 **페이지 인덱스 필드가 없음.** - `LoadScoutField()`(MainPage.cs:499)는 전적으로 GUI 상태 변수 `selectedIndexNo`만 키로 사용: ```csharp public List LoadScoutField() => RecipeFieldScan.GetPageFieldScan(GetAutoPageName(selectedIndexNo)); ``` ### 3-2. `selectedIndexNo`는 WinForms 티칭 콤보박스에서만 갱신됨 - `MainPage.cs:1379~1393` `toolStripComboBoxPage_SelectedIndexChanged`에서만 `selectedIndexNo = box.SelectedIndex` 로 대입. - 운전 화면(Recipe/Manual, WPF) 페이지 이동·자동운전·시퀀스 실행 경로에서는 갱신되지 않아 **stale** 상태가 됨 → 항상 "티칭 콤보박스가 마지막으로 머문 페이지"(보통 0=Page1) 로드. ### 3-3. 저장/로드 비대칭 (참고) | 동작 | 사용 인덱스 | |---|---| | 저장 `UpdateScoutField` (MainPage.cs:490) | `toolStripComboBoxPage.SelectedIndex` (라이브) | | 로드 `LoadScoutField` (MainPage.cs:502) | `selectedIndexNo` (캐시) | → 저장은 라이브 값이라 페이지별로 맞게 저장되지만, 로드는 캐시 값이라 stale → "저장은 맞고 로드만 틀림". ## 4. 수정 방침 (A안) GUI 전역 상태(`selectedIndexNo`) 의존을 제거하고, **처리 대상 페이지 번호를 시퀀스 InitParam으로 명시 주입**한다. 1. `SeqScoutNormal_InitParam`에 `PageIndex` 추가. 2. `MainPage`에 페이지 인덱스를 받는 `LoadScoutField(int pageIndex)` 오버로드 추가 + 현재 표시 페이지를 돌려주는 공개 접근자 `GetCurrentPageIndex()` 추가. 기존 `LoadScoutField()`는 호환 위해 보존(주석 설명). 3. `SeqScoutNormal`이 `_InitParam.PageIndex`로 로드하도록 변경. 4. 호출부 2곳에서 InitParam 생성 시 `PageIndex = CommonHelper.GetMainPage().GetCurrentPageIndex()` 주입. > 규칙: 기존 라인은 삭제하지 말고 **주석 처리**, 신규/변경 라인은 주석으로 의도 표기. --- ## 5. 파일별 변경안 (Before/After) ### [변경 1] `WeldingComponents/WeldingCommon/Sequences/Main/SeqScoutNormal.cs` — InitParam에 PageIndex 추가 위치: 373~378행 ```csharp public class SeqScoutNormal_InitParam : InitParam { public bool IsRealTimeScout { get; set; } public ScoutSeqParams ScoutSeqParams { get; set; } // [추가 2026-06-19] 스카우트 필드를 불러올 페이지 인덱스(0-base). // 기존: SeqScoutNormal 이 MainPage.selectedIndexNo(GUI 전역 상태)에만 의존 → 페이지 이동/자동운전 시 stale. // 변경: 호출부가 "현재 처리/표시 중인 페이지 번호"를 명시 주입하도록 함. public int PageIndex { get; set; } = 0; } ``` ### [변경 2] `WeldingComponents/WeldingCommon/Sequences/Main/SeqScoutNormal.cs` — PageIndex로 로드 위치: 150~152행 (`InitSequenceVariable` 내부) ```csharp var mainPage = CommonHelper.GetMainPage(); // [기존 2026-06-19 주석처리] GUI 전역 상태(selectedIndexNo)에 의존 → 페이지 정합성 버그 //_ChildBufRowCols = mainPage.LoadScoutField(); // [변경 2026-06-19] InitParam으로 전달된 페이지 인덱스로 명시 로드 // null 가드: 해당 페이지가 FieldHelper.json에 없으면(인덱스 초과 등) GetPageFieldScan이 null → // 기존 LoadScoutField()(selectedIndexNo 기반)로 폴백, 그래도 null이면 빈 리스트로 NRE 방지. _ChildBufRowCols = mainPage.LoadScoutField(_InitParam.PageIndex) ?? mainPage.LoadScoutField() ?? new List(); _ScoutNoList = GenerateSortedScoutNoList(_ChildBufRowCols); _ScoutNo = 0; ``` ※ `_InitParam`은 `Sequence_Enter`(107행)에서 이미 세팅되며, `InitSequenceVariable`은 그 이후 호출되므로 null 아님. (호출 순서: Sequence_Enter → InitSequenceVariable) ※ `List` 사용을 위해 `using System.Collections.Generic;` 존재 확인(SeqScoutNormal.cs는 이미 포함). `ChildBufRowCol`은 `VisionCommon`(이미 using). ※ 빈 리스트로 폴백되면 `_ScoutNoList`도 비어 `CaptureNextPosition`의 foreach가 즉시 종료 → 스캔 대상 없이 정상 진행(크래시 없음). 필요 시 빈 경우 Alarm/Log 추가 검토. ### [변경 3] `WeldingComponents/LASA/GUI/Page/MainPage.cs` — 오버로드 + 현재 페이지 접근자 추가 위치: 499~503행 (`LoadScoutField()`) ```csharp public List LoadScoutField() { // [기존 보존 2026-06-19] selectedIndexNo(GUI 캐시) 기반. 하위 호환용으로 남겨둠. return DB.RecipeFieldScan.Instance.GetPageFieldScan( DB.RecipeFieldScan.Instance.GetAutoPageName(selectedIndexNo)); } // [추가 2026-06-19] 페이지 인덱스를 명시 지정해 스카우트 필드 로드 (시퀀스 PageIndex 주입용) public List LoadScoutField(int pageIndex) { return DB.RecipeFieldScan.Instance.GetPageFieldScan( DB.RecipeFieldScan.Instance.GetAutoPageName(pageIndex)); } // [추가 2026-06-19] 현재 화면에 표시 중인 페이지 인덱스(라이브). 콤보박스 우선, 없으면 ActiveLayer.No. // 로드 키를 stale 캐시(selectedIndexNo) 대신 라이브 값으로 얻기 위함. public int GetCurrentPageIndex() { int idx = toolStripComboBoxPage.SelectedIndex; if (idx < 0) { var active = ScannerCanvasCtrlTeaching?.Model?.ActiveLayer; idx = active != null ? active.No : 0; } return idx; } ``` ※ `selectedIndexNo`(1379행), `toolStripComboBoxPage`(디자이너 필드), `ScannerCanvasCtrlTeaching`(202행 public)는 모두 동일 클래스/접근 가능. `GetCurrentPageIndex`는 WinForms UI 스레드에서 호출되어야 함(아래 6절 주의 참고). ### [변경 4] `WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs` — 호출부 주입 위치: 251~254행 (`PerformCmdRunScoutNormal`) ```csharp var scoutSeqParams = new ScoutSeqParams(VisionRecipeManager.GetFovInfos()[0], eDisplayType.Machine_B); var param = new SeqScoutNormal_InitParam() { IsRealTimeScout = false, ScoutSeqParams = scoutSeqParams, // [추가 2026-06-19] 현재 표시 중인 페이지의 스카우트 필드를 사용하도록 명시 주입 PageIndex = CommonHelper.GetMainPage().GetCurrentPageIndex(), }; ``` ※ 파일 상단 `using WeldingCommon.Utils;` 존재 여부 확인(미존재 시 추가). `CommonHelper`는 `WeldingCommon.Utils` 네임스페이스. ### [변경 5] `WeldingComponents/WeldingCommon/Views/Test/UCTestSequenceViewModel.cs` — 테스트 호출부 주입 위치: 361~364행 (`PerformCmdRunSeqScoutNormal`, case "Run") ```csharp var param = new SeqScoutNormal_InitParam() { IsRealTimeScout = false, ScoutSeqParams = new ScoutSeqParams(VisionRecipeManager.GetFovInfos()[0], eDisplayType.Machine_B), // [추가 2026-06-19] 현재 표시 중인 페이지 인덱스 주입 PageIndex = CommonHelper.GetMainPage().GetCurrentPageIndex(), }; ``` ※ 동일하게 `CommonHelper`(WeldingCommon.Utils) using 확인. --- ## 6. 검토/주의 사항 (적용 전 확인) 1. **스레드**: `GetCurrentPageIndex()`는 WinForms 컨트롤(`toolStripComboBoxPage`)을 읽음. 두 호출부 모두 WPF UI(Dispatcher) 스레드에서 실행되며 MainPage는 같은 UI 스레드에 호스팅되므로 정상. 만약 백그라운드 스레드에서 호출될 가능성이 생기면 `Invoke` 래핑 필요. 2. **페이지 인덱스 유효성 — null 가드 [확정 적용]**: `GetCurrentPageIndex()`가 반환한 인덱스가 `FieldHelper.json` 페이지 수를 초과하면 `GetPageFieldScan`이 `null` 반환 → `GenerateSortedScoutNoList(null)` NRE 위험. **변경 2에 null 가드를 반드시 포함**한다. ```csharp _ChildBufRowCols = mainPage.LoadScoutField(_InitParam.PageIndex) ?? mainPage.LoadScoutField() // selectedIndexNo 기반 폴백 ?? new List(); // 그래도 null이면 빈 리스트(NRE 방지) ``` 빈 리스트로 폴백되면 스캔 대상 없이 정상 종료(크래시 없음). 운영 판단에 따라 빈 경우 Alarm/Log 추가 가능(이번 범위 외). 3. **자동운전 경로**: 현재 PTOP에서 `SeqScoutNormal`은 위 두 VM에서만 Start됨(SequenceManager는 등록만, AlarmManager는 Stop만). 자동운전 시퀀스가 추후 ScoutNormal을 페이지 루프로 호출하게 되면, 그 호출부에서도 처리 중 페이지를 `PageIndex`로 넣어야 함(동일 패턴). 4. **저장 경로**: 본 수정은 로드만 교정. 저장(`UpdateScoutField`)은 이미 라이브 `SelectedIndex` 사용이라 변경 불필요. 단 일관성을 위해 추후 저장도 `GetCurrentPageIndex()`로 통일 검토 가능(이번 범위 외). ## 7. 영향 범위 / 빌드 - 수정 파일 4개: - `WeldingComponents/WeldingCommon/Sequences/Main/SeqScoutNormal.cs` - `WeldingComponents/LASA/GUI/Page/MainPage.cs` - `WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs` - `WeldingComponents/WeldingCommon/Views/Test/UCTestSequenceViewModel.cs` - 기존 `LoadScoutField()` 시그니처 보존 → 다른 호출부 영향 없음(호출부: SeqScoutNormal만, 본 작업에서 오버로드로 전환). - 빌드 대상: `WeldingCommon`, `LASA(GUI)`, 시작 프로젝트(PTOP). ## 8. 검증 시나리오 (현장) 1. 레시피 활성화 → 페이지1에서 필드 A1 저장, 페이지2에서 필드 A2 저장. 2. 페이지2 표시 상태에서 ScoutNormal 실행 → **A2**로 스캔/표시되는지 확인. 3. 페이지1로 이동 후 실행 → **A1** 확인. 4. 페이지2 재이동 후 실행 → **A2** 확인 (이전엔 A1으로 동작하던 버그). 5. 페이지 수보다 큰 인덱스/예외 상황에서 NRE 없이 동작(6-2 null 가드 — 폴백 후 빈 리스트로 정상 종료). ## 9. 롤백 - 변경 1~5의 신규 라인 제거 및 주석 처리한 기존 라인 복원. - 데이터(`FieldHelper.json`) 변경 없음 → 데이터 롤백 불필요.