@walwal-harness/cli 7.1.39 → 7.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HR-Resource/cdo/SKILL.md +9 -1
- package/HR-Resource/ceo/SKILL.md +33 -2
- package/HR-Resource/coo/SKILL.md +9 -1
- package/HR-Resource/cqo/SKILL.md +9 -1
- package/HR-Resource/cto/SKILL.md +10 -2
- package/HR-Resource/ops/SKILL.md +20 -2
- package/HR-Resource/ops-prompt-inspector/SKILL.md +105 -0
- package/HR-Resource/ops-prompt-inspector/agents/installation-agent.json +7 -0
- package/HR-Resource/ops-prompt-inspector/assets/PromptInspector.tsx +599 -0
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/references/handler-templates.md +255 -0
- package/HR-Resource/ops-prompt-inspector/scripts/discover_apis.py +509 -0
- package/HR-Resource/ops-prompt-inspector/scripts/install.sh +32 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +381 -0
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/components/Scene.tsx +568 -35
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +4 -3
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const VERSION = '1.2.0-static';
|
|
3
|
+
const ID = 'harness-ops-prompt-inspector';
|
|
4
|
+
if (window.__HARNESS_OPS_PROMPT_INSPECTOR__) return;
|
|
5
|
+
window.__HARNESS_OPS_PROMPT_INSPECTOR__ = { version: VERSION };
|
|
6
|
+
|
|
7
|
+
const state = {
|
|
8
|
+
selecting: false,
|
|
9
|
+
target: null,
|
|
10
|
+
bindings: [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const css = `
|
|
14
|
+
#${ID}-bar{position:fixed;right:18px;bottom:18px;z-index:2147483647;display:flex;gap:8px;align-items:center;background:#111827;color:#fff;border:1px solid #374151;border-radius:8px;padding:10px;box-shadow:0 16px 36px rgba(0,0,0,.32);font:13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
15
|
+
#${ID}-bar button,#${ID}-panel button{border:0;border-radius:6px;padding:8px 10px;background:#2563eb;color:#fff;cursor:pointer;font:inherit}
|
|
16
|
+
#${ID}-bar button.secondary,#${ID}-panel button.secondary{background:#374151}
|
|
17
|
+
#${ID}-bar button.danger{background:#dc2626}
|
|
18
|
+
#${ID}-panel{position:fixed;right:18px;bottom:76px;width:min(420px,calc(100vw - 36px));z-index:2147483647;background:#fff;color:#111827;border:1px solid #d1d5db;border-radius:8px;box-shadow:0 18px 48px rgba(0,0,0,.28);padding:14px;font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
19
|
+
#${ID}-panel h3{margin:0 0 10px;font-size:15px}
|
|
20
|
+
#${ID}-panel label{display:block;margin:10px 0 4px;color:#374151;font-weight:600}
|
|
21
|
+
#${ID}-panel input,#${ID}-panel textarea,#${ID}-panel select{width:100%;box-sizing:border-box;border:1px solid #d1d5db;border-radius:6px;padding:8px;font:inherit}
|
|
22
|
+
#${ID}-panel textarea{min-height:72px;resize:vertical}
|
|
23
|
+
#${ID}-panel .row{display:flex;gap:8px;margin-top:12px}
|
|
24
|
+
#${ID}-highlight{position:fixed;z-index:2147483646;pointer-events:none;border:2px solid #f59e0b;background:rgba(245,158,11,.12);border-radius:4px}
|
|
25
|
+
.${ID}-badge{position:absolute;z-index:2147483645;background:#f59e0b;color:#111827;border-radius:999px;padding:2px 7px;font:11px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.24)}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const style = document.createElement('style');
|
|
29
|
+
style.textContent = css;
|
|
30
|
+
document.head.appendChild(style);
|
|
31
|
+
|
|
32
|
+
const bar = document.createElement('div');
|
|
33
|
+
bar.id = `${ID}-bar`;
|
|
34
|
+
bar.innerHTML = `
|
|
35
|
+
<strong>Prompt Inspector</strong>
|
|
36
|
+
<button type="button" data-action="select">Select</button>
|
|
37
|
+
<button type="button" class="secondary" data-action="export">Export</button>
|
|
38
|
+
<button type="button" class="danger" data-action="clear">Clear</button>
|
|
39
|
+
`;
|
|
40
|
+
document.body.appendChild(bar);
|
|
41
|
+
|
|
42
|
+
const highlight = document.createElement('div');
|
|
43
|
+
highlight.id = `${ID}-highlight`;
|
|
44
|
+
highlight.hidden = true;
|
|
45
|
+
document.body.appendChild(highlight);
|
|
46
|
+
|
|
47
|
+
function selectorFor(el) {
|
|
48
|
+
if (!el || el === document.body) return 'body';
|
|
49
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
50
|
+
const parts = [];
|
|
51
|
+
let node = el;
|
|
52
|
+
while (node && node.nodeType === 1 && node !== document.body && parts.length < 4) {
|
|
53
|
+
let part = node.tagName.toLowerCase();
|
|
54
|
+
if (node.classList && node.classList.length) {
|
|
55
|
+
part += `.${Array.from(node.classList).slice(0, 2).map((c) => CSS.escape(c)).join('.')}`;
|
|
56
|
+
}
|
|
57
|
+
const parent = node.parentElement;
|
|
58
|
+
if (parent) {
|
|
59
|
+
const same = Array.from(parent.children).filter((child) => child.tagName === node.tagName);
|
|
60
|
+
if (same.length > 1) part += `:nth-of-type(${same.indexOf(node) + 1})`;
|
|
61
|
+
}
|
|
62
|
+
parts.unshift(part);
|
|
63
|
+
node = parent;
|
|
64
|
+
}
|
|
65
|
+
return parts.join(' > ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function positionHighlight(el) {
|
|
69
|
+
if (!el) {
|
|
70
|
+
highlight.hidden = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const rect = el.getBoundingClientRect();
|
|
74
|
+
highlight.hidden = false;
|
|
75
|
+
highlight.style.left = `${rect.left}px`;
|
|
76
|
+
highlight.style.top = `${rect.top}px`;
|
|
77
|
+
highlight.style.width = `${rect.width}px`;
|
|
78
|
+
highlight.style.height = `${rect.height}px`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function panelFor(el) {
|
|
82
|
+
const selector = selectorFor(el);
|
|
83
|
+
const panel = document.createElement('div');
|
|
84
|
+
panel.id = `${ID}-panel`;
|
|
85
|
+
panel.innerHTML = `
|
|
86
|
+
<h3>Bind selected element</h3>
|
|
87
|
+
<label>Selector</label>
|
|
88
|
+
<input value="${selector.replace(/"/g, '"')}" data-field="selector">
|
|
89
|
+
<label>Mode</label>
|
|
90
|
+
<select data-field="mode">
|
|
91
|
+
<option value="comment">Comment Only</option>
|
|
92
|
+
<option value="api">Connect API</option>
|
|
93
|
+
</select>
|
|
94
|
+
<label>API / Note</label>
|
|
95
|
+
<input placeholder="GET /api/example or short title" data-field="api">
|
|
96
|
+
<label>Comment</label>
|
|
97
|
+
<textarea placeholder="Describe intended behavior, visual issue, or API binding." data-field="comment"></textarea>
|
|
98
|
+
<div class="row">
|
|
99
|
+
<button type="button" data-action="save">Save</button>
|
|
100
|
+
<button type="button" class="secondary" data-action="cancel">Cancel</button>
|
|
101
|
+
</div>
|
|
102
|
+
`;
|
|
103
|
+
document.body.appendChild(panel);
|
|
104
|
+
panel.querySelector('[data-action="save"]').addEventListener('click', () => {
|
|
105
|
+
const binding = {
|
|
106
|
+
route: `${location.pathname}${location.hash || ''}`,
|
|
107
|
+
selector: panel.querySelector('[data-field="selector"]').value,
|
|
108
|
+
mode: panel.querySelector('[data-field="mode"]').value,
|
|
109
|
+
api: panel.querySelector('[data-field="api"]').value.trim(),
|
|
110
|
+
comment: panel.querySelector('[data-field="comment"]').value.trim(),
|
|
111
|
+
};
|
|
112
|
+
state.bindings.push(binding);
|
|
113
|
+
addBadge(el, state.bindings.length);
|
|
114
|
+
panel.remove();
|
|
115
|
+
state.target = null;
|
|
116
|
+
positionHighlight(null);
|
|
117
|
+
});
|
|
118
|
+
panel.querySelector('[data-action="cancel"]').addEventListener('click', () => {
|
|
119
|
+
panel.remove();
|
|
120
|
+
state.target = null;
|
|
121
|
+
positionHighlight(null);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function addBadge(el, index) {
|
|
126
|
+
const rect = el.getBoundingClientRect();
|
|
127
|
+
const badge = document.createElement('div');
|
|
128
|
+
badge.className = `${ID}-badge`;
|
|
129
|
+
badge.textContent = `PI-${index}`;
|
|
130
|
+
badge.style.left = `${Math.max(6, rect.left + window.scrollX)}px`;
|
|
131
|
+
badge.style.top = `${Math.max(6, rect.top + window.scrollY - 20)}px`;
|
|
132
|
+
document.body.appendChild(badge);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function exportMarkdown() {
|
|
136
|
+
const lines = [`# Prompt Inspector`, ``, `Route: ${location.pathname}${location.hash || ''}`, ``];
|
|
137
|
+
state.bindings.forEach((binding, i) => {
|
|
138
|
+
lines.push(`## ${i + 1}. ${binding.mode === 'api' ? 'API Binding' : 'Comment'}`);
|
|
139
|
+
lines.push(`- Selector: \`${binding.selector}\``);
|
|
140
|
+
if (binding.api) lines.push(`- API: ${binding.api}`);
|
|
141
|
+
if (binding.comment) lines.push(`- Comment: ${binding.comment}`);
|
|
142
|
+
lines.push('');
|
|
143
|
+
});
|
|
144
|
+
const markdown = lines.join('\n');
|
|
145
|
+
navigator.clipboard?.writeText(markdown).catch(() => {});
|
|
146
|
+
console.log(markdown);
|
|
147
|
+
alert(`Prompt Inspector export copied/logged. Bindings: ${state.bindings.length}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
document.addEventListener('mouseover', (event) => {
|
|
151
|
+
if (!state.selecting || bar.contains(event.target)) return;
|
|
152
|
+
positionHighlight(event.target);
|
|
153
|
+
}, true);
|
|
154
|
+
|
|
155
|
+
document.addEventListener('click', (event) => {
|
|
156
|
+
if (!state.selecting || bar.contains(event.target)) return;
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
state.selecting = false;
|
|
160
|
+
state.target = event.target;
|
|
161
|
+
bar.querySelector('[data-action="select"]').textContent = 'Select';
|
|
162
|
+
document.getElementById(`${ID}-panel`)?.remove();
|
|
163
|
+
panelFor(event.target);
|
|
164
|
+
}, true);
|
|
165
|
+
|
|
166
|
+
bar.addEventListener('click', (event) => {
|
|
167
|
+
const action = event.target && event.target.getAttribute('data-action');
|
|
168
|
+
if (action === 'select') {
|
|
169
|
+
state.selecting = !state.selecting;
|
|
170
|
+
event.target.textContent = state.selecting ? 'Selecting...' : 'Select';
|
|
171
|
+
if (!state.selecting) positionHighlight(null);
|
|
172
|
+
}
|
|
173
|
+
if (action === 'export') exportMarkdown();
|
|
174
|
+
if (action === 'clear') {
|
|
175
|
+
state.bindings = [];
|
|
176
|
+
document.querySelectorAll(`.${ID}-badge`).forEach((el) => el.remove());
|
|
177
|
+
positionHighlight(null);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
})();
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# API Handler Templates
|
|
2
|
+
|
|
3
|
+
## Success/Failure Handler Types
|
|
4
|
+
|
|
5
|
+
### 1. Toast Messages
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// Using react-hot-toast
|
|
9
|
+
import toast from 'react-hot-toast';
|
|
10
|
+
|
|
11
|
+
// Success
|
|
12
|
+
toast.success('${message}');
|
|
13
|
+
|
|
14
|
+
// Error
|
|
15
|
+
toast.error('${message}');
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// Using sonner
|
|
20
|
+
import { toast } from 'sonner';
|
|
21
|
+
|
|
22
|
+
// Success
|
|
23
|
+
toast.success('${message}');
|
|
24
|
+
|
|
25
|
+
// Error
|
|
26
|
+
toast.error('${message}');
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Using react-toastify
|
|
31
|
+
import { toast } from 'react-toastify';
|
|
32
|
+
|
|
33
|
+
// Success
|
|
34
|
+
toast.success('${message}');
|
|
35
|
+
|
|
36
|
+
// Error
|
|
37
|
+
toast.error('${message}');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Alert/Popup Dialogs
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// Native browser alert
|
|
44
|
+
alert('${message}');
|
|
45
|
+
|
|
46
|
+
// Using shadcn/ui AlertDialog
|
|
47
|
+
import {
|
|
48
|
+
AlertDialog,
|
|
49
|
+
AlertDialogAction,
|
|
50
|
+
AlertDialogContent,
|
|
51
|
+
AlertDialogDescription,
|
|
52
|
+
AlertDialogFooter,
|
|
53
|
+
AlertDialogHeader,
|
|
54
|
+
AlertDialogTitle,
|
|
55
|
+
} from "@/components/ui/alert-dialog";
|
|
56
|
+
|
|
57
|
+
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
|
58
|
+
<AlertDialogContent>
|
|
59
|
+
<AlertDialogHeader>
|
|
60
|
+
<AlertDialogTitle>${title}</AlertDialogTitle>
|
|
61
|
+
<AlertDialogDescription>${message}</AlertDialogDescription>
|
|
62
|
+
</AlertDialogHeader>
|
|
63
|
+
<AlertDialogFooter>
|
|
64
|
+
<AlertDialogAction onClick={() => setIsOpen(false)}>
|
|
65
|
+
확인
|
|
66
|
+
</AlertDialogAction>
|
|
67
|
+
</AlertDialogFooter>
|
|
68
|
+
</AlertDialogContent>
|
|
69
|
+
</AlertDialog>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Page Navigation
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// Using Next.js App Router
|
|
76
|
+
import { useRouter } from 'next/navigation';
|
|
77
|
+
|
|
78
|
+
const router = useRouter();
|
|
79
|
+
|
|
80
|
+
// Success redirect
|
|
81
|
+
router.push('${successPath}');
|
|
82
|
+
|
|
83
|
+
// With replace (no back button)
|
|
84
|
+
router.replace('${successPath}');
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// Using Next.js Pages Router
|
|
89
|
+
import { useRouter } from 'next/router';
|
|
90
|
+
|
|
91
|
+
const router = useRouter();
|
|
92
|
+
router.push('${successPath}');
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. State Update
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// Update local state
|
|
99
|
+
setData(response.data);
|
|
100
|
+
setLoading(false);
|
|
101
|
+
setError(null);
|
|
102
|
+
|
|
103
|
+
// With React Query
|
|
104
|
+
queryClient.invalidateQueries({ queryKey: ['${queryKey}'] });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Combined Handler Patterns
|
|
110
|
+
|
|
111
|
+
### Pattern A: Toast + State Update
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
const handleSuccess = (data: ResponseType) => {
|
|
115
|
+
toast.success('${successMessage}');
|
|
116
|
+
setData(data);
|
|
117
|
+
setLoading(false);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleError = (error: Error) => {
|
|
121
|
+
toast.error('${errorMessage}');
|
|
122
|
+
setError(error.message);
|
|
123
|
+
setLoading(false);
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Pattern B: Toast + Navigation
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
const handleSuccess = () => {
|
|
131
|
+
toast.success('${successMessage}');
|
|
132
|
+
router.push('${successPath}');
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleError = (error: Error) => {
|
|
136
|
+
toast.error(error.message || '${errorMessage}');
|
|
137
|
+
};
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Pattern C: Popup Dialog + Navigation
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const handleSuccess = () => {
|
|
144
|
+
setDialogConfig({
|
|
145
|
+
open: true,
|
|
146
|
+
title: '${successTitle}',
|
|
147
|
+
message: '${successMessage}',
|
|
148
|
+
onConfirm: () => router.push('${successPath}'),
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleError = (error: Error) => {
|
|
153
|
+
setDialogConfig({
|
|
154
|
+
open: true,
|
|
155
|
+
title: '${errorTitle}',
|
|
156
|
+
message: error.message || '${errorMessage}',
|
|
157
|
+
onConfirm: () => setDialogConfig({ open: false }),
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Trigger Patterns
|
|
165
|
+
|
|
166
|
+
### onLoad (페이지 로딩 시)
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// Using useEffect
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
fetchData();
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
// Using React Query
|
|
175
|
+
const { data, isLoading, error } = useQuery({
|
|
176
|
+
queryKey: ['${queryKey}'],
|
|
177
|
+
queryFn: () => fetch('${apiPath}').then(res => res.json()),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Using SWR
|
|
181
|
+
const { data, error, isLoading } = useSWR('${apiPath}', fetcher);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### onClick (클릭 시)
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
<button onClick={async () => {
|
|
188
|
+
try {
|
|
189
|
+
setLoading(true);
|
|
190
|
+
const response = await axios.${method}('${apiPath}', ${hasBody ? 'payload' : ''});
|
|
191
|
+
handleSuccess(response.data);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
handleError(error);
|
|
194
|
+
}
|
|
195
|
+
}}>
|
|
196
|
+
${buttonText}
|
|
197
|
+
</button>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### onSubmit (폼 제출 시)
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
try {
|
|
206
|
+
setLoading(true);
|
|
207
|
+
const response = await axios.${method}('${apiPath}', formData);
|
|
208
|
+
handleSuccess(response.data);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
handleError(error);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
<form onSubmit={handleSubmit}>
|
|
215
|
+
{/* form fields */}
|
|
216
|
+
<button type="submit" disabled={loading}>
|
|
217
|
+
{loading ? '처리 중...' : '${submitText}'}
|
|
218
|
+
</button>
|
|
219
|
+
</form>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### onBlur (포커스 해제 시)
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<input
|
|
226
|
+
onBlur={async (e) => {
|
|
227
|
+
const value = e.target.value;
|
|
228
|
+
if (value !== previousValue) {
|
|
229
|
+
try {
|
|
230
|
+
await axios.${method}('${apiPath}', { value });
|
|
231
|
+
handleSuccess();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
handleError(error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### onChange with Debounce (입력 변경 시)
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
import { useDebouncedCallback } from 'use-debounce';
|
|
244
|
+
|
|
245
|
+
const debouncedSave = useDebouncedCallback(async (value: string) => {
|
|
246
|
+
try {
|
|
247
|
+
await axios.${method}('${apiPath}', { value });
|
|
248
|
+
handleSuccess();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
handleError(error);
|
|
251
|
+
}
|
|
252
|
+
}, 500);
|
|
253
|
+
|
|
254
|
+
<input onChange={(e) => debouncedSave(e.target.value)} />
|
|
255
|
+
```
|