Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
- [FE] `autoInfer.ts`์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋ฐ UI ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ 100% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
- [FE] โฌ‡๏ธ **DBML Export**: ERD ๋‹ค์ด์–ด๊ทธ๋žจ์„ DBML (Database Markup Language) ํ˜•์‹์œผ๋กœ ๋‚ด๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒ๋‹จ์˜ DBML ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
- [FE] ๐Ÿ“š **Data Dictionary Export**: ERD ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ CSV ๋ฐ Markdown์œผ๋กœ ๋‚ด๋ณด๋‚ด๋ฉฐ, CSV formula injection๊ณผ Markdown ๋ Œ๋”๋ง escape๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
- [FE] ๐ŸŽจ **ํ”„๋กœ์ ํŠธ ๋ฐ ์—ฐ๊ฒฐ ์ƒ์„ฑ ํผ ์—”ํ„ฐํ‚ค ์ œ์ถœ ์ง€์›**: ํผ(`div` -> `form`) ์š”์†Œ๋ฅผ ํ†ตํ•ด ๋„ค์ดํ‹ฐ๋ธŒ HTML form ์ œ์ถœ(Enter ํ‚ค ๋™์ž‘)๊ณผ ์›น ์ ‘๊ทผ์„ฑ(WCAG ํ‚ค๋ณด๋“œ ์กฐ์ž‘)์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
10 changes: 5 additions & 5 deletions frontend/src/App.coverage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,20 +347,20 @@ describe('App orchestration coverage', () => {
fireEvent.click(screen.getByRole('button', { name: 'ํŽธ์ง‘๊ธฐ' }))

fireEvent.change(screen.getByLabelText('New project'), { target: { value: ' New ' } })
fireEvent.click(screen.getByRole('button', { name: 'Create' }))
fireEvent.submit(screen.getByLabelText('New project').closest('form')!)
await waitFor(() => expect(api.createProject).toHaveBeenCalledWith('New'))

const dsn = screen.getByLabelText('Connection DSN')
fireEvent.change(dsn, { target: { value: 'postgresql://[' } })
fireEvent.click(screen.getByRole('button', { name: 'Save connection' }))
fireEvent.submit(screen.getByLabelText('Connection DSN').closest('form')!)
expect(screen.getByRole('alert')).toHaveTextContent('Connection DSN must use')
fireEvent.change(dsn, { target: { value: 'http://bad.example/db' } })
fireEvent.click(screen.getByRole('button', { name: 'Save connection' }))
fireEvent.submit(screen.getByLabelText('Connection DSN').closest('form')!)
expect(screen.getByRole('alert')).toHaveTextContent('Connection DSN must use')
expect(dsn).toHaveValue('')

fireEvent.change(dsn, { target: { value: 'postgresql://db.example/test' } })
fireEvent.click(screen.getByRole('button', { name: 'Save connection' }))
fireEvent.submit(screen.getByLabelText('Connection DSN').closest('form')!)
await waitFor(() => expect(api.createConnection).toHaveBeenCalledWith('p3', 'target-db', 'postgresql://db.example/test'))

fireEvent.change(screen.getByLabelText('Schema filter (optional)'), { target: { value: ' public ' } })
Expand Down Expand Up @@ -682,7 +682,7 @@ describe('App orchestration coverage', () => {
await renderReadyApp()
fireEvent.click(screen.getByRole('button', { name: 'ํŽธ์ง‘๊ธฐ' }))
fireEvent.change(screen.getByLabelText('Connection DSN'), { target: { value: 'postgresql://db.example/test' } })
fireEvent.click(screen.getByRole('button', { name: 'Save connection' }))
fireEvent.submit(screen.getByLabelText('Connection DSN').closest('form')!)
await waitFor(() => expect(api.createConnection).toHaveBeenCalled())
fireEvent.click(screen.getByRole('button', { name: 'Reverse engineer โ†’ snapshot' }))
await waitFor(() => expect(api.createSnapshot).toHaveBeenCalledWith('p1', 'c2', undefined))
Expand Down
30 changes: 16 additions & 14 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,8 @@ export default function App() {
setLayoutMessage("๋˜๋Œ๋ ธ์Šต๋‹ˆ๋‹ค");
}

async function onCreateProject() {
async function handleProjectSubmit(e?: React.FormEvent) {
e?.preventDefault();
const nextProjectName = projectName.trim();
/* v8 ignore next -- the create control is disabled for both guard states */
if (!nextProjectName || isCreatingProject) return;
Expand All @@ -954,7 +955,8 @@ export default function App() {
}
}

async function onCreateConnection() {
async function handleConnectionSubmit(e?: React.FormEvent) {
e?.preventDefault();
/* v8 ignore next -- the save control is disabled without a project or while saving */
if (!selectedProjectId || isCreatingConnection) return;
const nextConnectionName = connName.trim();
Expand Down Expand Up @@ -1078,7 +1080,7 @@ export default function App() {
</div>
</div>

<div className="field">
<form className="field" onSubmit={handleProjectSubmit}>
<label htmlFor="project-name">New project</label>
<div className="row">
<input
Expand All @@ -1087,8 +1089,7 @@ export default function App() {
onChange={(e) => setProjectName(e.target.value)}
/>
<button
type="button"
onClick={onCreateProject}
type="submit"
disabled={!projectName.trim() || isCreatingProject}
aria-busy={isCreatingProject}
aria-describedby={
Expand All @@ -1103,7 +1104,7 @@ export default function App() {
{createProjectHint}
</span>
) : null}
</div>
</form>

<hr />

Expand All @@ -1126,7 +1127,7 @@ export default function App() {
</select>
</div>

<div className="field">
<form className="field" onSubmit={handleConnectionSubmit}>
<label htmlFor="conn-name">New connection (DSN)</label>
<input
id="conn-name"
Expand All @@ -1145,8 +1146,7 @@ export default function App() {
aria-label="Connection DSN"
/>
<button
type="button"
onClick={onCreateConnection}
type="submit"
disabled={
!selectedProjectId ||
!connName.trim() ||
Expand All @@ -1165,7 +1165,7 @@ export default function App() {
{createConnectionHint}
</span>
) : null}
</div>
</form>

<div className="field">
<label htmlFor="schema-filter">Schema filter (optional)</label>
Expand Down Expand Up @@ -1309,20 +1309,22 @@ export default function App() {
<h1 id="projects-title">ํ”„๋กœ์ ํŠธ</h1>
<p>ํ”„๋กœ์ ํŠธ๋ฅผ ์„ ํƒํ•˜๋ฉด ํ•ด๋‹น ๋‹ค์ด์–ด๊ทธ๋žจ ๋ชฉ๋ก์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
</div>
<div className="inlineCreate">
<form
className="inlineCreate"
onSubmit={handleProjectSubmit}
>
<input
aria-label="์ƒˆ ํ”„๋กœ์ ํŠธ ์ด๋ฆ„"
value={projectName}
onChange={(event) => setProjectName(event.currentTarget.value)}
/>
<button
type="button"
onClick={onCreateProject}
type="submit"
disabled={!projectName.trim() || isCreatingProject}
>
{isCreatingProject ? "์ƒ์„ฑ ์ค‘" : "์ƒˆ ํ”„๋กœ์ ํŠธ"}
</button>
</div>
</form>
</div>
<div className="dataTable" role="table" aria-label="ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก">
<div className="dataTable__row dataTable__row--projects dataTable__row--head" role="row">
Expand Down
Loading