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:
th-kim0823
2026-05-14 13:12:23 +09:00
parent 2b5ba8a50e
commit 906e9b6f7d
8 changed files with 51 additions and 13 deletions

View File

@@ -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>

View File

@@ -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 }}>

View File

@@ -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>

View File

@@ -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>

View 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>
);
}

View File

@@ -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

View File

@@ -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="이미지 분석 모델"

View File

@@ -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();
});