feat(settings): SectionIntro 설명 paragraph + SyncHelpModal 풀어쓰기
dogfood: 설정 페이지의 각 section 이 너무 단답형이고 도움말 텍스트도 기술 용어 (rebase, fast-forward, NTP) 위주라 불친절. - 공통 SectionIntro 컴포넌트 신설 (12px gray paragraph, margin-bottom 12). - 6 section (AI 제공자 / Vision / 자동실행 / 백업 / 동기화 / 정보) 상단에 "이게 뭐고 왜 필요한지" 1-2 문장 안내 추가. 톤은 담백 + 업무적 (존댓말, Inkling 1인칭). - SyncHelpModal section 1, 2, 3 의 기술 용어를 사용자 언어로 풀어쓰기. "fetch + rebase" → "원격 변경 먼저 받아오기", "NTP" → "기기 시각 어긋남", "non-fast-forward push 거부" → "업로드 거부 시 자동 재시도" 등. 시각/레이아웃은 그대로 유지 — 텍스트 변경만. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,8 +54,8 @@ export function SyncHelpModal({ onClose, initialAnchor }: Props): React.ReactEle
|
||||
</div>
|
||||
|
||||
<section id="main-conflict" style={sectionStyle}>
|
||||
<h4 style={h4Style}>1. 충돌 해결 (메인 시나리오)</h4>
|
||||
<p style={pStyle}>같은 노트를 두 기기에서 동시에 수정하면 충돌이 발생한다. "충돌 해결…" 버튼이 활성화되면 ConflictModal 이 열려 path 별 결정 (내 것 사용 / 원격 사용) 을 받는다.</p>
|
||||
<h4 style={h4Style}>1. 충돌 해결 (직접 결정해야 하는 일)</h4>
|
||||
<p style={pStyle}>같은 노트를 두 기기에서 동시에 수정하면 어느 쪽을 남길지 Inkling 이 자동으로 결정할 수 없습니다. 이때 "충돌 해결…" 버튼이 활성화되고, 노트별로 "내 것 사용" 또는 "원격 사용" 을 골라주시면 됩니다.</p>
|
||||
|
||||
<p style={{ ...pStyle, marginTop: 10, fontWeight: 600 }}>편집/편집 — 가장 흔한 경우</p>
|
||||
<ul style={{ paddingLeft: 18, margin: '4px 0' }}>
|
||||
@@ -79,21 +79,23 @@ export function SyncHelpModal({ onClose, initialAnchor }: Props): React.ReactEle
|
||||
</section>
|
||||
|
||||
<section id="auto" style={sectionStyle}>
|
||||
<h4 style={h4Style}>2. 자동 처리 (내가 안 해도 되는 일)</h4>
|
||||
<h4 style={h4Style}>2. 자동으로 처리되는 일</h4>
|
||||
<p style={pStyle}>아래 동작은 Inkling 이 알아서 처리합니다. 충돌이 없으면 사용자가 신경 쓸 일은 없습니다.</p>
|
||||
<ul style={{ paddingLeft: 18, margin: '4px 0' }}>
|
||||
<li style={liStyle}><b>fetch + rebase</b>: sync 시작 시 원격 변경을 가져와 내 변경 위에 다시 쌓음 (linear history). conflict 없으면 자동 진행</li>
|
||||
<li style={liStyle}><b>첫 sync 순서</b>: 빈 원격에는 어느 기기든 먼저 push 가능. 두 번째 기기는 fetch 후 자동 rebase</li>
|
||||
<li style={liStyle}><b>push 거부 (non-fast-forward)</b>: 다른 기기가 먼저 push 했어도 자동 fetch + rebase + 재시도. 사용자 개입은 rebase conflict 발생 시에만</li>
|
||||
<li style={liStyle}><b>자동 sync 주기</b>: 기본 30분 (설정에서 변경). 앱 종료 시 자동 1회 추가</li>
|
||||
<li style={liStyle}><b>원격 변경 먼저 받아오기</b>: 동기화를 시작하면 다른 기기가 올린 변경을 먼저 받아와 내 변경 위에 차곡차곡 올려놓습니다. 양쪽이 같은 줄을 건드리지 않으면 자동으로 진행됩니다.</li>
|
||||
<li style={liStyle}><b>첫 동기화 순서</b>: 비어있는 원격 저장소에는 어느 기기든 먼저 올릴 수 있습니다. 두 번째 기기는 받아온 뒤 자동으로 합쳐집니다.</li>
|
||||
<li style={liStyle}><b>업로드 거부 시 자동 재시도</b>: 다른 기기가 이미 변경을 올려둔 상태라 내 업로드가 막혀도, 받아오기 + 합치기 + 재시도가 자동으로 진행됩니다. 사용자가 개입할 일은 같은 위치를 양쪽이 동시에 수정한 경우에만 생깁니다.</li>
|
||||
<li style={liStyle}><b>자동 동기화 주기</b>: 기본 30분 (설정에서 변경). 앱 종료 시에도 한 번 추가로 실행됩니다.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="silent" style={sectionStyle}>
|
||||
<h4 style={h4Style}>3. 조용히 잘못될 수 있는 케이스 (silent risk)</h4>
|
||||
<h4 style={h4Style}>3. 모르고 넘어가기 쉬운 함정</h4>
|
||||
<p style={pStyle}>아래 상황은 에러처럼 보이지 않지만 결과가 잘못 나올 수 있으니 미리 알아두는 것이 좋습니다.</p>
|
||||
<ul style={{ paddingLeft: 18, margin: '4px 0' }}>
|
||||
<li style={liStyle}><b>시계 어긋남 (NTP)</b>: 양 기기 시계가 다르면 timestamp 기반 merge 가 잘못된 결과를 낼 수 있음. macOS / Windows 모두 기본 NTP 동기화 켜져 있음 — 수동으로 끄지 말 것</li>
|
||||
<li style={liStyle}><b>두 기기 동시 수정 회피</b>: 같은 노트를 동시에 수정하면 conflict 가 더 자주 발생. 한 기기에서 작업 중이면 다른 기기에서 같은 노트 수정 자제</li>
|
||||
<li style={liStyle}><b>자동 sync 실패 silent</b>: 주기적 sync 실패 시 토스트 안 뜸. 마지막 sync 시각 / 결과는 설정 페이지에서 확인 — 주 1회 점검 권장</li>
|
||||
<li style={liStyle}><b>두 기기의 시각 어긋남</b>: 시계가 어긋나면 변경 순서가 뒤바뀌어 한쪽 수정이 묻힐 수 있습니다. macOS / Windows 모두 기본적으로 시각이 자동 동기화되니, 일부러 끄지 마세요.</li>
|
||||
<li style={liStyle}><b>같은 노트를 두 기기에서 동시에 수정</b>: 충돌이 더 자주 발생합니다. 한 기기에서 작업 중이라면 다른 기기에서 같은 노트는 만지지 않는 편이 안전합니다.</li>
|
||||
<li style={liStyle}><b>자동 동기화 실패는 조용히 지나갑니다</b>: 주기적 동기화가 실패해도 알림 토스트는 뜨지 않습니다. 마지막 동기화 시각과 결과는 설정 페이지에서 확인할 수 있으니, 주 1회 정도 점검을 권장합니다.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { inboxApi } from '../../api.js';
|
||||
import { VisionSection } from './VisionSection.js';
|
||||
import { SectionIntro } from './SectionIntro.js';
|
||||
|
||||
const endpointSchema = z.string().url();
|
||||
|
||||
@@ -78,6 +79,10 @@ export function AiProviderSection(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionIntro>
|
||||
메모를 자동으로 정리하는 AI 제공자입니다. Inkling 은 기본적으로 로컬 Ollama 를 사용해
|
||||
데이터가 기기 밖으로 나가지 않게 합니다. 사용할 모델과 접속 주소를 여기서 지정합니다.
|
||||
</SectionIntro>
|
||||
{/* v0.2.9 Cut B Task 15 — AI 자동 처리 토글 (가장 위, 스위치 의미가 가장 큰 결정) */}
|
||||
{aiEnabled !== null && (
|
||||
<label style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 12, fontSize: 13 }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { inboxApi } from '../../api.js';
|
||||
import { SectionIntro } from './SectionIntro.js';
|
||||
|
||||
export function BackupSection(): React.ReactElement {
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
@@ -14,6 +15,10 @@ export function BackupSection(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<SectionIntro>
|
||||
메모와 첨부 이미지를 안전하게 백업하거나 다른 기기로 옮길 때 사용합니다. 자동 백업은 매일
|
||||
앱 종료 시 1회 실행되고, 여기서는 필요할 때 수동으로 실행할 수 있습니다.
|
||||
</SectionIntro>
|
||||
<button onClick={() => run('지금 백업', () => inboxApi.runBackup())}>지금 백업</button>
|
||||
<button onClick={() => run('내보내기', () => inboxApi.runExport())}>내보내기...</button>
|
||||
<button onClick={() => run('백업에서 복원', () => inboxApi.runImport())}>백업에서 복원...</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { inboxApi } from '../../api.js';
|
||||
import { SectionIntro } from './SectionIntro.js';
|
||||
|
||||
interface AppInfo {
|
||||
version: string;
|
||||
@@ -20,6 +21,9 @@ export function InfoSection(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionIntro>
|
||||
문제 보고나 호환성 확인이 필요할 때 참고하실 정보입니다.
|
||||
</SectionIntro>
|
||||
<dl style={{ fontSize: 12, lineHeight: 1.6 }}>
|
||||
<dt style={{ fontWeight: 600 }}>버전</dt>
|
||||
<dd>{info.version}</dd>
|
||||
|
||||
12
src/renderer/inbox/components/settings/SectionIntro.tsx
Normal file
12
src/renderer/inbox/components/settings/SectionIntro.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props { children: React.ReactNode; }
|
||||
|
||||
/** Settings page 각 section 상단에 표시되는 간단한 설명 paragraph. */
|
||||
export function SectionIntro({ children }: Props): React.ReactElement {
|
||||
return (
|
||||
<p style={{ fontSize: 12, color: '#666', lineHeight: 1.6, margin: '0 0 12px 0' }}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { inboxApi } from '../../api.js';
|
||||
import type { SyncStatusSnapshot } from '@shared/types';
|
||||
import { ConflictModal } from '../ConflictModal.js';
|
||||
import { SyncHelpModal, type SyncHelpAnchor } from '../SyncHelpModal.js';
|
||||
import { SectionIntro } from './SectionIntro.js';
|
||||
|
||||
export function SyncSection(): React.ReactElement {
|
||||
const [url, setUrl] = useState('');
|
||||
@@ -64,6 +65,10 @@ export function SyncSection(): React.ReactElement {
|
||||
return (
|
||||
<section style={{ marginTop: 24 }}>
|
||||
<h3 style={{ fontSize: 14, marginBottom: 8 }}>동기화 저장소</h3>
|
||||
<SectionIntro>
|
||||
Git 저장소를 통해 여러 기기 간 메모를 동기화합니다. 단일 기기에서만 사용하시면 URL 을
|
||||
비워두셔도 됩니다. 자동 동기화 주기와 충돌 처리는 아래에서 설정합니다.
|
||||
</SectionIntro>
|
||||
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 8 }}>
|
||||
<input
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { inboxApi } from '../../api.js';
|
||||
import { SectionIntro } from './SectionIntro.js';
|
||||
|
||||
export function VisionSection(): React.ReactElement {
|
||||
const [models, setModels] = useState<string[]>([]);
|
||||
@@ -44,6 +45,10 @@ export function VisionSection(): React.ReactElement {
|
||||
return (
|
||||
<section style={{ marginTop: 16 }}>
|
||||
<h4 style={{ fontSize: 13, marginBottom: 6 }}>이미지 분석 모델 (선택사항)</h4>
|
||||
<SectionIntro>
|
||||
첨부 이미지를 함께 분석할 vision 지원 모델입니다. 텍스트용 모델과 별도로 지정할 수 있고,
|
||||
미지정 시 이미지 첨부 메모는 텍스트만 정리됩니다.
|
||||
</SectionIntro>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center', marginBottom: 6 }}>
|
||||
<select
|
||||
aria-label="이미지 분석 모델"
|
||||
|
||||
@@ -13,8 +13,8 @@ describe('SyncHelpModal', () => {
|
||||
it('4 섹션 헤더 렌더링', () => {
|
||||
render(<SyncHelpModal onClose={() => {}} />);
|
||||
expect(screen.getByRole('heading', { name: /충돌 해결/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /자동 처리/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /조용히 잘못될 수 있는/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /자동으로 처리되는 일/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /모르고 넘어가기 쉬운 함정/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /Setup/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user