LGES-OC5-PtoP-Welder/docs/작업지시서/2026-08-03_AutoRun중_비활성레시피_저장차단.md
2026-08-06 11:29:00 +09:00

289 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 작업지시서 — AutoRun 중 비활성 레시피 저장 차단 (PLC AutoStatus 게이트)
- 작성일: 2026-08-03
- 대상 브랜치: `feature/ubisam-pptx-integration` (현장 반영 시 작업 PC 브랜치 기준)
- 작업 분류: 방어 로직 추가 (레시피 파일 오염 차단) — 기능 변경 없음
- ⚠️ 본 문서는 **수정안(검토용)** 입니다. **아직 소스는 변경하지 않았습니다.** 작업 PC에서 본 지시서대로 적용하세요.
- 근거 분석: [레시피 저장 교차 오염 — 활성 레시피 ↔ 선택 레시피 상태 불일치 원인 분석](../분석/2026/08/03/레시피저장_활성vs선택_교차오염_원인분석.md)
---
## 0. 결론 — 요청하신 방식, 구현 가능합니다
> "AutoRunPTOP 시퀀스 동작 중일 때 해당 PLC의 AutoStatus bit 값을 확인해서, 활성화된 레시피 이외의 레시피는 저장이 안 되도록 막을 수 있는가?"
**가능합니다.** 신규 배선·신규 디바이스 참조 없이, **파일 1개·함수 1개(`PreSaved`)만 수정**하면 됩니다.
| 필요한 것 | 현황 |
|---|---|
| 저장 직전 가로채는 훅 | **이미 있음** — `IRecipeViewModel.PreSaved(id, recipeName)`, `false` 반환 시 Aurora가 저장 취소 |
| 저장 대상 레시피 이름 | **이미 인자로 들어옴** — `PreSaved`의 `recipeName` (Aurora.xml:4017-4023 계약) |
| 활성 레시피 | `_RecipeManager.ActivatedRecipe()` — VM이 이미 보유 |
| PLC AutoStatus bit | `_PLC.ReadBit(PLCController.eCommonBitInput.PLC_AUTO_STATUS)` = **M70501** — VM이 이미 `_PLC` 필드 보유(`UCScannerObjectTeachingViewModel.cs:34,50`) |
즉 **추가로 끌어와야 할 의존성이 하나도 없습니다.**
---
## 1. 배경 — 왜 지금 게이트가 못 막는가
`PreSaved`에 **이미 Auto 차단 코드가 있습니다.**
```csharp
// UCScannerObjectTeachingViewModel.cs:189-192 (현재 코드)
if (!IsManualStatus) {
MessageUtil.ShowErrorToast("Auto 상태에서 레시피 변경 불가", Log);
return false;
}
```
그런데 `IsManualStatus`의 출처는 **PC측 장비 상태**입니다.
```csharp
// :77
IsManualStatus = Status.EquipmentStatus == eEqpStatus.Manual;
```
**bit15 Resume 흐름이 이 게이트를 정확히 무력화합니다.**
```csharp
// UCMainAutoRunViewModel.cs:229-238
if (_SeqAutoRunPTOP.RunState == SequenceRunState.Running) _SeqAutoRunPTOP.Stop();
Aurora.Api.Control.MessageDialog.Show(...); // "용접 위치 확인해주세요."
Status.SetStatus(eEqpStatus.Manual); // ← IsManualStatus = true 가 됨
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, false); // ← PC측만 Auto OFF
```
→ **작업자가 가변 용접을 하도록 일부러 Manual로 내려주는 흐름**이므로 `IsManualStatus == true`,
따라서 기존 게이트는 통과되고 **어떤 레시피든 저장 가능한 상태**가 됩니다.
이때 라인(PLC)은 여전히 Auto 이고, 활성 레시피는 PLC가 지정한 것(`ActivateRecipe`, UCMainAutoRunViewModel.cs:246)입니다.
**2026-08-01 현장 상황이 정확히 이 구간입니다.** 활성=A(구 캘 레시피)인데 작업자는 B(신규 복사본)를 티칭·저장 → 캔버스/저장대상 불일치로 B 파일이 A 내용으로 덮어써짐.
> **핵심**: `IsManualStatus`(PC 상태)로는 "라인이 Auto 운전 중인가"를 알 수 없습니다.
> 그래서 **PLC측 원본 신호(M70501)를 직접 보는 것이 맞습니다.** 요청하신 방향이 정확합니다.
---
## 2. 판정 규칙
```
target = PreSaved 인자 recipeName (Aurora가 알려주는 실제 저장 대상)
active = _RecipeManager.ActivatedRecipe()?.Name
plcAuto = _PLC.ReadBit(eCommonBitInput.PLC_AUTO_STATUS).data // M70501
if (_PLC == null || _PLC.IsSimul) → 게이트 미적용 (통과) // 오프라인/시뮬 티칭 보호
if (PLC 읽기 err != E1.OK) → 게이트 미적용 + Log.W // fail-open (아래 3-3 참고)
if (!plcAuto) → 통과 // 라인 정지 상태 = 자유 티칭
if (active == null) → 통과 + Log.W
if (target != active) → ★ 차단 (Toast 안내)
else → 통과
```
### 진리표
| PLC M70501 | PC 상태 | 저장 대상 | 활성 레시피 | 결과 |
|:---:|:---:|---|---|---|
| OFF | Manual | B | A | **저장 O** (라인 정지, 자유 티칭) |
| OFF | Manual | A | A | **저장 O** |
| ON | Auto | 아무거나 | – | **저장 X** (기존 `IsManualStatus` 게이트가 먼저 차단) |
| **ON** | **Manual (bit15 Resume)** | **B** | **A** | **저장 X ← 이번에 막는 케이스** |
| ON | Manual (bit15 Resume) | A | A | **저장 O** (활성 레시피 보정은 허용) |
| (PLC null / Simul) | – | – | – | 저장 O (게이트 미적용) |
**기존 `IsManualStatus` 게이트는 그대로 둡니다.** 신규 게이트는 그 아래에 추가되는 2차 방어선입니다.
---
## 3. 수정 내용
### 3-1. 대상 파일 (1개)
`WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs`
### 3-2. `PreSaved` 수정안
```csharp
// 파일 상단 using 에 추가 (E1.OK 사용 목적)
using Hermes;
```
```csharp
public bool PreSaved(int id, string recipeName)
{
try {
if (ApplicationExt.PermissionLevel > (uint)PermissionLevel.LEVEL3) {
MessageUtil.ShowErrorToast("Engineer 권한으로 로그인 하시기 바랍니다.", Log);
return false;
}
if (!IsManualStatus) {
MessageUtil.ShowErrorToast("Auto 상태에서 레시피 변경 불가", Log);
return false;
}
// ── 260803 추가: PLC AutoStatus(M70501) ON 이면 활성 레시피만 저장 허용 ─────────────
// 배경: bit15 Resume 경로(UCMainAutoRunViewModel.cs:229-238)는 라인이 Auto인 채로
// PC만 Manual로 내린다. 이때 위의 IsManualStatus 게이트가 통과되므로,
// PLC측 원본 신호로 한 번 더 판정한다.
if (!IsActiveRecipeOnlySaveAllowed(recipeName)) {
return false;
}
return true;
} catch (Exception ex) {
Log.E($"PreSaved failed: {ex}");
}
return false;
}
/// <summary>
/// PLC가 Auto 운전 중(M70501 ON)이면 활성 레시피 외 저장을 거부한다.
/// PLC 미연결·시뮬·읽기 실패 시에는 게이트를 적용하지 않는다(fail-open, 티칭 작업 보호).
/// </summary>
private bool IsActiveRecipeOnlySaveAllowed(string recipeName)
{
if (_PLC == null || _PLC.IsSimul) {
return true;
}
var auto = _PLC.ReadBit(PLCController.eCommonBitInput.PLC_AUTO_STATUS); // M70501
if (auto.err != E1.OK) {
Log.W($"[SaveGate] PLC AutoStatus 읽기 실패(err:{auto.err}) → 게이트 미적용");
return true;
}
if (!auto.data) {
return true; // 라인 정지 → 자유 티칭
}
var active = _RecipeManager.ActivatedRecipe();
if (active == null) {
Log.W("[SaveGate] PLC Auto ON 이지만 활성 레시피 없음 → 게이트 미적용");
return true;
}
if (!string.Equals(recipeName, active.Name, StringComparison.Ordinal)) {
Log.W($"[SaveGate] 차단 — PLC Auto 운전 중 비활성 레시피 저장 시도 " +
$"(대상:[{recipeName}] / 활성:[{active.Name}] / 선택:[{RecipeHelper.SelectedRecipe.name}])");
MessageUtil.ShowErrorToast(
$"자동 운전 중에는 활성 레시피만 저장할 수 있습니다.\n" +
$"활성 : {active.Name}\n저장 시도 : {recipeName}", Log);
return false;
}
Log.I($"[SaveGate] 통과 — PLC Auto 운전 중 활성 레시피 저장 (대상:[{recipeName}])");
return true;
}
```
### 3-3. fail-open / fail-closed 선택 근거
본 게이트는 **안전(safety) 인터락이 아니라 데이터 보호 게이트**입니다.
PLC 읽기 실패 시 fail-closed로 잡으면 **PLC 통신이 불안정할 때 티칭 저장이 전부 막혀** 현장 작업이 멈춥니다.
따라서 위 안은 **fail-open + 경고 로그**로 잡았습니다.
반대 정책(읽기 실패 시 차단)을 원하시면 `auto.err != E1.OK` 분기를 `return false` 로 바꾸면 됩니다. **정책 결정 필요 — 4장 확인 사항 ①**
---
## 4. 확인 필요 사항 (현장 확인 후 확정)
| # | 항목 | 왜 필요한가 | 확인 방법 |
|---|---|---|---|
| ① | PLC 읽기 실패 시 정책 (fail-open / fail-closed) | 3-3 참고 | 담당자 결정 |
| ② | **bit15 Resume 구간에서 M70501이 ON을 유지하는가** | 이 게이트의 대전제. PLC가 이 구간에 Auto를 내려버리면 게이트가 발동하지 않음 | 현장에서 bit15 팝업 뜬 상태로 PLC M70501 모니터 확인 |
| ③ | ② 가 OFF로 확인될 경우 대체 신호 | 게이트 조건 교체 필요 | `SeqAutoRunPTOP.ResumeMode == true` 또는 `PC_RECIPE_NO != 0` 을 OR 조건으로 추가 (5-2 참고) |
| ④ | AutoRun 중 "활성 레시피 저장"을 허용할지 | 표의 5행. 허용이 기본안이나, 운전 중 활성 레시피 변경 자체를 막으려면 조건을 `plcAuto → 전면 차단`으로 단순화 | 담당자 결정 |
> ②가 확정되기 전에는 **이 게이트가 현장에서 발동하지 않을 수 있습니다.** 적용 후 반드시 6장 테스트로 발동 여부를 눈으로 확인하세요.
---
## 5. 본 게이트가 **막지 못하는** 경로 (중요)
`PreSaved`는 **Aurora 저장 버튼 경로에만** 걸립니다. 아래 두 경로는 `RecipeHelper.SaveRecipe()`를 **직접** 호출하므로 이 게이트를 타지 않습니다. 2026-08-01 오염의 **3단계(자동 저장)** 가 여기에 해당합니다.
### 5-1. 레시피 활성화 시 자동 저장 — `UCScannerObjectTeachingViewModel.cs:155-156`
```csharp
var activeRecipe = _RecipeManager.ActivatedRecipe();
RecipeHelper.SaveRecipe(activeRecipe.Name); // ← 사용자가 저장을 누른 적이 없는데 파일이 바뀜
```
AutoRun이 `ActivateRecipe(A)` 하는 순간(`SeqAutoRunPTOP.cs:403`), **작업자가 B를 티칭 중이던 캔버스가 A 파일에 기록**됩니다.
로그도 남지 않아 조용히 오염됩니다.
**권장 보완 (별건 작업으로 분리 가능, 3~5줄):**
```csharp
// 캔버스에 로드된 레시피와 활성 레시피가 같을 때만 저장
var loaded = System.IO.Path.GetFileName(
System.IO.Path.GetDirectoryName(DB.Dao.SystemRecipe.WorkRecipeFile));
if (string.Equals(loaded, activeRecipe.Name, StringComparison.Ordinal)) {
RecipeHelper.SaveRecipe(activeRecipe.Name);
} else {
Log.W($"[SaveGate] 활성화 자동저장 생략 — 캔버스:[{loaded}] ≠ 활성:[{activeRecipe.Name}]");
}
```
### 5-2. 신규 레시피 생성 시 자동 저장 — `UCScannerObjectTeachingViewModel.cs:111-115`
`includeModel:false`라 도형은 안 쓰이지만 **용접 파라미터는 그 순간 캔버스에 로드돼 있던 레시피의 것**이 기록됩니다.
신규/복사 레시피가 엉뚱한 파라미터로 출발합니다. (분석문서 §5-1)
### 5-3. `OnSaved`가 인자를 버리는 문제 — `:202-208`
```csharp
public void OnSaved(int id, string recipeName) // Aurora가 대상을 알려줌
...
RecipeHelper.SaveRecipe(RecipeHelper.SelectedRecipe.name); // ← 그런데 static 값으로 씀
```
`PreSaved`에서 `recipeName`으로 검증했는데 `OnSaved`에서 `SelectedRecipe.name`으로 쓰면 **검증한 대상과 실제 쓰는 대상이 다를 수 있습니다.**
본 게이트의 실효성을 위해 **`OnSaved`의 저장 대상도 `recipeName`으로 통일할 것을 강력히 권장**합니다 (해당 함수 내 `RecipeHelper.SelectedRecipe.name` → `recipeName` 치환, 1줄 + 이후 참조 정리).
> **정리**: 본 작업(3장)만 적용하면 **"작업자가 저장 버튼을 눌러 남의 레시피를 덮는" 경로는 차단**됩니다.
> **"시퀀스가 조용히 덮는" 경로(5-1)는 별도 작업이 필요**합니다. 두 건은 원인이 같으므로 함께 적용하는 것을 권장합니다.
---
## 6. 테스트 시나리오
| # | 절차 | 기대 결과 |
|---|---|---|
| T1 | PLC 정지(M70501 OFF) + Manual → 임의 레시피 B 편집 후 저장 | **정상 저장** (기존과 동일, 회귀 없음) |
| T2 | Auto 운전(M70501 ON, PC도 Auto) → 저장 시도 | 기존 토스트 "Auto 상태에서 레시피 변경 불가" |
| T3 | **Auto 운전 중 bit15 ON → 팝업 [확인] → Manual 전환** → 활성 레시피 **A** 저장 | **정상 저장** + 로그 `[SaveGate] 통과` |
| T4 | **T3 상태에서 콤보로 B 선택 → 편집 → 저장** | **차단 토스트** + 로그 `[SaveGate] 차단 — ... 대상:[B] / 활성:[A]` |
| T5 | PLC 케이블 분리(통신 실패) 상태에서 저장 | fail-open 정책이면 저장됨 + `Log.W ... 읽기 실패` |
| T6 | 시뮬 모드에서 저장 | 게이트 미적용, 정상 저장 |
**T4가 이번 작업의 핵심 검증 항목입니다.** T4에서 차단이 안 뜨면 4장 ② (M70501 상태)를 먼저 확인하세요.
---
## 7. 영향도 / 롤백
| 항목 | 내용 |
|---|---|
| 수정 파일 | `WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs` **1개** |
| 신규 참조 | 없음 (`_PLC`, `_RecipeManager` 모두 기존 필드) |
| PLC 맵 변경 | **없음** — 기존 M70501(`PLC_AUTO_STATUS`) 읽기만 추가. PLC측 작업 불필요 |
| 시퀀스 영향 | 없음 (읽기 전용, 저장 경로에서만 동작) |
| 정상 운전 영향 | 없음 (M70501 OFF 시 기존 동작과 100% 동일) |
| 롤백 | `IsActiveRecipeOnlySaveAllowed` 호출 1줄 주석 처리 |
---
## 8. 작업 순서 체크리스트
- [ ] 4장 ①·②·④ 정책 확인 (특히 ② — bit15 구간 M70501 상태)
- [ ] `UCScannerObjectTeachingViewModel.cs` 3-2 적용 (`using Hermes;` 포함)
- [ ] 빌드 확인
- [ ] T1·T3·T4 현장 검증 (T4 로그 `[SaveGate] 차단` 확인)
- [ ] (권장, 별건) 5-3 `OnSaved` 저장 대상 → `recipeName` 통일
- [ ] (권장, 별건) 5-1 활성화 자동저장 캔버스 일치 가드
- [ ] SW Version 시트 반영 · 버전업