LGES-OC5-PtoP-Welder/docs/작업지시서/작업지시서_2026-08-03_Laser_Recipe_Save_교차오염.txt
2026-08-06 11:29:00 +09:00

403 lines
22 KiB
Plaintext

================================================================================
작업지시서 — Laser Recipe Save 교차 오염 (선택 레시피에 Active 레시피가 덮어써짐)
================================================================================
작성일 : 2026-08-03
대상 : LGES-OC5-PtoP-Welder / ILSOS_PrismaticRD_PTOP
분류 : 버그 수정 (데이터 손실)
원본분석 : docs/분석/2026/08/03/173852_Laser_Recipe_Save_교차오염_분석.md
상태 : 소스 미수정. 본 지시서대로 작업 PC에서 적용할 것.
[!] 경고 1 - 본 작업은 WeldingComponents 하위 파일을 수정한다.
프로젝트 규약상 WeldingComponents / External/Aurora 수정은
반드시 사전 승인이 필요하다. 승인 없이 적용하지 말 것.
[!] 경고 2 - 한글 포함 파일 수정 시 UTF-8 인코딩을 유지할 것.
(기존 파일 인코딩을 그대로 두고 편집. 새 인코딩으로 저장 금지)
[!] 경고 3 - 아래 1단계(가드)를 먼저 단독 적용하고 현장 로그를 확보한 뒤
2~4단계를 적용할 것. 한 번에 전부 넣지 말 것.
================================================================================
1. 증상
================================================================================
- 레시피 저장 경로 : D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\_config_recipe
- Laser Recipe를 Save하면 "어쩌다 한 번씩" Active 레시피의 내용이
현재 선택된 레시피에 덮어써진다.
- 선택 레시피의 Page와 Param이 전부 Active 레시피 것으로 바뀐다.
- AutoRun 뿐 아니라 Manual 상태에서도 발생했다고 보고됨.
================================================================================
2. 원인 요약
================================================================================
저장 "대상"과 저장 "내용"이 서로 다른 전역 변수이고, 둘을 묶는 검사가 없다.
저장 내용(소스) : LASA/GUI/DB/Dao.cs:7
public static RecipeWeldingLines WeldingLinesRecipe
-> 프로세스 전역 싱글톤 1개
저장 대상(경로) : WeldingCommon/Utils/RecipeHelper.cs:18
public static (int id, string name) SelectedRecipe
Page 데이터 : LASA/GUI/DB/RecipeFieldScan.cs:18-22 Instance (또 다른 싱글톤)
MainPage.SaveRecipe(fullpath) (LASA/GUI/Page/MainPage.cs:425-434) 는
"지금 메모리에 있는 것을 지정 경로에 덤프"할 뿐, 그 메모리가 그 경로의
레시피인지 확인하지 않는다.
확인할 수단은 이미 있는데 안 쓰고 있다:
MainPage.cs:466 ReloadRecipe 가 DB.Dao.SystemRecipe.WorkRecipeFile = fullpath
로 "메모리에 든 레시피"를 기록한다.
그러나 WorkRecipeFile 을 읽는 곳은 MainPage.cs:537 / 547 /
RecipeLaserInterlock.cs:25 뿐이고 SaveRecipe 는 조회조차 하지 않는다.
여기에 저장 대상이 UI 인자가 아니라 static 필드로 정해진다:
UCScannerObjectTeachingViewModel.cs:202-208
public void OnSaved(int id, string recipeName)
-> 인자 id / recipeName 을 한 번도 참조하지 않고
RecipeHelper.SelectedRecipe.name 으로 저장한다.
=> 두 전역이 어긋난 순간, Save 한 번이 곧 레시피 교차 복사가 된다.
Page 까지 같이 바뀌는 이유도 동일하다 (MainPage.cs:433 이 싱글톤
RecipeFieldScan.Instance 를 대상 레시피의 FieldHelper.json 에 쓴다).
[ 유력 발생 경로 - 우선순위 순 ]
(1) 정상 경로의 부분 실패 <= 최유력. 트리거(Save 클릭)와 간헐성이 모두 일치
아래 세 곳 모두 "대상을 먼저 바꾸고 내용을 나중에" 로드한다. 원자적이지 않다.
RecipeHelper.cs:76-78 SelectedRecipe = ... -> ReloadRecipe(...)
UCScannerObjectTeachingViewModel.cs:165-166 SelectedRecipe = ... -> ReloadRecipe(...)
UCScannerObjectTeachingViewModel.cs:172-173 SelectedRecipe = ... -> ReloadRecipe(...)
ReloadRecipe 가 예외로 끝나거나 조용히 무동작하면
-> 대상 = 새 레시피 / 내용 = 직전(대개 Active) 그대로
-> 다음 Save 가 그대로 교차 복사.
ReloadRecipe 실패/무동작 지점:
RecipeHelper.cs:66-67 GetDeviceConfig(CAM1) as CamConfig 가 null 이면 NRE
MainPage.cs:464 -> LASA/DB/Recipe/Recipe.cs:42-70 파일IO/파싱 실패 시 throw
MainPage.cs:474 UpdateWeldingLinesRecipe() -> MainPage.cs:409-417 throw
MainPage.cs:485 -> RecipeFieldScan.cs:88 throw
Recipe.cs:64 루트 자식 노드 이름이 RecipeWeldingLines 가 아니면
"무예외 무동작"으로 성공 반환 (가장 위험)
RecipeWeldingLines.cs:68-75 예외를 삼킴
-> 파라미터는 새 것, 페이지는 옛 것인 혼합 상태
(2) PLC 레시피 전환 시 역방향 오염 (신규 Active 파일이 구 레시피 내용으로 덮어써짐)
UCScannerObjectTeachingViewModel.cs:155-156
var activeRecipe = _RecipeManager.ActivatedRecipe();
RecipeHelper.SaveRecipe(activeRecipe.Name); // 이 시점 메모리는 아직 구 레시피
SeqAutoRunPTOP.cs:403 이 백그라운드 스레드에서 ActivateRecipe 호출
-> SYS_RECIPE_ACTIVATED 가 ObserveOnDispatcher 로 UI 큐 1홉
-> 위 SaveRecipe 가 먼저 실행되고, 메모리 갱신(SeqAutoRunPTOP.cs:425)은 2홉 뒤.
같은 1홉/2홉 시차는 이미 다른 개발자가 발견해 주석으로 남겨 두었다
(UCScannerObjectTeachingControlBoxViewModel.cs:193-195).
다만 자기 화면만 회피했고 :156 의 SaveRecipe 는 손대지 않았다.
(3) Manual 복귀 시 대상 미복구
EquipmentSetup.cs:276-283 - Auto 진입 시 OnActiveReloaded()(279)로
SelectedRecipe 가 Active 로 바뀌지만, Manual 복귀 분기의
RollbackRecipe() 는 주석 처리(282) 되어 있다.
[중요] 이 주석을 풀어도 고쳐지지 않는다.
RollbackRecipe(RecipeHelper.cs:105-110)는 ReloadRecipe(SelectedRecipe.name)
인데, Auto 진입 시 OnActiveReloaded(RecipeHelper.cs:76)가 이미
SelectedRecipe 를 Active 로 덮어써서 되돌릴 원본이 없다(백업 필드 0건).
=> "Active 를 Active 로 다시 로드"하는 의미 없는 동작.
=> 282행 주석 해제는 이번 작업 범위에서 제외한다.
(4) 신규 레시피 생성 시 메모리 미갱신 저장
UCScannerObjectTeachingViewModel.cs:114-115
SelectedRecipe = 신규 직후, 메모리 재로드 없이 SaveRecipe(신규) 호출.
[ 원인이 아닌 것 - 같은 오답 반복 방지 ]
RMSHandlerForPTOP 은 원인이 아니다. 한 번도 등록되지 않는다.
EquipmentSetup.cs:95 //AuroraEx.RegisterRmsHandler<RMSHandlerForPTOP>();
EquipmentSetup.cs:96 //AuroraEnv.RecipeManager.RmsHandler().RegistryDeviceHandler(AuroraEnv);
EquipmentSetup.cs:97 //AuroraEnv.RecipeManager.RmsHandler().Start();
RegisterRmsHandler 호출은 저장소 전체에서 이 3줄(전부 주석)이 전부다.
따라서 RMSHandlerForPTOP.cs:62-66 / :103-149 의 레이스는 실행되지 않는다.
여기를 고쳐도 증상은 그대로 재발한다.
1차 분석의 "PLC Word 를 읽어서 메모리에 Active 가 남는다" 도 인과가 틀렸다.
AutoRun 의 PLC_RECIPE_NO 읽기(SeqAutoRunPTOP.cs:346-425)는 OnActiveReloaded()로
대상과 내용을 함께 갱신하므로 신고된 방향의 오염을 만들지 못한다.
(단 "메모리에 Active 가 남는다"는 상태 서술 자체는 맞다. 경로가 다를 뿐이다.)
================================================================================
3. 수정 작업 - 1단계 (최우선. 이것부터 단독 적용)
================================================================================
목적 : 잘못된 저장을 실제로 차단하고, 원인 확정을 위한 로그를 남긴다.
이것만 적용해도 데이터 손실은 멈춘다.
파일 : WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs
[현재 36~39행]
public static void SaveRecipe(string recipeName, bool includeModel = true)
{
CommonHelper.GetMainPage().SaveRecipe(GetTeachingEditorFullPath(recipeName), includeModel);
}
[변경안]
public static void SaveRecipe(string recipeName, bool includeModel = true)
{
var targetPath = GetTeachingEditorFullPath(recipeName);
// [추가 2026-08-03] 저장 대상과 메모리에 로드된 레시피가 다르면 저장 거부.
// MainPage.SaveRecipe 는 전역 싱글톤(DB.Dao.WeldingLinesRecipe)을 지정 경로에
// 그대로 덤프하므로, 둘이 어긋나면 레시피가 교차 복사된다.
// ReloadRecipe(MainPage.cs:466)가 기록해 둔 WorkRecipeFile 이 유일한 신원 정보다.
var loadedPath = DB.Dao.SystemRecipe.WorkRecipeFile;
if (!string.IsNullOrWhiteSpace(loadedPath) &&
!string.Equals(System.IO.Path.GetFullPath(loadedPath),
System.IO.Path.GetFullPath(targetPath),
System.StringComparison.OrdinalIgnoreCase))
{
// 저장하지 않고 중단 - 교차 오염 차단
UtilUnit.logUnit.SetLog(LogUnit.eLogType.ALARM, "RecipeHelper.SaveRecipe",
$"저장 거부 - 메모리 레시피와 저장 대상 불일치. loaded=[{loadedPath}] target=[{targetPath}]");
return;
}
CommonHelper.GetMainPage().SaveRecipe(targetPath, includeModel);
}
[구현 시 확인 사항]
- DB.Dao.SystemRecipe.WorkRecipeFile 이 WeldingCommon 프로젝트에서 참조 가능한지
빌드로 먼저 확인할 것. RecipeHelper.cs 상단에 이미 using DB; using DB.Recipe; 가 있으나
Dao.SystemRecipe 는 LASAGUI 쪽 partial class 이므로 참조가 안 될 수 있다.
=> 참조 불가 시 대안: 동일한 가드를 LASA/GUI/Page/MainPage.cs 의 SaveRecipe(425행)
맨 앞에 넣는다. 그 파일 안에서는 DB.Dao.SystemRecipe 접근이 확실히 가능하다
(MainPage.cs:466 에서 이미 쓰고 있음).
- 로그 API(UtilUnit.logUnit.SetLog) 역시 WeldingCommon 에서 접근 안 되면
Logger 또는 MessageUtil.ShowErrorToast 로 대체할 것. 중요한 것은
"저장을 중단한다" + "불일치 사실을 로그로 남긴다" 두 가지다.
- loadedPath 가 비어 있을 때(초기 기동 등)는 기존 동작 유지(저장 허용).
처음부터 막으면 정상 저장까지 못 하게 된다.
[1단계 적용 후 반드시 할 것]
- 현장에서 며칠 운용하며 ALARM 로그에 "저장 거부 - 메모리 레시피와 저장 대상 불일치"
항목이 찍히는지 확인한다.
- 찍히면 loaded / target 값으로 어느 경로에서 어긋났는지 확정된다.
그 결과를 보고 2~4단계 중 필요한 것만 적용한다.
================================================================================
4. 수정 작업 - 2단계 (1단계 로그 확보 후)
================================================================================
목적 : 원인 (1) 차단. 대상과 내용의 갱신 순서를 바로잡는다.
"내용 로드가 성공한 뒤에만 대상을 바꾼다".
[2-A] WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs 73~79행
[현재]
public static void OnActiveReloaded()
{
var activeRecipe = mRecipeManager.ActivatedRecipe();
SelectedRecipe = (activeRecipe.Id, activeRecipe.Name);
ReloadRecipe(activeRecipe.Name);
}
[변경안]
public static void OnActiveReloaded()
{
var activeRecipe = mRecipeManager.ActivatedRecipe();
// [변경 2026-08-03] 로드 성공 후에 대상(SelectedRecipe)을 갱신한다.
// 먼저 대입하면 ReloadRecipe 실패 시 대상만 새 레시피이고
// 메모리는 직전 레시피인 상태로 남아, 다음 Save 가 교차 복사가 된다.
ReloadRecipe(activeRecipe.Name);
SelectedRecipe = (activeRecipe.Id, activeRecipe.Name);
}
[2-B] WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs
163~174행
[현재]
public void OnChanged((int id, string name) recipe)
{
RecipeHelper.SelectedRecipe = recipe;
RecipeHelper.ReloadRecipe(recipe.name);
_RecipeBeforeChanged = RecipeHelper.getRecipeInfo(recipe.name);
}
public void Reloaded((int id, string name) recipe)
{
RecipeHelper.SelectedRecipe = recipe;
RecipeHelper.ReloadRecipe(recipe.name);
}
[변경안]
public void OnChanged((int id, string name) recipe)
{
// [변경 2026-08-03] 로드 성공 후에 대상 갱신 (교차 저장 방지)
RecipeHelper.ReloadRecipe(recipe.name);
RecipeHelper.SelectedRecipe = recipe;
_RecipeBeforeChanged = RecipeHelper.getRecipeInfo(recipe.name);
}
public void Reloaded((int id, string name) recipe)
{
// [변경 2026-08-03] 로드 성공 후에 대상 갱신 (교차 저장 방지)
RecipeHelper.ReloadRecipe(recipe.name);
RecipeHelper.SelectedRecipe = recipe;
}
[주의] 순서를 바꾸면 ReloadRecipe 가 예외를 던졌을 때 SelectedRecipe 가
직전 값으로 남는다. 이는 의도된 동작이다(메모리 내용과 일치하므로).
다만 화면 선택과 SelectedRecipe 가 달라질 수 있으므로,
예외 발생 시 사용자에게 토스트로 알리는 것을 함께 검토할 것.
================================================================================
5. 수정 작업 - 3단계
================================================================================
목적 : 원인 (2) 차단. PLC 레시피 전환 시 신규 Active 파일이
구 레시피 내용으로 덮어써지는 것을 막는다.
파일 : WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs
133~161행 OnActivatedRecipe
[현재 155~156행]
var activeRecipe = _RecipeManager.ActivatedRecipe();
RecipeHelper.SaveRecipe(activeRecipe.Name);
[변경안 - 택1]
(A안, 권장) SaveRecipe 호출 제거
var activeRecipe = _RecipeManager.ActivatedRecipe();
// [변경 2026-08-03] SYS_RECIPE_ACTIVATED 1홉 시점에는 메모리가 아직 구 레시피다.
// 여기서 저장하면 신규 Active 파일이 구 레시피 내용으로 덮어써진다.
// 저장이 필요하면 레시피 전환 전에 별도로 수행할 것.
// RecipeHelper.SaveRecipe(activeRecipe.Name);
(B안) 3단계 1단계 가드가 이미 들어가 있다면 그대로 두어도 가드가 막는다.
단 이때 ALARM 로그가 레시피 전환마다 찍히므로 로그 노이즈를 감수해야 한다.
A안이 더 깨끗하다.
[주의] 이 호출을 지우면 "레시피 활성화 시 자동 저장" 동작이 사라진다.
해당 동작에 의존하는 곳이 있는지 확인할 것.
RMSHandlerForPTOP.cs:171 의 주석이 이 연쇄를 의도된 설계로 기술하고 있으나,
그 RMS 핸들러 자체가 죽어 있으므로(2절 참조) 실사용 의존은 없을 가능성이 높다.
=> 현장 확인 필요.
================================================================================
6. 수정 작업 - 4단계 (선택)
================================================================================
목적 : 원인 (4) 차단. 신규 레시피 생성 시 직전 레시피 내용이 기록되는 것을 막는다.
파일 : WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs
110~115행 OnRecipeEntryChanged 의 New 분기
[현재]
if (param.AddedId != -1) { //New
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == param.AddedId);
if (recipe == null) return;
RecipeHelper.SelectedRecipe = (recipe.Id, recipe.Name);
RecipeHelper.SaveRecipe(recipe.Name, includeModel: false);
}
[변경안]
if (param.AddedId != -1) { //New
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == param.AddedId);
if (recipe == null) return;
// [변경 2026-08-03] 메모리를 새 레시피로 먼저 맞춘 뒤 저장한다.
// 그렇지 않으면 직전 레시피 내용이 신규 파일에 기록된다.
RecipeHelper.ReloadRecipe(recipe.Name);
RecipeHelper.SelectedRecipe = (recipe.Id, recipe.Name);
RecipeHelper.SaveRecipe(recipe.Name, includeModel: false);
}
[주의] 신규 생성 직후 파일이 아직 없거나 비어 있으면 ReloadRecipe 가
실패할 수 있다. 이 경우 try/catch 로 감싸고 실패 시 저장을 건너뛸 것.
빈 레시피를 만드는 것이 목적이라면 SaveRecipe 자체가 불필요할 수도 있다.
=> 실동작 확인 후 적용. 4단계는 필수가 아니다.
================================================================================
7. 이번 작업에서 하지 않을 것
================================================================================
- RMSHandlerForPTOP 수정 : 죽은 코드다. 고쳐도 증상은 재발한다.
- EquipmentSetup.cs:282 RollbackRecipe 주석 해제 : 의미 없는 동작이다.
되돌릴 원본(Auto 진입 이전 선택 레시피)을 백업하는 코드가 없기 때문이다.
진짜 롤백이 필요하면 별도 백업 필드 신설이 선행되어야 하며 별건으로 다룬다.
- SeqAutoRunPTOP.cs 의 CheckRecipeNoComp 수정 : 별건(2026-06-21 분석 참조).
================================================================================
8. 검증 절차
================================================================================
[빌드]
1) ILSOS_All.sln 을 Visual Studio 2022 로 열고 Release|x64 빌드.
2) 경고를 "사소한 것"으로 넘기지 말 것. 빌드 실패 시 진행 중단.
[기능 확인 - 정상 경로가 깨지지 않았는지]
3) Manual 상태에서 Recipe 화면 진입 -> 레시피 A 선택 -> 파라미터 수정 -> Save
-> A 파일만 바뀌었는지 확인 (B, Active 레시피 파일 타임스탬프 불변 확인)
4) 레시피 B 선택 -> Save -> B 파일만 바뀌는지 확인
5) 레시피 전환(A->B->A)을 10회 반복 후 A/B 내용이 서로 섞이지 않았는지 확인
6) AutoRun 기동 -> PLC 레시피 전환 발생 -> Manual 복귀 -> Recipe 화면에서
선택 레시피 Save -> 엉뚱한 파일이 바뀌지 않는지 확인
[로그 확인]
7) ALARM 로그에 "저장 거부 - 메모리 레시피와 저장 대상 불일치" 가 찍히는지 확인.
찍힌다면 loaded / target 경로를 기록해 둘 것. 원인 경로 확정 근거가 된다.
8) 함께 확인할 기존 ALARM 항목:
ReloadRecipe / UpdateWeldingLinesRecipe / LoadFieldHelper
사고 시각 근처에 이 항목이 있으면 원인 (1)이 확정된다.
[백업]
9) 작업 전 D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\_config_recipe 폴더를
통째로 복사해 둘 것. 검증 중 레시피가 오염될 수 있다.
================================================================================
9. 확인이 필요한 항목 (미확정 - 추측으로 메우지 말 것)
================================================================================
1) 현장 PC 에 배포된 바이너리가 이 소스와 동일한지 미확인.
다르면 "RMSHandlerForPTOP 은 죽은 코드" 판정이 뒤집힌다.
=> 설비 로그에서 아래 문자열을 검색해 판별할 것.
"Complete OnParameterReadRequest"
"RMS Change Recipe Parameter"
하나라도 나오면 RMS 핸들러가 살아 있는 빌드이므로 재분석 필요.
2) 원인 (1)의 실제 발생 여부 미측정. 3단계 1단계 가드 로그로 확정한다.
3) Aurora 바이너리(External/Aurora/bin)의 UCRecipe 호출 규약 미확인.
OnChanged / Reloaded / OnSaved 가 언제 호출되는지 소스가 없어 알 수 없다.
PreSaved 가 false 를 반환할 때 Aurora 가 실제로 저장을 중단하는지도 미확인.
4) UCScannerObjectTeachingViewModel.cs:189-192 가 Auto 상태 Save 를 막고 있다.
따라서 "AutoRun 에서도 발생" 이라는 현장 보고 자체가 검증 대상이다.
=> 발생 당시 설비가 정말 Auto 였는지 재확인 요청.
5) _config_recipe 실물 파일 구조 미보유 -> Recipe.cs:64 무동작 가설 미검증.
================================================================================
10. 변경 파일 요약
================================================================================
1단계 : WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs (36-39행)
또는 WeldingComponents/LASA/GUI/Page/MainPage.cs (425행)
2단계 : WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs (73-79행)
WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs (163-174행)
3단계 : WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs (155-156행)
4단계 : WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs (110-115행)
전부 WeldingComponents 하위 => 사전 승인 필요.
ILSOS_PrismaticRD_PTOP 소스는 이번 작업에서 수정하지 않는다.
================================================================================
끝.
================================================================================