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)가 RxSubscribe안에서 터져 이후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. 남아 있는 위험 🟨
- 오탐 —
IRecipeViewModel.OnChanged(:163)는SelectedRecipe만 바꾸고 활성은 건드리지 않는다. Aurora 가 활성화 없이 뷰어만 전환하는 흐름을 가졌다면, 정상 상황인데도 저장이 막힌다. 막히면 판정 조건을 로드 파일 신원(DB.Recipe.Recipe.SelectedRecipeFile) 기준으로 바꾸면 된다. 그 비교 코드는RecipeHelper.cs:48-55에[관측]로그로 이미 들어가 있다(차단만 안 할 뿐). - 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를 손대야 하는데, 비용 대비 이득이 크지 않다고 판단했다. 필요해지면 이 방법이 가장 작은 조치다.
- 근본 원인 미해결 —
SaveRecipe가 목적지와 내용을 묶어 검증하지 않는다. 가드를 안 거치는 S3(신규생성)·S4(RMS)에는 같은 구멍이 남는다. 근본 대응은SaveRecipe에 불변식을 넣는 것인데, S3 가 의도적으로 신원을 위반하므로 (includeModel:false로 현재 메모리를 새 파일에 씀)allowCrossIdentity같은 의도 인자 분리가 필요하다. - 진단계측이 미연결 —
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), 현장 차단 동작 검증 |
노도연 |