From be5de9605adb0a07aa9ef4f15cd8a9180ac72b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=ED=9B=88?= Date: Mon, 7 Sep 2026 15:31:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EB=B0=B0=EA=B2=BD=EC=83=89=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=B4=88?= =?UTF-8?q?=EC=95=88=20=EC=9C=A0=EC=A7=80=EC=99=80=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 저장된 화면 상태와 편집 초안을 분리하고 팝업 안에서 오류와 재저장 동작을 제공한다. 초안 유지·재시도·저장 응답 순서에 대한 회귀 테스트를 보완한다. 기존 오버레이 표시·리사이즈 기준점 정책과 fixed-position API 값을 한글·영문 문서에 반영한다. --- .../content/en/api-reference/overlay/page.mdx | 5 +- .../en/api-reference/settings/page.mdx | 3 +- docs/content/en/guide/settings/page.mdx | 9 +++ .../content/ko/api-reference/overlay/page.mdx | 3 +- .../ko/api-reference/settings/page.mdx | 3 +- docs/content/ko/guide/settings/page.mdx | 9 +++ src/renderer/hooks/Modal/usePalette.test.tsx | 33 ++++++++- src/renderer/hooks/Modal/usePalette.ts | 70 +++++++++++-------- src/renderer/windows/main/App.tsx | 24 ++++++- 9 files changed, 120 insertions(+), 39 deletions(-) diff --git a/docs/content/en/api-reference/overlay/page.mdx b/docs/content/en/api-reference/overlay/page.mdx index 839cef289..2c98dcb92 100644 --- a/docs/content/en/api-reference/overlay/page.mdx +++ b/docs/content/en/api-reference/overlay/page.mdx @@ -45,13 +45,14 @@ await dmn.overlay.setLock(false); Sets the fixed point used when resizing the overlay. - Supported values: `top-left`, `top-right`, `bottom-left`, `bottom-right`, and - `center`. + Supported values: `top-left`, `top-right`, `bottom-left`, `bottom-right`, + `center`, and `fixed-position`. ```javascript await dmn.overlay.setAnchor('top-left'); await dmn.overlay.setAnchor('center'); +await dmn.overlay.setAnchor('fixed-position'); // Preserve existing elements' screen positions await dmn.overlay.setAnchor('bottom-right'); ``` diff --git a/docs/content/en/api-reference/settings/page.mdx b/docs/content/en/api-reference/settings/page.mdx index d25e6b124..b252bbae0 100644 --- a/docs/content/en/api-reference/settings/page.mdx +++ b/docs/content/en/api-reference/settings/page.mdx @@ -34,7 +34,8 @@ interface SettingsState { | 'top-right' | 'bottom-left' | 'bottom-right' - | 'center'; + | 'center' + | 'fixed-position'; keyCounterEnabled: boolean; gridSettings: { alignmentGuides: boolean; diff --git a/docs/content/en/guide/settings/page.mdx b/docs/content/en/guide/settings/page.mdx index 73a50c547..152254a99 100644 --- a/docs/content/en/guide/settings/page.mdx +++ b/docs/content/en/guide/settings/page.mdx @@ -20,6 +20,8 @@ Select the program interface language: Set the background color of the overlay window. Default is transparent. +If saving fails, the background returns to the saved color while the popup retains your input. After resolving the cause, click **Save again** to retry the same draft. The draft survives closing and reopening the popup, but is lost when you exit the app. + Using window capture in OBS maintains transparent background. No chroma key needed. @@ -27,6 +29,10 @@ Set the background color of the overlay window. Default is transparent. ## Window Settings +### Overlay Visibility + +The overlay starts closed on first launch. The app saves whether you show or hide it and restores that visibility on the next launch. + ### Always on Top Keeps the overlay window always above other windows. Useful when placing the key viewer over game screens. @@ -70,6 +76,9 @@ Select the reference point when resizing the overlay window: - **bottom-left**: Fixed bottom-left, expand top-right - **bottom-right**: Fixed bottom-right, expand top-left - **center**: Fixed center, expand all directions +- **fixed-position**: Preserve existing elements' screen positions when the window size or content origin changes + +The default, top-left, fixes the window corner. When a sprite extends beyond the left or top edge and changes the content origin, existing elements can move on screen. Choose fixed-position to preserve their screen positions. ## Custom CSS/JS diff --git a/docs/content/ko/api-reference/overlay/page.mdx b/docs/content/ko/api-reference/overlay/page.mdx index 6552be691..550037bf8 100644 --- a/docs/content/ko/api-reference/overlay/page.mdx +++ b/docs/content/ko/api-reference/overlay/page.mdx @@ -53,12 +53,13 @@ await dmn.overlay.setLock(false); // 잠금 해제 리사이즈 기준점을 설정합니다. - 지원 값: `top-left`, `top-right`, `bottom-left`, `bottom-right`, `center` + 지원 값: `top-left`, `top-right`, `bottom-left`, `bottom-right`, `center`, `fixed-position` ```javascript await dmn.overlay.setAnchor("top-left"); // 좌상단 기준 await dmn.overlay.setAnchor("center"); // 중앙 기준 +await dmn.overlay.setAnchor("fixed-position"); // 기존 요소의 화면 위치 유지 await dmn.overlay.setAnchor("bottom-right"); // 우하단 기준 ``` diff --git a/docs/content/ko/api-reference/settings/page.mdx b/docs/content/ko/api-reference/settings/page.mdx index dd16b97c1..99cc7134a 100644 --- a/docs/content/ko/api-reference/settings/page.mdx +++ b/docs/content/ko/api-reference/settings/page.mdx @@ -34,7 +34,8 @@ interface SettingsState { | "top-right" | "bottom-left" | "bottom-right" - | "center"; + | "center" + | "fixed-position"; keyCounterEnabled: boolean; gridSettings: { alignmentGuides: boolean; diff --git a/docs/content/ko/guide/settings/page.mdx b/docs/content/ko/guide/settings/page.mdx index 6bdc534fc..dcb80ac4f 100644 --- a/docs/content/ko/guide/settings/page.mdx +++ b/docs/content/ko/guide/settings/page.mdx @@ -20,12 +20,18 @@ DM Note의 다양한 설정 옵션을 안내합니다. 오버레이 창의 배경색을 설정합니다. 기본값은 투명입니다. +저장에 실패하면 화면 배경은 저장된 값으로 복원되지만 팝업의 입력값은 유지됩니다. 오류 원인을 해소한 뒤 **다시 저장**을 누르면 같은 초안을 재시도할 수 있습니다. 초안은 팝업을 닫았다 다시 열어도 유지되지만 앱을 종료하면 사라집니다. + OBS에서 윈도우 캡쳐를 사용하면 투명 배경이 유지됩니다. 크로마키가 필요 없습니다. ## 창 설정 +### 오버레이 표시 상태 + +처음 실행할 때 오버레이는 닫힌 상태입니다. 오버레이를 켜거나 끈 상태를 저장하며, 다음 실행 때 마지막 표시 상태를 복원합니다. + ### 항상 위에 표시 오버레이 창이 다른 창 위에 항상 표시되도록 합니다. 게임 화면 위에 키뷰어를 배치할 때 유용합니다. @@ -69,6 +75,9 @@ DM Note의 다양한 설정 옵션을 안내합니다. - **좌하단(bottom-left)**: 좌하단 고정, 우상단으로 확장 - **우하단(bottom-right)**: 우하단 고정, 좌상단으로 확장 - **중앙(center)**: 중앙 고정, 모든 방향으로 확장 +- **고정(fixed-position)**: 창 크기와 콘텐츠 원점이 바뀌어도 기존 요소의 화면 위치 유지 + +기본값인 좌상단은 창의 모서리를 고정합니다. 스프라이트가 왼쪽이나 위쪽으로 넘쳐 콘텐츠 원점이 바뀌면 기존 요소의 화면 위치도 이동할 수 있습니다. 기존 요소의 화면 위치를 유지하려면 고정을 선택합니다. ## 커스텀 CSS/JS diff --git a/src/renderer/hooks/Modal/usePalette.test.tsx b/src/renderer/hooks/Modal/usePalette.test.tsx index 7b5450f22..e9f2d8edc 100644 --- a/src/renderer/hooks/Modal/usePalette.test.tsx +++ b/src/renderer/hooks/Modal/usePalette.test.tsx @@ -50,13 +50,38 @@ describe('배경색 저장 실패 복원', () => { vi.restoreAllMocks(); }); - it('거절된 낙관 배경색을 저장된 값으로 되돌리고 안내한다', async () => { + it('화면만 저장값으로 복원하고 팝업을 다시 열어도 초안과 오류를 유지한다', async () => { mocks.update.mockRejectedValue(new Error('disk full')); await act(async () => palette.handleColorChange('#222222')); expect(useSettingsStore.getState().backgroundColor).toBe('#111111'); - expect(palette.color).toBe('#111111'); - expect(mocks.alert).toHaveBeenCalledTimes(1); + expect(palette.color).toBe('#222222'); + expect(palette.saveError).toBe('common.saveFailed'); + expect(palette.isSaving).toBe(false); + expect(mocks.alert).not.toHaveBeenCalled(); + act(() => palette.setPalette(true)); + act(() => palette.handlePaletteClose()); + act(() => palette.setPalette(true)); + expect(palette.color).toBe('#222222'); + expect(palette.saveError).toBe('common.saveFailed'); + }); + + it('쓰기 복구 후 같은 초안을 재시도하고 이후 외부 변경을 반영한다', async () => { + mocks.update + .mockRejectedValueOnce(new Error('disk full')) + .mockResolvedValue({}); + await act(async () => palette.handleColorChange('#222222')); + await act(async () => palette.handleColorChange(palette.color)); + + expect(mocks.update).toHaveBeenCalledTimes(2); + expect(mocks.update).toHaveBeenLastCalledWith({ + backgroundColor: '#222222', + }); + expect(useSettingsStore.getState().backgroundColor).toBe('#222222'); + expect(palette.saveError).toBeNull(); + expect(palette.isSaving).toBe(false); + act(() => useSettingsStore.getState().setBackgroundColor('#555555')); + expect(palette.color).toBe('#555555'); }); it('이전 저장 실패가 나중에 선택한 배경색을 덮지 않는다', async () => { @@ -75,6 +100,7 @@ describe('배경색 저장 실패 복원', () => { expect(useSettingsStore.getState().backgroundColor).toBe('#333333'); expect(palette.color).toBe('#333333'); + expect(palette.saveError).toBeNull(); }); it('저장값 재조회 중 도착한 외부 배경색을 덮지 않는다', async () => { @@ -90,5 +116,6 @@ describe('배경색 저장 실패 복원', () => { await act(async () => finishRead({ backgroundColor: '#111111' })); expect(useSettingsStore.getState().backgroundColor).toBe('#444444'); + expect(palette.color).toBe('#222222'); }); }); diff --git a/src/renderer/hooks/Modal/usePalette.ts b/src/renderer/hooks/Modal/usePalette.ts index 69e917ec9..26df8dbc1 100644 --- a/src/renderer/hooks/Modal/usePalette.ts +++ b/src/renderer/hooks/Modal/usePalette.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useRef, useCallback } from 'react'; import { useSettingsStore } from '@stores/useSettingsStore'; import { settingsApi } from '@api/modules/app/settingsApi'; import { useTranslation } from '@contexts/useTranslation'; @@ -6,40 +6,50 @@ import { useTranslation } from '@contexts/useTranslation'; export function usePalette() { const { t } = useTranslation(); const writeSequence = useRef(0); + const [saveError, setSaveError] = useState(null); + const [isSaving, setIsSaving] = useState(false); const [palette, setPalette] = useState(false); const backgroundColor = useSettingsStore((state) => state.backgroundColor); const setBackgroundColor = useSettingsStore( (state) => state.setBackgroundColor, ); - const [color, setColor] = useState(backgroundColor); + const [draftColor, setDraftColor] = useState(null); + const color = draftColor ?? backgroundColor; - useEffect(() => { - setColor(backgroundColor); - }, [backgroundColor]); - - const handleColorChange = (newColor: string) => { - const previousColor = useSettingsStore.getState().backgroundColor; - const sequence = ++writeSequence.current; - setColor(newColor); - setBackgroundColor(newColor); - settingsApi.update({ backgroundColor: newColor }).catch(async (error) => { - console.error('Failed to update background color', error); - if (sequence !== writeSequence.current) return; - let restoredColor = previousColor; - try { - restoredColor = (await settingsApi.get()).backgroundColor; - } catch (syncError) { - console.error('Failed to reload background color', syncError); - } - if (sequence !== writeSequence.current) return; - if (useSettingsStore.getState().backgroundColor === newColor) { - setBackgroundColor(restoredColor); - } - void window.api.ui.dialog - .alert(t('common.saveFailed'), { confirmText: t('common.ok') }) - .catch(() => {}); - }); - }; + const handleColorChange = useCallback( + (newColor: string) => { + const previousColor = useSettingsStore.getState().backgroundColor; + const sequence = ++writeSequence.current; + setIsSaving(true); + setDraftColor(newColor); + setBackgroundColor(newColor); + settingsApi + .update({ backgroundColor: newColor }) + .then(() => { + if (sequence !== writeSequence.current) return; + setSaveError(null); + setDraftColor(null); + setIsSaving(false); + }) + .catch(async (error) => { + console.error('Failed to update background color', error); + if (sequence !== writeSequence.current) return; + let restoredColor = previousColor; + try { + restoredColor = (await settingsApi.get()).backgroundColor; + } catch (syncError) { + console.error('Failed to reload background color', syncError); + } + if (sequence !== writeSequence.current) return; + if (useSettingsStore.getState().backgroundColor === newColor) { + setBackgroundColor(restoredColor); + } + setSaveError(t('common.saveFailed')); + setIsSaving(false); + }); + }, + [setBackgroundColor, t], + ); const handlePaletteClose = () => { if (palette) setPalette(false); @@ -51,6 +61,8 @@ export function usePalette() { return { color, + saveError, + isSaving, palette, setPalette, handleColorChange, diff --git a/src/renderer/windows/main/App.tsx b/src/renderer/windows/main/App.tsx index abeb9139a..9c09def02 100644 --- a/src/renderer/windows/main/App.tsx +++ b/src/renderer/windows/main/App.tsx @@ -172,8 +172,15 @@ export default function App() { handleUndo, handleRedo, } = useKeyManager(); - const { color, palette, setPalette, handleColorChange, handlePaletteClose } = - usePalette(); + const { + color, + palette, + setPalette, + handleColorChange, + handlePaletteClose, + saveError, + isSaving, + } = usePalette(); const [activeTool, setActiveTool] = useState('move'); const [toolbarAddRequest, setToolbarAddRequest] = useState<{ @@ -501,6 +508,19 @@ export default function App() { contentMountStrategy="after-paint" > + {saveError && ( +
+

{saveError}

+ +
+ )} {noteSettingPresence.mounted && shownNoteSettings && ( Date: Tue, 20 Oct 2026 19:48:06 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EB=B0=B0=EA=B2=BD=EC=83=89=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=BA=94?= =?UTF-8?q?=EB=B2=84=EC=8A=A4=20=EB=B0=B0=EA=B2=BD=EC=9D=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EA=B0=92=EC=9C=BC=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 그리드 캔버스가 팝업 초안 색을 받아 저장 실패 뒤에도 실패한 색이 남던 문제를 고친다. 캔버스는 적용된 배경색을, 팝업은 실패 초안을 각각 사용한다. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/renderer/hooks/Modal/usePalette.test.tsx | 2 ++ src/renderer/hooks/Modal/usePalette.ts | 2 ++ src/renderer/windows/main/App.tsx | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/renderer/hooks/Modal/usePalette.test.tsx b/src/renderer/hooks/Modal/usePalette.test.tsx index e9f2d8edc..108c9ec77 100644 --- a/src/renderer/hooks/Modal/usePalette.test.tsx +++ b/src/renderer/hooks/Modal/usePalette.test.tsx @@ -55,6 +55,7 @@ describe('배경색 저장 실패 복원', () => { await act(async () => palette.handleColorChange('#222222')); expect(useSettingsStore.getState().backgroundColor).toBe('#111111'); + expect(palette.backgroundColor).toBe('#111111'); expect(palette.color).toBe('#222222'); expect(palette.saveError).toBe('common.saveFailed'); expect(palette.isSaving).toBe(false); @@ -116,6 +117,7 @@ describe('배경색 저장 실패 복원', () => { await act(async () => finishRead({ backgroundColor: '#111111' })); expect(useSettingsStore.getState().backgroundColor).toBe('#444444'); + expect(palette.backgroundColor).toBe('#444444'); expect(palette.color).toBe('#222222'); }); }); diff --git a/src/renderer/hooks/Modal/usePalette.ts b/src/renderer/hooks/Modal/usePalette.ts index 26df8dbc1..d09e44f65 100644 --- a/src/renderer/hooks/Modal/usePalette.ts +++ b/src/renderer/hooks/Modal/usePalette.ts @@ -60,6 +60,8 @@ export function usePalette() { }; return { + // 캔버스 배경은 적용값, 팝업 입력은 실패 초안 포함 + backgroundColor, color, saveError, isSaving, diff --git a/src/renderer/windows/main/App.tsx b/src/renderer/windows/main/App.tsx index 9c09def02..6a455c527 100644 --- a/src/renderer/windows/main/App.tsx +++ b/src/renderer/windows/main/App.tsx @@ -173,6 +173,7 @@ export default function App() { handleRedo, } = useKeyManager(); const { + backgroundColor, color, palette, setPalette, @@ -436,7 +437,7 @@ export default function App() { Date: Tue, 20 Oct 2026 19:48:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=EB=A6=AC=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=20=EA=B8=B0=EC=A4=80=EC=A0=90=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=EC=8B=A4=EC=A0=9C=20=EC=98=A4=EB=B2=84=ED=96=89=20?= =?UTF-8?q?=EB=B3=B4=EC=A0=95=20=EB=8F=99=EC=9E=91=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스프라이트 오버행은 모든 기준점에서 보정되므로 좌상단과 위치 고정의 차이를 가장 왼쪽·위쪽 요소 변화 기준으로 설명한다. 한글 항목명을 설정 화면의 '위치 고정'에 맞춘다. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/content/en/guide/settings/page.mdx | 2 +- docs/content/ko/guide/settings/page.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/en/guide/settings/page.mdx b/docs/content/en/guide/settings/page.mdx index 152254a99..cf46153a9 100644 --- a/docs/content/en/guide/settings/page.mdx +++ b/docs/content/en/guide/settings/page.mdx @@ -78,7 +78,7 @@ Select the reference point when resizing the overlay window: - **center**: Fixed center, expand all directions - **fixed-position**: Preserve existing elements' screen positions when the window size or content origin changes -The default, top-left, fixes the window corner. When a sprite extends beyond the left or top edge and changes the content origin, existing elements can move on screen. Choose fixed-position to preserve their screen positions. +The default, top-left, keeps the window's top-left corner fixed. If you move an element past the left or top edge of the content, or delete the leftmost or topmost element, the remaining elements shift on screen. Choose fixed-position to preserve their screen positions. Sprites extending past the left or top edge are compensated under every anchor, so they do not move existing elements. ## Custom CSS/JS diff --git a/docs/content/ko/guide/settings/page.mdx b/docs/content/ko/guide/settings/page.mdx index dcb80ac4f..5a8093d79 100644 --- a/docs/content/ko/guide/settings/page.mdx +++ b/docs/content/ko/guide/settings/page.mdx @@ -75,9 +75,9 @@ DM Note의 다양한 설정 옵션을 안내합니다. - **좌하단(bottom-left)**: 좌하단 고정, 우상단으로 확장 - **우하단(bottom-right)**: 우하단 고정, 좌상단으로 확장 - **중앙(center)**: 중앙 고정, 모든 방향으로 확장 -- **고정(fixed-position)**: 창 크기와 콘텐츠 원점이 바뀌어도 기존 요소의 화면 위치 유지 +- **위치 고정(fixed-position)**: 창 크기와 콘텐츠 원점이 바뀌어도 기존 요소의 화면 위치 유지 -기본값인 좌상단은 창의 모서리를 고정합니다. 스프라이트가 왼쪽이나 위쪽으로 넘쳐 콘텐츠 원점이 바뀌면 기존 요소의 화면 위치도 이동할 수 있습니다. 기존 요소의 화면 위치를 유지하려면 고정을 선택합니다. +기본값인 좌상단은 창의 좌상단 모서리를 고정합니다. 요소를 콘텐츠의 왼쪽이나 위쪽 끝 너머로 옮기거나 가장 왼쪽·위쪽 요소를 삭제하면 나머지 요소의 화면 위치가 함께 이동합니다. 기존 요소의 화면 위치를 유지하려면 위치 고정을 선택합니다. 스프라이트가 왼쪽이나 위쪽으로 넘치는 경우는 모든 기준점에서 보정되므로 기존 요소가 움직이지 않습니다. ## 커스텀 CSS/JS