- FieldHelper.json 분석 및 selectedIndexNo stale 버그 원인 정리 - A안(SeqScoutNormal PageIndex 주입) 수정안 작업지시서 작성 (소스 미수정, null 가드 포함) - 작업일지 16:30 항목 + 주말 출근(06.20~06.21) 기록 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
작업지시서 — 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_ChildBufRowCols = mainPage.LoadScoutField(); // 인자 없음SeqScoutNormal_InitParam(SeqScoutNormal.cs:373)에는 페이지 인덱스 필드가 없음.LoadScoutField()(MainPage.cs:499)는 전적으로 GUI 상태 변수selectedIndexNo만 키로 사용:public List<ChildBufRowCol> LoadScoutField() => RecipeFieldScan.GetPageFieldScan(GetAutoPageName(selectedIndexNo));
3-2. selectedIndexNo는 WinForms 티칭 콤보박스에서만 갱신됨
MainPage.cs:1379~1393toolStripComboBoxPage_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으로 명시 주입한다.
SeqScoutNormal_InitParam에PageIndex추가.MainPage에 페이지 인덱스를 받는LoadScoutField(int pageIndex)오버로드 추가 + 현재 표시 페이지를 돌려주는 공개 접근자GetCurrentPageIndex()추가. 기존LoadScoutField()는 호환 위해 보존(주석 설명).SeqScoutNormal이_InitParam.PageIndex로 로드하도록 변경.- 호출부 2곳에서 InitParam 생성 시
PageIndex = CommonHelper.GetMainPage().GetCurrentPageIndex()주입.
규칙: 기존 라인은 삭제하지 말고 주석 처리, 신규/변경 라인은 주석으로 의도 표기.
5. 파일별 변경안 (Before/After)
[변경 1] WeldingComponents/WeldingCommon/Sequences/Main/SeqScoutNormal.cs — InitParam에 PageIndex 추가
위치: 373~378행
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 내부)
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())
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)
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")
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. 검토/주의 사항 (적용 전 확인)
- 스레드:
GetCurrentPageIndex()는 WinForms 컨트롤(toolStripComboBoxPage)을 읽음. 두 호출부 모두 WPF UI(Dispatcher) 스레드에서 실행되며 MainPage는 같은 UI 스레드에 호스팅되므로 정상. 만약 백그라운드 스레드에서 호출될 가능성이 생기면Invoke래핑 필요. - 페이지 인덱스 유효성 — null 가드 [확정 적용]:
GetCurrentPageIndex()가 반환한 인덱스가FieldHelper.json페이지 수를 초과하면GetPageFieldScan이null반환 →GenerateSortedScoutNoList(null)NRE 위험. 변경 2에 null 가드를 반드시 포함한다.
빈 리스트로 폴백되면 스캔 대상 없이 정상 종료(크래시 없음). 운영 판단에 따라 빈 경우 Alarm/Log 추가 가능(이번 범위 외)._ChildBufRowCols = mainPage.LoadScoutField(_InitParam.PageIndex) ?? mainPage.LoadScoutField() // selectedIndexNo 기반 폴백 ?? new List<ChildBufRowCol>(); // 그래도 null이면 빈 리스트(NRE 방지) - 자동운전 경로: 현재 PTOP에서
SeqScoutNormal은 위 두 VM에서만 Start됨(SequenceManager는 등록만, AlarmManager는 Stop만). 자동운전 시퀀스가 추후 ScoutNormal을 페이지 루프로 호출하게 되면, 그 호출부에서도 처리 중 페이지를PageIndex로 넣어야 함(동일 패턴). - 저장 경로: 본 수정은 로드만 교정. 저장(
UpdateScoutField)은 이미 라이브SelectedIndex사용이라 변경 불필요. 단 일관성을 위해 추후 저장도GetCurrentPageIndex()로 통일 검토 가능(이번 범위 외).
7. 영향 범위 / 빌드
- 수정 파일 4개:
WeldingComponents/WeldingCommon/Sequences/Main/SeqScoutNormal.csWeldingComponents/LASA/GUI/Page/MainPage.csWeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.csWeldingComponents/WeldingCommon/Views/Test/UCTestSequenceViewModel.cs
- 기존
LoadScoutField()시그니처 보존 → 다른 호출부 영향 없음(호출부: SeqScoutNormal만, 본 작업에서 오버로드로 전환). - 빌드 대상:
WeldingCommon,LASA(GUI), 시작 프로젝트(PTOP).
8. 검증 시나리오 (현장)
- 레시피 활성화 → 페이지1에서 필드 A1 저장, 페이지2에서 필드 A2 저장.
- 페이지2 표시 상태에서 ScoutNormal 실행 → A2로 스캔/표시되는지 확인.
- 페이지1로 이동 후 실행 → A1 확인.
- 페이지2 재이동 후 실행 → A2 확인 (이전엔 A1으로 동작하던 버그).
- 페이지 수보다 큰 인덱스/예외 상황에서 NRE 없이 동작(6-2 null 가드 — 폴백 후 빈 리스트로 정상 종료).
9. 롤백
- 변경 1~5의 신규 라인 제거 및 주석 처리한 기존 라인 복원.
- 데이터(
FieldHelper.json) 변경 없음 → 데이터 롤백 불필요.