From 3849d4d9514f99dff3871417b7a53b03235dcb41 Mon Sep 17 00:00:00 2001 From: NohDoyeon241104 Date: Fri, 19 Jun 2026 17:23:54 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20ScoutField=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=B3=84=20=EB=A1=9C=EB=93=9C=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=A7=80=EC=8B=9C=EC=84=9C=20+=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=9D=BC=EC=A7=80=202026-06-19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FieldHelper.json 분석 및 selectedIndexNo stale 버그 원인 정리 - A안(SeqScoutNormal PageIndex 주입) 수정안 작업지시서 작성 (소스 미수정, null 가드 포함) - 작업일지 16:30 항목 + 주말 출근(06.20~06.21) 기록 Co-Authored-By: Claude Opus 4.8 --- docs/작업일지/2026_06_19.md | 21 ++ ...026-06-19_ScoutField_페이지인덱스_시퀀스주입.md | 198 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 docs/작업지시서/2026-06-19_ScoutField_페이지인덱스_시퀀스주입.md diff --git a/docs/작업일지/2026_06_19.md b/docs/작업일지/2026_06_19.md index 673352d..463f1e6 100644 --- a/docs/작업일지/2026_06_19.md +++ b/docs/작업일지/2026_06_19.md @@ -202,3 +202,24 @@ - 서브에이전트 검증 통과(로직·XAML 그리드·VM·인코딩 PASS) → 작업지시서 삭제 - 공유(WeldingCommon)라 같은행은 150(기존 동일), **행 전환 기본 300ms**(기존 150) — 타 머신도 영향, Recipe View에서 조정 가능 - → 커밋·push (e263ab8) + +## 16:30 — ScoutField 페이지별 로드 버그 분석 + 작업지시서 작성 (소스 미수정) + +### 분석 (레시피 데이터 + 코드) +- `_config_recipe/<레시피>/` 분석: "Selected Scout Field" 팝업 값의 실제 저장처는 **`FieldHelper.json`** (페이지 배열, 각 페이지 `FieldScans` 80셀=10×8, `Enabled`=선택 셀) + - `ScannerObjectTeaching.json`/`LaserScannerTeaching.json`/`PLCRecipe.json`/`VisionInspectionRecipe.json` 은 **전부 빈 `{}`(미사용)** +- **버그**: 페이지 이동해도 그 페이지 필드를 다시 안 불러옴 → 항상 마지막 머문 페이지(보통 Page1) 필드로 ScoutNormal 동작 + - 원인: `LoadScoutField()`(MainPage.cs:499/502)가 GUI 전역 `selectedIndexNo`만 키로 사용. 이 값은 WinForms 티칭 콤보박스 `toolStripComboBoxPage_SelectedIndexChanged`(1393)에서만 갱신 → 운전(WPF)/자동운전/시퀀스 경로에선 stale + - 비대칭: 저장 `UpdateScoutField`는 라이브 `SelectedIndex`(490) 사용해 페이지별 정상 저장되나, **로드만 stale** + - `SeqScoutNormal_InitParam`(SeqScoutNormal.cs:373)엔 페이지 인덱스 필드 없음 → 시퀀스가 처리 페이지를 모름 + +### 작업지시서 작성 (방식 A 채택, 소스는 미수정) +- 문서: `docs/작업지시서/2026-06-19_ScoutField_페이지인덱스_시퀀스주입.md` +- **A안**: InitParam에 `PageIndex` 추가 → `MainPage.LoadScoutField(int)` 오버로드 + `GetCurrentPageIndex()`(콤보박스 라이브값, fallback ActiveLayer.No) → `SeqScoutNormal.cs:151`에서 `_InitParam.PageIndex`로 로드 → 호출부 2곳(`UCScannerObjectTeachingControlBoxViewModel.cs:251`, `UCTestSequenceViewModel.cs:361`)에서 주입 +- **null 가드 확정**: `LoadScoutField(PageIndex) ?? LoadScoutField() ?? new List<>()` (인덱스 초과 시 NRE 방지, 빈 리스트면 스캔 대상 없이 정상 종료) +- B안(GUI 콤보박스 동기화)은 위험성으로 제외 +- 기존 소스는 주석 처리 / 신규·변경 라인은 의도 주석 표기 형식으로 작성 (Before/After 포함) +- **상태: 도연님이 더블체크 후 git 반영 예정 → 추후 pull 받아 적용** (이번엔 소스 변경/커밋 없음) + +## 주말 출근 예정 +- **06.20(토)~06.21(일) 주말 출근 예정** diff --git a/docs/작업지시서/2026-06-19_ScoutField_페이지인덱스_시퀀스주입.md b/docs/작업지시서/2026-06-19_ScoutField_페이지인덱스_시퀀스주입.md new file mode 100644 index 0000000..fb59080 --- /dev/null +++ b/docs/작업지시서/2026-06-19_ScoutField_페이지인덱스_시퀀스주입.md @@ -0,0 +1,198 @@ +# 작업지시서 — 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`) 변경 없음 → 데이터 롤백 불필요.