You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[P1][A04] Local Profile 부분 갱신으로 알림 설정·점수·백업 revision 보존
문제와 사용자 영향
기본 여유시간을 변경하거나 일정을 완료하면 사용자가 끈 알림이 다시 켜지고, 상세 알림 표시 설정과 백업 이력이 초기값으로 덮인다. UserEntity가 알림 설정과 백업 메타데이터를 포함하지 않는데도 이를 완전한 DB row로 변환하여 전체 upsert하기 때문이다. 또한 프로필을 읽은 뒤 여유시간을 저장하는 사이 일정 완료가 발생하면 이전 UserEntity에 담긴 오래된 점수로 새 점수를 되돌릴 수 있다.
2026-09-23 종합 감사 A04 / P1에 대응한다. 이 이슈는 소스에서 확인한 데이터 보존 결함과 이를 막을 회귀 검증의 명세다. 구현·테스트 완료를 주장하는 문서가 아니다.
조사 기준과 근거
감사 기준 SHA: 44067d7ab26b6290c16cdb13b99f57387dec08f6. 문답 당시 HEAD: 6e17e76436ead5bd03a19e2c3fef2406cccdb504. 후자의 A01 변경은 아래 A04 구현 경로를 바꾸지 않았다.
row 변환 시 alarmsEnabled=true, alarmOffsetMinutes=0, detailedNotificationContent=false, dataRevision=0, lastDurableDataAt=null을 지정한다. nullable export/first-durable metadata도 전달하지 않는다. row→entity는 이 값을 보존할 자리가 없다.
생성/조회 및 중복 완료 점수만 검사한다. non-default 알림 설정·export metadata·revision·stale profile 경쟁·실패 rollback 검증이 없다.
제품 계약과 기존 문서
Local Profile은 현재 설치의 단일 프로필이며 원격 account가 아니다.
Durable OnTime Data에는 사용자 선호 설정과 결과가 포함된다. unrelated profile 수정은 이를 지우는 행위가 아니다.
Schedule Notification Setting과 Detailed Notification Content는 명시적인 사용자 선택이다. 여유시간 변경이나 점수 집계가 이를 바꾸지 않는다.
Local Punctuality Score는 유효한 Schedule Outcome이 최대 한 번 기여하는 집계다.
Backup Freshness는 현재 durable revision과 최근 성공한 export cutoff의 비교다. 일반 프로필 수정은 export 이력을 제거하지 않는다.
Backup Restore는 검증된 백업으로 전체 데이터를 의도적으로 교체하는 별도 동작이다. 본 이슈의 일반 수정 규칙을 복원 교체와 혼동하지 않는다.
기존 CONTEXT의 용어와 ADR 0013(복원 전체 교체), 0022(단일 DB), 0025(완료/점수 원자성·중복 방지), 0030(명시적 알림 표시 선호), 0035(단조 revision과 export freshness)를 적용한다. 새 용어 정의나 새로운 장기 아키텍처 선택이 없으므로 CONTEXT/ADR를 추가하지 않는다.
합의한 구현 범위
새 빈 Local Profile 생성은 insert-if-absent로 구현한다. 이미 존재하는 row를 기본값이나 전달된 오래된 entity로 갱신하지 않는다. 초기화 호출 경쟁도 row를 덮지 않는다.
프로필 수정은 목적별 API로 좁힌다. 여유시간 변경은 spareTime만, 온보딩 완료는 해당 profile 필드와 기본 Preparation만 수정한다. 여유시간 API가 점수·온보딩·알림·백업 필드를 인자로 받아 재저장하는 경로를 제거한다.
production의 일반 saveUser(UserEntity)/full putUser 경로는 제거하거나 creation-only로 제한해 호출 의도를 분명히 한다. domain repository 계약 및 실제 호출부를 함께 수정한다. UserEntity에 DB의 모든 숨은 필드를 무조건 확장하는 방식으로 문제를 숨기지 않는다.
일정 완료는 기존 DB transaction 안에서 outcome, scoreContributionRecorded, 점수 카운터의 원자적 증분, revision을 함께 commit한다. 읽어온 UserEntity 전체를 재저장하지 않는다.
알림 enabled·상세 표시·점수 reset은 소유한 column만 갱신하고 실제 변경과 revision 증가를 같은 DB transaction에 묶는다. 반환 모델은 committed DB 값과 일치해야 하며 unrelated alarm offset도 보존한다.
온보딩의 기본 Preparation 저장과 profile 변경은 하나의 transaction에 포함한다. 준비 단계만 남거나 온보딩 완료만 반영된 중간 상태를 노출하지 않는다. 이 사용자 변경 전체의 revision은 한 번만 증가한다.
실제 값이 달라지는 이 이슈 범위의 한 원자적 사용자 변경마다 revision을 정확히 한 번 증가시킨다. 공통 helper가 중첩 호출되어 두 번 증가하지 않도록 검사한다. 실제 write 실패 또는 revision write 실패 시 양쪽 모두 rollback한다.
동일한 여유시간/알림 설정/상세 표시/온보딩 값을 다시 적용하거나 기존 row에 insert-if-absent를 호출하면 row·revision·durable timestamps·export metadata를 변경하지 않는다. 온보딩 준비 자체가 달라지면 실제 durable 변경이다.
새 빈 프로필은 revision 0, firstDurableDataAt null을 유지한다. 최초 실제 사용자 설정·준비 데이터 변경 시 첫 durable 시점을 설정하며 이후 관련 없는 수정으로 이를 지우거나 재시작하지 않는다.
점수 카운터가 이미 0이면 score reset은 no-op이다. 0이 아닌 값을 의도적으로 reset하면 관련 카운터만 0으로 바꾸고 revision을 한 번 증가시킨다. 완료된 Schedule의 기존 scoreContributionRecorded 계약은 유지하여 같은 일정을 다시 집계하지 않는다.
기존 exported revision/cutoff는 성공한 새 export 또는 별도 reset/restore의 명시적 계약 외에는 변경하지 않는다. dataRevision은 현재 row 기준 증분하고 0/1로 되돌리지 않는다.
DB commit 전에 repository stream에 성공 상태를 발행하지 않는다. 실패 뒤에는 기존 값으로 재조회되고 재시도가 가능해야 한다.
구체 재현 및 기대 결과
합성 DB fixture를 직접 seed한다. 예: 알림 OFF, 상세 ON, offset=7, spareTime=5, eligible=10/onTime=8, revision=42, exportedRevision=42, 알려진 export cutoff·first/last durable timestamp, 온보딩 완료 상태다.
spareTime=15, revision=43; 알림/상세/offset/점수/온보딩/export/first timestamp 보존
미완료 Schedule 정상 완료
점수 변경과 함께 hidden columns 기본값 덮기
outcome 정상 완료, eligible=11/onTime=9, revision +1; 관련 없는 모든 필드 보존
같은 Schedule 다시 완료
기존 guard가 있음
outcome/점수/revision/timestamps 추가 변경 없음
프로필 읽기 → 일정 완료 → 여유시간 저장
읽어둔 10/8 점수로 11/9가 복구될 수 있음
여유시간만 변경하며 점수 11/9 유지, 두 실제 변경이면 revision 총 +2
여유시간 15→15분 또는 OFF→OFF
현재 별도 helper가 revision 증가
전체 row 불변, freshness 불변
두 서로 다른 Schedule 동시 완료
전체 entity 증분 방식의 소유 경계가 불명확
두 결과 모두 1회 기여, lost update 없음, revision 총 +2
같은 Schedule 동시 완료
중복 집계 위험 방지 필요
하나의 결과만 기여, revision +1
알림 OFF/상세 ON 변경과 점수 완료 교차
unrelated overwrite 가능
각 변경이 소유한 필드와 증가량 모두 보존
기존 row가 있을 때 getUser/초기화 경쟁
생성용 upsert가 초기값 덮을 수 있음
기존 row 전체 불변, 반환값은 실제 stored profile
온보딩 준비 단계 쓰기 뒤 profile/revision 실패
준비만 남을 수 있음
준비와 profile 모두 변경 전 상태, 완료 stream 없음
일반 프로필 write 뒤 revision 실패
내용만 바뀌고 freshness가 fresh일 수 있음
내용·revision 함께 rollback
백업 revision 42 snapshot 후 사용자 변경
exporter와 일반 수정의 metadata 경합
dataRevision=43, lastExportedRevision=42이면 Unexported Changes 유지
score reset과 후속 완료
reset과 일반 갱신 혼동 위험
reset 후 새 집계만 추가; 과거 완료 재호출은 추가 기여 안 함
초기 fixture를 설정하는 테스트 helper의 raw DB write는 production의 일반 저장 API로 노출하지 않는다. 실제 수정 전 자동 테스트가 위 결함을 탐지하는지 확인하고 수정 후 통과시키며, mock이 반환한 UserEntity만 비교하지 말고 DB row 전체를 read-back한다.
검증 계획
test/data/daos/user_dao_test.dart: 초기 insert, 기존 row 보존, partial update의 필드 소유권, 동일값 no-op, timestamps/export metadata, 실제 write+revision 원자성, 점수 reset.
프로필/preparation repository 회귀 테스트: 여유시간 변경의 non-default 설정 보존, onboarding transaction, stale entity를 받지 않는 API, commit 후 stream, 실패·재시도.
test/data/repositories/local_schedule_score_test.dart: 정상/지각 완료, 중복 완료, 두 완료, 완료와 설정 변경 교차, rollback, aggregate 보존을 실제 Drift DB로 검증.
test/data/mappers/domain_persistence_mappers_test.dart: mapper를 완전한 persisted row round-trip으로 오해하지 않도록 생성/부분 수정 계약을 검사. 생성 기본값 테스트와 기존 row 보존 테스트를 분리.
test/core/backup/backup_service_test.dart: 수정 직후 Unexported Changes, export metadata 보존, snapshot 이후 수정 revision이 export 완료로 지워지지 않음. A02 export lifecycle 구현과 같은 fixture를 공유할 수 있으나 서로 다른 실패 원인을 구분.
가능한 한 제어된 completer/DB transaction 순서와 실패 주입으로 경쟁을 재현하고 임의 sleep에 의존하지 않는다. 오류를 swallowing하거나 억지로 테스트를 통과시키지 않는다.
source 변경에 맞춰 codegen을 재실행하되 generated outputs는 commit하지 않는다. targeted tests 후 flutter analyze와 full flutter test를 실행한다. 환경 부족으로 미실행이면 정확히 구분하고 CI 실행 URL/SHA를 기록한다.
UI 연결 확인: 합성 local profile에서 알림 OFF·상세 ON 설정 → 여유시간 수정 → 일정 완료 → 설정 화면 재진입/앱 재시작 후 보존. 실제 OS 알림 도착 정확성은 A01/A05/A13/A14의 검증과 별개다.
의존 관계와 제외 범위
A02 [P0][A02] Android·iOS 암호화 백업 내보내기와 저장 결과·수명주기 보장 #584: export snapshot revision과 성공 시 markExported. A04는 일반 저장이 revision/metadata를 지우지 않게 하며 A02의 export/reset/restore operation gate와 충돌하지 않게 수정한다. 순차 계획상 A02 구현 후 A04를 적용하되 A02의 실제 기기 검증 대기를 이유로 A04를 멈추지 않는다.
A05/A15: 알림 상세 표시 변경의 OS 재등록 및 reconcile 경합. A04의 persisted 설정 보존이 선행 조건이고 OS payload/reconcile 정책은 해당 이슈다.
A08: 일정 생성+준비의 transaction. 본 이슈에서 온보딩 profile+기본 준비 원자성은 포함하되 일정 생성 orchestration 전체는 A08에 남긴다.
A09/C01: restore/reset lifecycle과 operation gate. 의도적 전체 교체와 일반 partial update를 구분하고 기존 restore DTO를 삭제하지 않는다.
D06: 30일 백업 안내 계산 개선. A04는 first/last durable와 export metadata 보존만 보장하며 reminder 기준일 정책을 바꾸지 않는다.
C05: pass-through usecase 전반 정리는 제외한다. 안전한 목적별 API를 위해 필요한 호출부 변경만 포함한다.
기존 사용자 설치에서 이미 덮인 알림 설정·백업 이력·점수를 추측해 되돌리는 자동 데이터 복구는 하지 않는다. 현재 DB에 남은 값을 보존하고 미래 손상을 막는다. 수동 검증된 백업 복원은 별도 계약이다.
새로운 계정/네트워크/동기화, 통계 규칙 변경, backup format 변경, schema migration, 전체 repository 재작성은 제외한다.
완료 기준
production 일반 수정 경로에 full-profile upsert가 남아 hidden columns를 덮을 수 없다.
초기생성은 기존 row를 변경하지 않고 경쟁 상황에서도 저장값을 반환한다.
여유시간·온보딩·설정 변경이 관련 없는 점수/알림/backup metadata를 보존한다.
완료 outcome·점수 증분·중복 기여 방지·revision이 같은 transaction에서 처리된다.
실제 사용자 원자 변경당 revision 1회, no-op/중복 finish는 0회다.
새 빈 profile의 revision 0/first timestamp null과 최초 실제 변경 시점 계약을 보존한다.
온보딩 profile·기본 준비·revision의 부분 성공이 없다.
실패 주입에서 write와 revision이 모두 rollback하고 성공 stream이 발행되지 않는다.
stale read·동시 완료·완료와 설정 교차의 실제 DB 테스트를 통과한다.
백업 이력이 있는 non-default fixture의 전체 row read-back 및 freshness 회귀 테스트를 통과한다.
targeted/full tests·analyze의 실행 SHA와 결과, PR/CI 링크를 기록했다. 실행하지 않은 검증을 통과로 표시하지 않았다.
생성 Dart 출력을 commit하지 않았고 실제 완료된 검증 범위에 맞춰 이슈 상태를 갱신했다.
위험과 검토 포인트
일반 saveUser 계약을 축소하면 mock/fake 및 onboarding 호출부가 함께 영향을 받는다. 컴파일을 맞추기 위해 무분별한 all-fields update 우회 API를 다시 만들지 않는다. DAO helper와 상위 transaction 양쪽에서 revision을 증가시키는 중복도 주의한다. Drift watcher가 트랜잭션 경계를 존중하는지, raw SQL helper가 table invalidation을 빠뜨리지 않는지도 실제 구독 테스트로 확인한다. nullable metadata에 absent와 null의 의미가 다르므로 creation-only companion과 update companion을 구분한다. 전체 DB 교체/삭제와 export freshness는 별도의 사용자 의도이므로 일반 no-op 규칙을 무비판적으로 적용하지 않는다.
grill-with-docs 실제 문답 전체
전담 서브에이전트 /root/grill_a04가 /Users/ejunpark/.agents/skills/grill-with-docs/SKILL.md, CONTEXT-FORMAT.md, ADR-FORMAT.md, AGENTS.md, CONTEXT 및 관련 ADR·소스·테스트를 읽었다. 사용자 지시에 따라 root가 의사결정을 대리했고, 질문을 한 번에 하나씩 보내 실제 응답을 기다렸다. 아래 문답은 질문과 답변을 그대로 보존한다.
Q1 — 일반 upsert 수정 범위
서브에이전트 질문
A04에서 grill-with-docs와 관련 glossary/ADR 0030·0035 및 실제 호출부를 읽었습니다. Q1: 단순히 putUser의 hidden column을 보존하는 패치로 끝낼까요, 아니면 saveUser(UserEntity)의 stale score/profile 덮어쓰기까지 막기 위해 목적별 쓰기로 좁힐까요? 코드상 updateSpareTime/createDefaultPreparation이 이전 UserEntity의 점수까지 재저장하므로, hidden column만 제외해도 일정 완료와 경쟁하면 점수를 되돌릴 수 있습니다. 추천: 새 Local Profile 생성은 insert-if-absent(기존 row 절대 갱신 안 함), 여유시간·온보딩은 필요한 프로필 필드만 UPDATE, 일정 완료는 기존 transaction 안에서 점수 column 증분 UPDATE, 알림 설정·score reset은 해당 필드만 갱신하고 각 durable write와 revision 증가를 같은 transaction에 묶습니다. 일반 saveUser/putUser의 production full-upsert 경로는 제거하거나 creation-only로 명확히 제한하고 백업 복원의 전체 교체 경로는 별도 유지합니다. 이 범위를 A04로 확정할까요?
root 답변
추천안 동의합니다. A04는 hidden prefs/revision 보존만이 아니라 각 호출 목적에 필요한 필드만 갱신해 stale UserEntity가 점수·온보딩·설정을 되돌리지 못하게 합니다. 초기생성 insert-if-absent, 완료 점수 transaction 내 원자적 증분, 프로필 partial update, durable 변경과 revision의 동일 transaction을 검증하세요. 기존 row 값을 유지하는 no-op 생성과 의도적인 score reset 구별하고 실패 rollback/경쟁 시나리오를 포함하세요. restore 전체교체는 별도 의도적 경로로 유지합니다. 공개 domain API 축소가 필요한 호출부까지 범위에 포함하되 C05 전체usecase정리와 혼합하지 않습니다.
Q2 — no-op, 초기 revision, 온보딩 원자성
서브에이전트 질문
A04 Q2: revision의 no-op·초기생성 의미를 확정하고 싶습니다. 추천: 기존 Local Profile에 같은 여유시간/알림 설정/상세 표시/온보딩 값을 다시 적용하거나 insert-if-absent를 다시 호출한 경우 row·revision·durable timestamps·export metadata가 모두 그대로여야 합니다. 실제 값이 달라지는 이 이슈 범위의 한 원자적 사용자 변경은 revision을 정확히 한 번 증가시키며, 정상 일정 완료는 outcome·점수·revision을 함께 commit하고 중복 finish는 증가시키지 않습니다. 새 빈 프로필 생성은 기존 semantics대로 revision 0/firstDurableDataAt null을 유지하고 최초 사용자 설정·준비 데이터 변경 시에만 첫 durable 시점을 설정합니다. score reset은 카운터가 0일 때 no-op, 0이 아니면 한 번 증가(미래 완료의 새 집계는 기존 scoreContributionRecorded 계약 유지)로 둘까요? 온보딩의 기본 준비 저장+프로필 변경은 하나의 transaction으로 묶어 중간 실패 시 orphan 준비/완료 플래그가 남지 않게 하고 revision 1회만 기록하는 것을 권장합니다.
root 답변
동의합니다. 값이 동일한 no-op은 row/revision/timestamps/export metadata 그대로, 실제 사용자 원자 변경당 1회 revision 증가. 빈 초기 프로필 revision0/firstnull 유지. 완료 중복방지 기존 scoreContributionRecorded 계약 유지, score0 reset no-op. 온보딩 기본 준비+profile 한 transaction/한revision으로 묶어 부분쓰기 금지. 기존 revision 증가 helper가 중첩호출되는지 확인하고 regression에 assert하세요.
문답 후 코드 기반 보충 통지
A04 두 문답으로 결정이 충분히 닫혔습니다. 추가 확인으로 AlarmRepositoryImpl.updateAlarmSettings의 반환값도 DB offset을 읽지 않고 0을 조립하므로 보존 계약의 read-back 검증에 포함하겠습니다. 소스 재현 범위와 기존 테스트가 hidden columns를 assert하지 않는 공백, 실제 값 변경/동일값/동시 완료/rollback matrix를 이슈에 정리 중입니다. 새 glossary/ADR는 필요 없습니다.
[P1][A04] Local Profile 부분 갱신으로 알림 설정·점수·백업 revision 보존
문제와 사용자 영향
기본 여유시간을 변경하거나 일정을 완료하면 사용자가 끈 알림이 다시 켜지고, 상세 알림 표시 설정과 백업 이력이 초기값으로 덮인다.
UserEntity가 알림 설정과 백업 메타데이터를 포함하지 않는데도 이를 완전한 DB row로 변환하여 전체 upsert하기 때문이다. 또한 프로필을 읽은 뒤 여유시간을 저장하는 사이 일정 완료가 발생하면 이전UserEntity에 담긴 오래된 점수로 새 점수를 되돌릴 수 있다.2026-09-23 종합 감사 A04 / P1에 대응한다. 이 이슈는 소스에서 확인한 데이터 보존 결함과 이를 막을 회귀 검증의 명세다. 구현·테스트 완료를 주장하는 문서가 아니다.
조사 기준과 근거
감사 기준 SHA:
44067d7ab26b6290c16cdb13b99f57387dec08f6. 문답 당시 HEAD:6e17e76436ead5bd03a19e2c3fef2406cccdb504. 후자의 A01 변경은 아래 A04 구현 경로를 바꾸지 않았다.toUserRow().toCompanion(false)전체 row를 insertOnConflictUpdate한다. nullable 필드도 absent patch가 아닌 명시 값으로 취급되는 경로다. createUser도 같은 upsert를 호출한다.제품 계약과 기존 문서
기존 CONTEXT의 용어와 ADR 0013(복원 전체 교체), 0022(단일 DB), 0025(완료/점수 원자성·중복 방지), 0030(명시적 알림 표시 선호), 0035(단조 revision과 export freshness)를 적용한다. 새 용어 정의나 새로운 장기 아키텍처 선택이 없으므로 CONTEXT/ADR를 추가하지 않는다.
합의한 구현 범위
saveUser(UserEntity)/fullputUser경로는 제거하거나 creation-only로 제한해 호출 의도를 분명히 한다. domain repository 계약 및 실제 호출부를 함께 수정한다. UserEntity에 DB의 모든 숨은 필드를 무조건 확장하는 방식으로 문제를 숨기지 않는다.구체 재현 및 기대 결과
합성 DB fixture를 직접 seed한다. 예: 알림 OFF, 상세 ON, offset=7, spareTime=5, eligible=10/onTime=8, revision=42, exportedRevision=42, 알려진 export cutoff·first/last durable timestamp, 온보딩 완료 상태다.
초기 fixture를 설정하는 테스트 helper의 raw DB write는 production의 일반 저장 API로 노출하지 않는다. 실제 수정 전 자동 테스트가 위 결함을 탐지하는지 확인하고 수정 후 통과시키며, mock이 반환한 UserEntity만 비교하지 말고 DB row 전체를 read-back한다.
검증 계획
test/data/daos/user_dao_test.dart: 초기 insert, 기존 row 보존, partial update의 필드 소유권, 동일값 no-op, timestamps/export metadata, 실제 write+revision 원자성, 점수 reset.test/data/repositories/local_schedule_score_test.dart: 정상/지각 완료, 중복 완료, 두 완료, 완료와 설정 변경 교차, rollback, aggregate 보존을 실제 Drift DB로 검증.test/data/mappers/domain_persistence_mappers_test.dart: mapper를 완전한 persisted row round-trip으로 오해하지 않도록 생성/부분 수정 계약을 검사. 생성 기본값 테스트와 기존 row 보존 테스트를 분리.test/core/backup/backup_service_test.dart: 수정 직후 Unexported Changes, export metadata 보존, snapshot 이후 수정 revision이 export 완료로 지워지지 않음. A02 export lifecycle 구현과 같은 fixture를 공유할 수 있으나 서로 다른 실패 원인을 구분.flutter analyze와 fullflutter test를 실행한다. 환경 부족으로 미실행이면 정확히 구분하고 CI 실행 URL/SHA를 기록한다.의존 관계와 제외 범위
완료 기준
위험과 검토 포인트
일반 saveUser 계약을 축소하면 mock/fake 및 onboarding 호출부가 함께 영향을 받는다. 컴파일을 맞추기 위해 무분별한 all-fields update 우회 API를 다시 만들지 않는다. DAO helper와 상위 transaction 양쪽에서 revision을 증가시키는 중복도 주의한다. Drift watcher가 트랜잭션 경계를 존중하는지, raw SQL helper가 table invalidation을 빠뜨리지 않는지도 실제 구독 테스트로 확인한다. nullable metadata에 absent와 null의 의미가 다르므로 creation-only companion과 update companion을 구분한다. 전체 DB 교체/삭제와 export freshness는 별도의 사용자 의도이므로 일반 no-op 규칙을 무비판적으로 적용하지 않는다.
grill-with-docs 실제 문답 전체
전담 서브에이전트
/root/grill_a04가/Users/ejunpark/.agents/skills/grill-with-docs/SKILL.md,CONTEXT-FORMAT.md,ADR-FORMAT.md, AGENTS.md, CONTEXT 및 관련 ADR·소스·테스트를 읽었다. 사용자 지시에 따라 root가 의사결정을 대리했고, 질문을 한 번에 하나씩 보내 실제 응답을 기다렸다. 아래 문답은 질문과 답변을 그대로 보존한다.Q1 — 일반 upsert 수정 범위
서브에이전트 질문
root 답변
Q2 — no-op, 초기 revision, 온보딩 원자성
서브에이전트 질문
root 답변
문답 후 코드 기반 보충 통지