docs: 레시피 저장 교차오염 트리거 차단 인수인계 문서 (ISSUE-260904-02)

This commit is contained in:
노도연 2026-09-04 14:19:16 +09:00
parent bd6a7cd21f
commit 3584cacc6e

View File

@ -0,0 +1,245 @@
# 레시피 저장 교차오염 트리거 차단 (2026-09-04)
> **목적** : 액티브 전환 시 엉뚱한 레시피 파일에 다른 레시피 내용이 덮어써지던 건의
> 조사 결과와 적용한 차단 조치를 남긴다.
>
> - 대상 설비 : PtoP 각형화 단동 용접기 (유니셀)
> - 이슈번호 : `ISSUE-260904-02` (원인 이슈 `ISSUE-260826-04` 의 조치)
> - 커밋 : `bd6a7cd`
> - 선행 분석 : `docs/분석/2026/08/26/레시피_Drawing_덮어쓰기_원인분석_260826.md`,
> `docs/분석/2026/08/27/레시피_파일IO_동작별_순서맵_260827.md`
**표기 규칙** (기존 인수인계 문서와 동일)
| 표기 | 의미 |
|---|---|
| 🟩 **소스확인** | PC 소스/설정 파일에서 직접 대조해 확인한 사실 |
| 🟦 **현장기록** | 현장 로그·시험 기록에 남아 있는 값 |
| 🟨 **미확보** | 아직 확인되지 않음. 이어받는 사람이 채워야 할 항목 |
---
## 1. 한 줄 요약
**저장 "대상"과 저장 "내용"이 묶여 있지 않다.** LASA 는 레시피를 메모리에 1개만 유지하므로
활성 레시피가 바뀌는 순간 메모리 내용이 통째로 바뀌는데, 저장 함수는 목적지를 **이름 문자열로만**
만들고 내용은 **그 시점 메모리**를 쓴다. 둘이 같은 레시피인지 검증하는 코드가 없다.
이번 조치는 **트리거 차단**이다. 구조적 원인은 그대로 남아 있다.
---
## 2. 현상
- 오창 선행라인 CCW 설비에서 발생. PTOP 에서도 동일 계열 이슈 있었음.
- 레시피 10번을 액티브로 쓰던 중 갑자기 2번으로 액티브가 바뀌고,
**원래 쓰던 10번 파일 안에 2번 내용이 복사되어 덮어써짐.**
- 간헐적. Auto↔Manual 전환 시 특히 잦음.
### 2.1 방향이 범인을 특정한다 🟩
"**10번 파일에 2번 내용**" — 목적지가 10(선택), 내용이 2(활성)다.
| 저장 기준 | 만들어지는 결과 |
|---|---|
| **활성** 기준 저장 | 2번 파일에 10번 내용 ← 보고와 **반대** |
| **선택** 기준 저장 | **10번 파일에 2번 내용** ← 보고와 일치 |
→ 선택(`SelectedRecipe`) 기준으로 저장하는 경로가 범인이다.
---
## 3. ★ 사수 지시와 PTOP 소스가 다르다 🟩
CCW 기준으로 받은 지시 두 가지는 **PTOP 소스에 그 형태로 존재하지 않는다.**
그대로 주석 처리하면 아무것도 바뀌지 않는다. 이어받는 사람이 반드시 알아야 할 부분이다.
| 지시 | PTOP 실제 |
|---|---|
| ① `OnSaveRecipeForEditing()` 호출부 전부 주석 처리 | **호출부 0건.** `RecipeHelper.cs:71` 에 정의만 있는 **사문**. AZ 소스처럼 이미 제거된 상태 |
| ② AutoRun 시작 시 "선택 레시피 선저장" 로직 차단 | **선저장이 없다.** `RunSequenceAutoRun:344` → `OnUpdateCurrentActivatedRecipe` → `OnActiveReloaded()` + `UpdateAutorunRecipe()` 이며 **둘 다 읽기 전용** |
Auto↔Manual 전환 경로에도 저장이 없다.
| 경로 | 코드 | 동작 |
|---|---|---|
| Auto 진입 | `EquipmentSetup.cs:286` `OnActiveReloaded()` | 읽기 |
| Manual 진입 | `EquipmentSetup.cs:289` `//RollbackRecipe()` | 이미 주석 |
| AutoRun 정지 | `UCMainAutoRunViewModel.cs:462` `RollbackRecipe()` | 읽기 |
> 사수님의 **구조 진단(선택 기준 저장이 범인)은 정확하다.** 다만 PTOP 에서 그 역할을 하는 함수 이름이
> 다를 뿐이다.
---
## 4. 저장이 실제로 일어나는 경로 5개 (전수) 🟩
| # | 위치 | 저장 목적지 | 트리거 | 이번 조치 |
|---|---|---|---|---|
| **S1** | `UCScannerObjectTeachingViewModel.cs` `OnActivatedRecipe` | **활성** | `SYS_RECIPE_ACTIVATED` 마다 **무조건** | ✅ 조건부화 |
| **S2** | 같은 파일 `OnSaved` | **선택** | Aurora 저장 버튼 | ✅ 가드 |
| S3 | 같은 파일 `OnRecipeEntryChanged` | 신규 레시피 | 레시피 New | ❌ 미경유 (의도적) |
| S4 | `RMSHandlerForPTOP.cs:146` | recipe.Name | RMS 레시피 체인지 | ❌ 미경유 |
| S5 | `RecipeHelper.cs:77` `OnSaveRecipeForEditing` | 선택 | **호출 없음** | 사문 |
**S1 이 사용자 조작 없이 발화한다.** 260825 로그 실측 🟦 — 저장 조작이 0건인 16분 구간에
`from [OnActivatedRecipe]` 로그가 8줄, 즉 **자동 저장 8회**.
**S4(RMS 읽기)만 순서가 올바르다** — `Reload → 수정 → Save`(`RMSHandlerForPTOP.cs:132~147`).
수정 시 참조 모델로 쓸 것.
---
## 5. 저장 주체는 Aurora 가 아니다 🟩
"SaveRecipe" 라는 이름이 3개라 헷갈린다.
| 계층 | 위치 | 소스 | 우리가 고칠 수 있나 |
|---|---|---|---|
| ① Aurora 내부 저장 | `Aurora.exe` | ❌ 없음 | ❌ |
| ② `RecipeHelper.SaveRecipe(name, includeModel)` | `WeldingCommon/Utils/RecipeHelper.cs:38` | ✅ | ✅ |
| ③ `MainPage.SaveRecipe(targetPath, includeModel)` | `LASA/GUI/Page/MainPage.cs` | ✅ | ✅ |
②가 ③을 호출한다(`RecipeHelper.cs:57`). **①은 우리 TeachingEditor/FieldHelper 파일 저장에 관여하지 않는다.**
### 5.1 Aurora 는 콜백만 준다 — 그런데 정답을 이미 넘겨주고 있다
`IRecipeViewModel` (`Aurora.Api`, `Aurora.xml:3989~4030`) — *"커스텀 레시피 뷰어를 구현할 때 상속받는 인터페이스"*.
`UCScannerObjectTeachingViewModel : NotifyPropertyChangedBase, IRecipeViewModel`.
| 멤버 | 시점 |
|---|---|
| `PreSaved(int id, string recipeName)` | Save 되기 **전** 호출. **`false` 반환 시 저장 중단** 🟦 |
| `OnSaved(int id, string recipeName)` | Save 된 **후** 호출 |
| `OnChanged` / `Reloaded` / `OnModelSelectChanged` | 뷰어 레시피 변경·리로드·모델 변경 |
저장소 어디에도 `PreSaved`/`OnSaved` 호출부가 없다. Aurora 가 부른다.
**★ `OnSaved` 가 Aurora 인자를 버린다.**
```csharp
public void OnSaved(int id, string recipeName) // ← Aurora 가 알려준 "진짜 저장 대상"
{
...
RecipeHelper.SaveRecipe(RecipeHelper.SelectedRecipe.name); // ← 인자 무시, 전역값 사용
```
`RecipeTrace.cs:104` 주석에도 *"OnSaved 가 인자를 버리는 경로"* 라고 이미 적혀 있었다.
**어긋남을 판정할 기준값이 이미 손에 들어와 있다.**
---
## 6. 이벤트 체인 — 어긋남을 처리하는 곳은 없다 🟩
**`SYS_RECIPE_ACTIVATED`** (Aurora 발행) 구독 5곳
| 구독 위치 | 하는 일 | 저장 |
|---|---|---|
| `UCScannerObjectTeachingViewModel.cs:64` | ActivateModel + **SaveRecipe(활성)** + 이벤트 재발행 | ✅ S1 |
| `UCScannerObjectTeachingControlBoxViewModel.cs:224` | LaserInterlock 로드 (Background 지연) | ❌ |
| `CommonEquipmentSetup.cs:114` | Merge → **로그 전용** | ❌ |
| `VisionCommon/VisionEventId.cs:47,62,74,84` | 비전 Merge | ❌ |
| `AuroraUIComponent/UCRecipeSelectViewModel.cs:74` | SelectRecipe — **PTOP 생성 0건, 사문** | ❌ |
**`EVENT_CHANGE_RECIPE_INFO`** (우리 발행 — `OnActivatedRecipe`, `OnSaved`) 구독 2곳
| 구독 위치 | 하는 일 | 저장 |
|---|---|---|
| `UCMainAutoRunViewModel.cs:142` | `OnActiveReloaded()` + `UpdateAutorunRecipe()` | ❌ |
| `RMSHandlerForPTOP.cs:56` | `ReloadRecipe()` + PLC 파라미터 업로드 | ❌ |
**결론: 발행·구독 어느 지점에도 신원 검증이 없다.** 전부 넘어온 값을 그대로 따라간다.
---
## 7. 적용한 조치 — 파일 1개, 블록 3개
`WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs`
마커 `[레시피 덮어쓰기 이슈]`.
### ① `OnActivatedRecipe` — 조건부 저장 + 스킵 로그
```csharp
if (string.Equals(activeRecipe.Name, selectedNameOnActivate, StringComparison.OrdinalIgnoreCase)) {
RecipeHelper.SaveRecipe(activeRecipe.Name);
} else {
Log.W($"[레시피 덮어쓰기 차단] OnActivatedRecipe 자동저장 스킵. ...");
}
```
> **왜 완전 삭제가 아닌가** — `OnRecipeEntryChanged` 의 **Copy/Rename 분기가 비어 있다**(`:118-121`).
> 이 저장이 새 레시피 폴더의 TeachingEditor 파일 생성을 겸했을 가능성이 있고, 파일이 없으면
> `ReloadRecipe` 의 `throw;`(`RecipeHelper.cs:116`)가 Rx `Subscribe` 안에서 터져
> **이후 `SYS_RECIPE_ACTIVATED` 를 영구히 못 받는다.** 레시피 전환이 통째로 죽는 모드다.
### ② `PreSaved` — 활성≠선택이면 저장 차단 + 모달
`return false` 로 저장을 막고, 사유와 조치 방법을 담은 확인 모달을 띄운다.
### ③ `OnSaved` — 동일 검사 2중 방어 + 모달
`PreSaved` 를 뚫고 온 경우를 대비. **조용히 스킵하면 사용자가 "저장됐다"고 오인**하므로 반드시 알린다.
**`RecipeHelper`·`MessageUtil` 무수정, 신규 파일 없음, 컴파일 영향 없음.**
---
## 8. 현장 검증 (2026-09-04)
| 항목 | 결과 |
|---|---|
| 활성≠선택 상태에서 저장 → 모달 + 저장 차단 | 🟦 **정상 동작 확인** |
| 모달에 선택/활성 값 표시 | 🟦 정상 (`FindStrToken` 통과 확인) |
| **`PreSaved`=false 시 Aurora 가 실제로 저장 중단** | 🟦 **확증** — 소스 부재로 추정만 하던 사항 |
| 평소 저장(활성=선택)이 막히지 않는지 | 🟨 미확인 |
| **레시피 복사·이름변경 → 활성화** | 🟨 **미확인 (최우선)** |
| 자동 사이클 페이지 전환 | 🟨 미확인 |
> `PreSaved` 가 `false` 를 반환하면 Aurora 가 **`Save Failed` 모달을 추가로 띄운다**
> (`AuroraUIComponent.ShowSaveFailedDialog`). 모달이 2개 뜨지만, 우리 것은 *사유와 조치*를
> Aurora 것은 *실패 사실*을 알리므로 역할이 겹치지 않아 현행 유지하기로 했다.
---
## 9. 남아 있는 위험 🟨
1. **오탐** — `IRecipeViewModel.OnChanged`(`:163`)는 `SelectedRecipe` 만 바꾸고 활성은 건드리지 않는다.
Aurora 가 **활성화 없이 뷰어만 전환**하는 흐름을 가졌다면, 정상 상황인데도 저장이 막힌다.
막히면 판정 조건을 **로드 파일 신원**(`DB.Recipe.Recipe.SelectedRecipeFile`) 기준으로 바꾸면 된다.
그 비교 코드는 `RecipeHelper.cs:48-55` 에 `[관측]` 로그로 **이미 들어가 있다**(차단만 안 할 뿐).
2. **Copy/Rename 분기 공백** — 위 §7 ① 참조. 배포 상태에서 반드시 1회 시험할 것.
3. **근본 원인 미해결** — `SaveRecipe` 가 목적지와 내용을 묶어 검증하지 않는다.
가드를 안 거치는 S3(신규생성)·S4(RMS)에는 같은 구멍이 남는다.
근본 대응은 `SaveRecipe` 에 불변식을 넣는 것인데, S3 가 **의도적으로** 신원을 위반하므로
(`includeModel:false` 로 현재 메모리를 새 파일에 씀) `allowCrossIdentity` 같은 의도 인자 분리가 필요하다.
4. **진단계측이 미연결** — `WeldingCommon/Utils/RecipeTrace.cs` 의 `BeforeSave`/`AfterSave` 가
저장 전후 **파일 MD5** 로 교차오염을 판정하도록 완성돼 있으나 **호출이 0건**이다. 2줄이면 살아난다.
---
## 10. 로그 grep 키
| 문자열 | 의미 |
|---|---|
| `[레시피 덮어쓰기 차단] OnActivatedRecipe 자동저장 스킵` | 정상. 활성 전환마다 나온다 |
| `[레시피 덮어쓰기 차단] PreSaved 저장 차단` | 평소 저장에서 나오면 **오탐** |
| `[레시피 덮어쓰기 차단] OnSaved 저장 스킵` | Aurora 가 PreSaved 를 무시함. 이상 상황 |
| `[관측] 메모리/대상 불일치` | 기존 계측. **가드 미경유 경로**(S3·S4)의 어긋남 |
| `*** RELOAD 실패 ***` | §9-2 위험이 실재. 이후 레시피 전환 불가 가능 |
---
## 11. 운영 지침
- **레시피는 항상 "액티브된 레시피 = 선택된 레시피" 상태로 두고 사용한다.** (사수 지시)
- **AutoRun 중 Recipe 화면의 레시피 목록을 클릭하지 말 것.** 조회가 아니라 활성 변경이며 저장을 유발한다.
- drawing 객체는 변경 이력에 남지 않는다(`RchHandler` 는 ScannerParam 6필드만 기록).
**덮어써지면 복구 경로가 없다** — OS 이전 버전(VSS) 또는 수동 백업뿐이다.
---
## 변경 이력
| 일자 | 내용 | 작성 |
|---|---|---|
| 2026-09-04 | 최초 작성. 조사 결과 + 3블록 차단 조치 적용(`bd6a7cd`), 현장 차단 동작 검증 | 노도연 |