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

12 KiB
Raw Permalink Blame History

작업지시서 — 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~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행

    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. 검토/주의 사항 (적용 전 확인)

  1. 스레드: GetCurrentPageIndex()는 WinForms 컨트롤(toolStripComboBoxPage)을 읽음. 두 호출부 모두 WPF UI(Dispatcher) 스레드에서 실행되며 MainPage는 같은 UI 스레드에 호스팅되므로 정상. 만약 백그라운드 스레드에서 호출될 가능성이 생기면 Invoke 래핑 필요.
  2. 페이지 인덱스 유효성 — null 가드 [확정 적용]: GetCurrentPageIndex()가 반환한 인덱스가 FieldHelper.json 페이지 수를 초과하면 GetPageFieldScan이 null 반환 → GenerateSortedScoutNoList(null) NRE 위험. 변경 2에 null 가드를 반드시 포함한다.
    _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) 변경 없음 → 데이터 롤백 불필요.