LGES-OC5-PtoP-Welder/docs/작업지시서/2026-06-19_ScoutField_페이지인덱스_시퀀스주입.md
NohDoyeon241104 3849d4d951 docs: ScoutField 페이지별 로드 버그 작업지시서 + 작업일지 2026-06-19
- FieldHelper.json 분석 및 selectedIndexNo stale 버그 원인 정리
- A안(SeqScoutNormal PageIndex 주입) 수정안 작업지시서 작성 (소스 미수정, null 가드 포함)
- 작업일지 16:30 항목 + 주말 출근(06.20~06.21) 기록

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 17:23:54 +09:00

199 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 작업지시서 — 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<ChildBufRowCol> 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<ChildBufRowCol>();
_ScoutNoList = GenerateSortedScoutNoList(_ChildBufRowCols);
_ScoutNo = 0;
```
※ `_InitParam`은 `Sequence_Enter`(107행)에서 이미 세팅되며, `InitSequenceVariable`은 그 이후 호출되므로 null 아님. (호출 순서: Sequence_Enter → InitSequenceVariable)
※ `List<ChildBufRowCol>` 사용을 위해 `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<ChildBufRowCol> LoadScoutField()
{
// [기존 보존 2026-06-19] selectedIndexNo(GUI 캐시) 기반. 하위 호환용으로 남겨둠.
return DB.RecipeFieldScan.Instance.GetPageFieldScan(
DB.RecipeFieldScan.Instance.GetAutoPageName(selectedIndexNo));
}
// [추가 2026-06-19] 페이지 인덱스를 명시 지정해 스카우트 필드 로드 (시퀀스 PageIndex 주입용)
public List<ChildBufRowCol> 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<ChildBufRowCol>(); // 그래도 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`) 변경 없음 → 데이터 롤백 불필요.