Skip to content

Commit 2370232

Browse files
rlafurudReokyoungKimclaude
authored
Improve generation error handling and loading UX (#16)
- friendlyError(): map backend codes (no_matching_scenario, ...) and network errors to clear Korean messages - error banner gets a '다시 시도' retry button (re-runs last query) - loading shows an animated spinner Frontend build verified. Closes #7 Co-authored-by: ReokyoungKim <zmffhem1207@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2feab2f commit 2370232

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

frontend/src/App.jsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,25 @@ import MiniMap from './components/MiniMap'
66
import SimPlayback from './components/SimPlayback'
77
import JsonView from './components/JsonView'
88

9+
// Map backend error codes / raw messages to friendly Korean text.
10+
function friendlyError(err) {
11+
const map = {
12+
no_matching_scenario: '비슷한 시나리오를 찾지 못했어요. 프롬프트를 조금 더 구체적으로 적어보세요.',
13+
failed_to_load_scenario: '시나리오 데이터를 불러오지 못했어요. 잠시 후 다시 시도해주세요.',
14+
}
15+
if (map[err]) return map[err]
16+
if (typeof err === 'string' && /failed to fetch|networkerror|load failed/i.test(err)) {
17+
return '백엔드에 연결하지 못했어요. 서버가 실행 중인지 확인해주세요 (localhost:8000).'
18+
}
19+
return `생성에 실패했어요: ${err}`
20+
}
21+
922
export default function App() {
1023
const [catalog, setCatalog] = useState({ scenarios: [], maps: [] })
1124
const [loading, setLoading] = useState(false)
1225
const [error, setError] = useState(null)
1326
const [result, setResult] = useState(null)
27+
const [lastQuery, setLastQuery] = useState('')
1428

1529
useEffect(() => {
1630
getCatalog()
@@ -19,18 +33,19 @@ export default function App() {
1933
}, [])
2034

2135
async function handleGenerate(query) {
36+
setLastQuery(query)
2237
setLoading(true)
2338
setError(null)
2439
setResult(null)
2540
try {
2641
const res = await generate(query)
2742
if (res.error || !res.config) {
28-
setError(res.error || 'No config was generated for this prompt.')
43+
setError(friendlyError(res.error || 'no_matching_scenario'))
2944
} else {
3045
setResult(res)
3146
}
3247
} catch (e) {
33-
setError(e.message)
48+
setError(friendlyError(e.message))
3449
} finally {
3550
setLoading(false)
3651
}
@@ -56,9 +71,23 @@ export default function App() {
5671
onGenerate={handleGenerate}
5772
/>
5873

59-
{error && <div className="error">⚠️ {error}</div>}
74+
{error && !loading && (
75+
<div className="error">
76+
<span>⚠️ {error}</span>
77+
{lastQuery && (
78+
<button className="btn retry" onClick={() => handleGenerate(lastQuery)}>
79+
다시 시도
80+
</button>
81+
)}
82+
</div>
83+
)}
6084

61-
{loading && <div className="loading">생성 중… (DB 매칭 → 스크립트 작성)</div>}
85+
{loading && (
86+
<div className="loading">
87+
<span className="spinner" aria-hidden="true" />
88+
생성 중… (DB 매칭 → 스크립트 작성)
89+
</div>
90+
)}
6291

6392
{result && config && (
6493
<div className="result">

frontend/src/styles.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,23 @@ body {
8888
margin-top: 18px; padding: 14px 16px;
8989
background: #3b1414; border: 1px solid #7f1d1d; color: #fecaca;
9090
border-radius: 10px;
91+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
9192
}
93+
.error .retry {
94+
background: #7f1d1d; border-color: #b91c1c; color: #fee2e2; white-space: nowrap;
95+
}
96+
.error .retry:hover { background: #991b1b; }
9297
.loading {
9398
margin-top: 18px; padding: 14px 16px; color: var(--muted);
9499
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
100+
display: flex; align-items: center; gap: 10px;
101+
}
102+
.spinner {
103+
width: 16px; height: 16px; border-radius: 50%;
104+
border: 2px solid var(--border); border-top-color: var(--accent);
105+
display: inline-block; animation: spin 0.7s linear infinite;
95106
}
107+
@keyframes spin { to { transform: rotate(360deg); } }
96108

97109
/* result */
98110
.result { margin-top: 24px; display: flex; flex-direction: column; gap: 20px; }

0 commit comments

Comments
 (0)