LGES-OC5-PtoP-Welder/Doc/인수인계/260904_레시피저장_교차오염_트리거차단.md

14 KiB

레시피 저장 교차오염 트리거 차단 (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 인자를 버린다.

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 — 조건부 저장 + 스킵 로그

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 가 실제로 저장 중단 🟦 확증 — 소스 부재로 추정만 하던 사항
평소 저장(활성=선택)이 막히지 않는지 🟨 미확인
레시피 복사·이름변경 → 활성화 🟨 미확인 (§9-2 재평가로 우선순위 하향)
자동 사이클 페이지 전환 🟨 미확인

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 ① 참조. 아래 2.1 에서 재평가했다.

9.1 파일 부재 시 예외는 확실히 던져진다 🟩

DB.Recipe.Recipe.Load(string) (LASA/GUI/DB/Recipe.cs:42-73) 는 .json 이면 File.ReadAllText, 아니면 new StreamReader 로 읽고 catch 에서 throw ex; 로 재전파한다. MainPage.ReloadRecipe 는 잡지 않고 RecipeHelper.ReloadRecipe:116 도 throw; 로 다시 던진다. OnActiveReloaded 호출부가 전부 Rx Subscribe 안이므로(UCMainAutoRunViewModel:174, EquipmentSetup:286, RMSHandlerForPTOP:64) 구독이 종료된다.

9.2 종전 코드는 파일 존재를 "보증"하고 있었다 🟩

OnActivatedRecipe 의 순서가 이렇다.

① SaveRecipe(활성)  ← 파일 생성 (GetTeachingEditorFullPath 가 폴더도 만듦)
② EVENT_CHANGE_RECIPE_INFO 발행
③ 구독자가 ReloadRecipe 수행

저장이 reload 보다 먼저다. 즉 무조건 저장은 교차오염의 원인이면서 동시에 파일 생성 보증을 겸하고 있었고, 이번 조건부 변경이 그 보증을 걷어냈다.

9.3 그럼에도 위험도를 낮게 본 이유 🟨

남은 미지수는 "Aurora 가 Copy/Rename 시 레시피 폴더를 통째로 복사하는가" 하나뿐이다. 복사한다면 파일이 이미 있으므로 문제가 성립하지 않는다.

  • Aurora 는 레시피를 폴더 단위로 관리하므로 폴더째 복사하는 것이 정상 동작이라고 판단했다 (2026-09-04 협의). 다만 Aurora 소스가 없어 코드로 확인한 것은 아니다 — 합리적 추정이다.
  • 설령 예외가 나더라도 재기동하면 구독이 다시 걸려 복구된다. 데이터 손상이 아니라 그 세션 동안 레시피 전환만 막히는 상태다.
  • 위 두 가지로 "발생 시 즉시 중단" 에서 일반 검증 항목으로 하향했다.

선제 방어(조건에 "대상 파일 없으면 저장 허용" 을 OR 로 추가)는 넣지 않기로 했다. GetTeachingEditorFullPath 가 private 이라 공용 RecipeHelper 를 손대야 하는데, 비용 대비 이득이 크지 않다고 판단했다. 필요해지면 이 방법이 가장 작은 조치다.

  1. 근본 원인 미해결 — SaveRecipe 가 목적지와 내용을 묶어 검증하지 않는다. 가드를 안 거치는 S3(신규생성)·S4(RMS)에는 같은 구멍이 남는다. 근본 대응은 SaveRecipe 에 불변식을 넣는 것인데, S3 가 의도적으로 신원을 위반하므로 (includeModel:false 로 현재 메모리를 새 파일에 씀) allowCrossIdentity 같은 의도 인자 분리가 필요하다.
  2. 진단계측이 미연결 — 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), 현장 차단 동작 검증 노도연