@farmzone/fz-template-react 1.0.3 → 1.0.4

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.
Files changed (50) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +5 -5
  3. package/template/package.json +55 -55
  4. package/template/pnpm-lock.yaml +4214 -4214
  5. package/template/public/mockServiceWorker.js +349 -349
  6. package/template/src/app/api/api.ts +178 -178
  7. package/template/src/app/api/queries.ts +321 -321
  8. package/template/src/app/api/queryKey.ts +7 -7
  9. package/template/src/app/api/token.ts +7 -7
  10. package/template/src/app/layout/Layout.tsx +33 -33
  11. package/template/src/app/layout/ListContents.tsx +9 -9
  12. package/template/src/app/layout/ListHeader.tsx +41 -41
  13. package/template/src/app/layout/MultiTabNav.tsx +101 -101
  14. package/template/src/app/layout/Sidebar.tsx +33 -33
  15. package/template/src/app/layout/UserInfo.tsx +94 -94
  16. package/template/src/app/layout/tabSwitchStore.ts +11 -11
  17. package/template/src/app/router/Router.tsx +56 -56
  18. package/template/src/app/store/index.ts +26 -26
  19. package/template/src/index.tsx +21 -21
  20. package/template/src/mocks/browser.ts +17 -17
  21. package/template/src/mocks/handlers.ts +43 -43
  22. package/template/src/mocks/scenarios.ts +57 -57
  23. package/template/src/pages/dashboard/index.tsx +541 -541
  24. package/template/src/pages/error/Error.tsx +29 -29
  25. package/template/src/pages/error/NotFound.tsx +27 -27
  26. package/template/src/pages/login/index.tsx +317 -317
  27. package/template/src/pages/post/PostFormModal.tsx +128 -128
  28. package/template/src/pages/post/detail/index.tsx +548 -548
  29. package/template/src/pages/post/index.tsx +267 -267
  30. package/template/src/pages/sample/SampleFormModal.tsx +77 -77
  31. package/template/src/pages/sample/detail/index.tsx +424 -424
  32. package/template/src/pages/sample/index.tsx +269 -269
  33. package/template/src/pages/sample/modal/index.tsx +253 -253
  34. package/template/src/pages/system/log/index.tsx +173 -173
  35. package/template/src/pages/user/config/columns.tsx +109 -109
  36. package/template/src/pages/user/config/schema.ts +54 -54
  37. package/template/src/pages/user/index.tsx +641 -641
  38. package/template/src/shared/components/CommentInput.tsx +243 -243
  39. package/template/src/shared/config/text.ts +27 -27
  40. package/template/src/shared/utils/format.ts +11 -11
  41. package/template/src/types/auth.ts +10 -10
  42. package/template/src/types/comment.ts +33 -33
  43. package/template/src/types/common.ts +19 -19
  44. package/template/src/types/dashboard.ts +53 -53
  45. package/template/src/types/index.ts +16 -16
  46. package/template/src/types/log.ts +21 -21
  47. package/template/src/types/post.ts +32 -32
  48. package/template/src/types/sample.ts +28 -28
  49. package/template/src/types/user.ts +51 -51
  50. package/template/src/vite-env.d.ts +10 -10
@@ -1,253 +1,253 @@
1
- import { useState, type ReactNode } from "react";
2
- import { Button, Modal, Tab } from "@farmzone/fz-react-ui";
3
- import { ArrowRight, Clock, Maximize2, Minimize2, X } from "lucide-react";
4
- import ListHeader from "@/app/layout/ListHeader";
5
-
6
- const CATTLE = {
7
- id: "KOR-00212345678",
8
- farm: "송아지-34567",
9
- gender: "수",
10
- birthDate: "2026-03-01",
11
- breedingType: "비육우",
12
- status: "활동",
13
- barn: "우사1",
14
- farmName: "목장1",
15
- registrationCategory: "협동",
16
- registrationNumber: "12345678",
17
- currentStatus: "상정 중",
18
- };
19
-
20
- const HISTORY = [
21
- { id: 1, date: "2026-04-10", title: "사료 교체", desc: "비육 간등", active: true },
22
- { id: 2, date: "2026-05-15", title: "예방 접종", desc: "구제역", active: false },
23
- { id: 3, date: "2026-04-01", title: "정기 체중 측정", desc: "118kg (+30kg/일)", active: false },
24
- ];
25
-
26
- const TABS = ["기본 정보", "혈통 정보", "유전 능력", "질병 관리", "체중 관리"];
27
-
28
- function InfoRow({
29
- left,
30
- right,
31
- }: {
32
- left: { label: string; value: ReactNode; accent?: boolean };
33
- right: { label: string; value: ReactNode; accent?: boolean };
34
- }) {
35
- return (
36
- <div className="grid grid-cols-2 border-b border-gray-100 last:border-0">
37
- <div className="flex items-center gap-3 border-r border-gray-100 px-4 py-3">
38
- <span className="w-24 shrink-0 text-sm text-gray-500">{left.label}</span>
39
- <span className={`text-sm font-medium ${left.accent ? "text-green-600" : "text-gray-800"}`}>
40
- {left.value}
41
- </span>
42
- </div>
43
- <div className="flex items-center gap-3 px-4 py-3">
44
- <span className="w-20 shrink-0 text-sm text-gray-500">{right.label}</span>
45
- <span className={`text-sm font-medium ${right.accent ? "text-green-600" : "text-gray-800"}`}>
46
- {right.value}
47
- </span>
48
- </div>
49
- </div>
50
- );
51
- }
52
-
53
- function CattleDetailModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
54
- const [activeTab, setActiveTab] = useState(TABS[0]);
55
- const [isMaximized, setIsMaximized] = useState(false);
56
-
57
- const handleClose = () => {
58
- setIsMaximized(false);
59
- onClose();
60
- };
61
-
62
- return (
63
- <Modal
64
- isOpen={isOpen}
65
- onClose={handleClose}
66
- contentClassName={
67
- isMaximized
68
- ? "w-screen h-screen max-w-none max-h-none rounded-none bg-white overflow-hidden"
69
- : "w-200 max-w-[80vw] h-200 max-h-[90vh] rounded-xl bg-white overflow-hidden"
70
- }
71
- >
72
- {/* Header */}
73
- <div className="border-b border-gray-100 px-6 py-4">
74
- <div className="flex items-center justify-between gap-4">
75
- <div className="flex items-center gap-0 overflow-x-auto">
76
- {/* ID + status */}
77
- <div className="shrink-0 pr-6">
78
- <h2 className="text-xl font-bold text-gray-900">{CATTLE.id}</h2>
79
- <div className="mt-1.5 flex items-center gap-2">
80
- <span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-700">
81
- {CATTLE.status}
82
- </span>
83
- <span className="flex items-center gap-1 text-xs text-gray-400">
84
- <span>{CATTLE.barn}</span>
85
- <ArrowRight className="h-3 w-3" />
86
- <span>{CATTLE.farmName}</span>
87
- </span>
88
- </div>
89
- </div>
90
-
91
- {/* Divider */}
92
- <div className="mx-1 h-10 w-px shrink-0 bg-gray-200" />
93
-
94
- {/* Meta fields */}
95
- <div className="flex items-center">
96
- <div className="px-5">
97
- <p className="text-xs text-gray-400">목장</p>
98
- <p className="mt-0.5 text-sm font-semibold text-gray-800">{CATTLE.farm}</p>
99
- </div>
100
- <div className="h-8 w-px shrink-0 bg-gray-200" />
101
- <div className="px-5">
102
- <p className="text-xs text-gray-400">성별</p>
103
- <p className="mt-0.5 text-sm font-semibold text-gray-800">{CATTLE.gender}</p>
104
- </div>
105
- <div className="h-8 w-px shrink-0 bg-gray-200" />
106
- <div className="px-5">
107
- <p className="text-xs text-gray-400">생년월일</p>
108
- <p className="mt-0.5 text-sm font-semibold text-green-600">{CATTLE.birthDate}</p>
109
- </div>
110
- <div className="h-8 w-px shrink-0 bg-gray-200" />
111
- <div className="px-5">
112
- <p className="text-xs text-gray-400">사육 구분</p>
113
- <p className="mt-0.5 text-sm font-bold text-gray-900">{CATTLE.breedingType}</p>
114
- </div>
115
- </div>
116
- </div>
117
-
118
- {/* Action buttons */}
119
- <div className="flex shrink-0 items-center gap-0.5">
120
- <Button
121
- variant="ghost"
122
- size="icon"
123
- onClick={() => setIsMaximized((v) => !v)}
124
- className="rounded p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
125
- >
126
- {isMaximized ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
127
- </Button>
128
- <Button
129
- variant="ghost"
130
- size="icon"
131
- onClick={handleClose}
132
- className="rounded p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
133
- >
134
- <X className="h-4 w-4" />
135
- </Button>
136
- </div>
137
- </div>
138
- </div>
139
-
140
- {/* Tabs */}
141
- <Tab
142
- tabs={TABS.map((tab) => ({ label: tab, value: tab }))}
143
- activeTab={activeTab}
144
- onChange={setActiveTab}
145
- showContent={false}
146
- showIndicator
147
- tabListClassName="px-4"
148
- tabClassName="p-4 text-body hover:text-blue-500 hover:bg-gray-50"
149
- indicatorClassName="h-0.5 bg-blue-400"
150
- />
151
-
152
- {/* Body */}
153
- <div className="flex max-h-[500px] border-t-1 border-gray-200">
154
- {/* Main content */}
155
- <div className="flex-1 overflow-y-auto p-5">
156
- {activeTab === "기본 정보" ? (
157
- <>
158
- <div className="mb-4 overflow-hidden rounded-xs border border-gray-200">
159
- <div className="border-b border-gray-200 bg-gray-50 px-4 py-2.5 text-sm font-semibold text-gray-700">
160
- 상세 정보
161
- </div>
162
- <InfoRow
163
- left={{ label: "개체식별번호", value: CATTLE.id }}
164
- right={{ label: "목장", value: CATTLE.farm }}
165
- />
166
- <InfoRow
167
- left={{ label: "성별", value: CATTLE.gender }}
168
- right={{ label: "생년월일", value: CATTLE.birthDate }}
169
- />
170
- <InfoRow
171
- left={{ label: "등록구분", value: CATTLE.registrationCategory }}
172
- right={{ label: "등록번호", value: CATTLE.registrationNumber }}
173
- />
174
- <InfoRow
175
- left={{ label: "사육구분", value: CATTLE.breedingType }}
176
- right={{
177
- label: "현재 상태",
178
- value: (
179
- <span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-700">
180
- {CATTLE.currentStatus}
181
- </span>
182
- ),
183
- }}
184
- />
185
- </div>
186
-
187
- <div className="overflow-hidden rounded-lg border border-gray-200">
188
- <div className="border-b border-gray-200 bg-gray-50 px-4 py-2.5 text-xs font-semibold uppercase tracking-wider text-gray-500">
189
- Asset Visualization
190
- </div>
191
- <div className="flex h-44 items-center justify-center bg-gray-100">
192
- <img
193
- src="https://placehold.co/780x176/d1d5db/9ca3af?text=Cattle+Image"
194
- alt="cattle visualization"
195
- className="h-full w-full object-cover"
196
- />
197
- </div>
198
- </div>
199
- </>
200
- ) : (
201
- <div className="flex h-40 items-center justify-center text-sm text-gray-400">준비 중입니다.</div>
202
- )}
203
- </div>
204
-
205
- {/* Recent history sidebar */}
206
- <div className="w-52 shrink-0 overflow-y-auto border-l border-gray-200 p-4">
207
- <div className="mb-4 flex items-center gap-2">
208
- <Clock className="h-4 w-4 text-gray-500" />
209
- <span className="text-sm font-semibold text-gray-700">최근 이력</span>
210
- </div>
211
- <div>
212
- {HISTORY.map((item, i) => (
213
- <div key={item.id} className="relative flex gap-3 pb-5 last:pb-0">
214
- {i < HISTORY.length - 1 && (
215
- <div className="absolute left-[7px] top-4 h-full w-px bg-gray-200" />
216
- )}
217
- <div
218
- className={`mt-0.5 h-[15px] w-[15px] shrink-0 rounded-full ${
219
- item.active ? "bg-green-500" : "bg-gray-300"
220
- }`}
221
- />
222
- <div>
223
- <p className="text-xs text-gray-400">{item.date}</p>
224
- <p className="mt-0.5 text-sm font-semibold text-gray-800">{item.title}</p>
225
- <p className="text-xs text-gray-500">{item.desc}</p>
226
- </div>
227
- </div>
228
- ))}
229
- </div>
230
- </div>
231
- </div>
232
- </Modal>
233
- );
234
- }
235
-
236
- export default function SampleModalPage() {
237
- const [isOpen, setIsOpen] = useState(false);
238
-
239
- return (
240
- <div className="p-6">
241
- <ListHeader title="샘플 모달" />
242
- <div className="mt-6 flex items-center justify-center rounded-xl border border-dashed border-gray-300 bg-white p-16">
243
- <div className="text-center">
244
- <p className="mb-4 text-sm text-gray-500">아래 버튼을 클릭하여 개체 상세 모달을 확인하세요.</p>
245
- <Button variant="save" onClick={() => setIsOpen(true)}>
246
- 개체 상세 보기
247
- </Button>
248
- </div>
249
- </div>
250
- <CattleDetailModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
251
- </div>
252
- );
253
- }
1
+ import { useState, type ReactNode } from "react";
2
+ import { Button, Modal, Tab } from "@farmzone/fz-react-ui";
3
+ import { ArrowRight, Clock, Maximize2, Minimize2, X } from "lucide-react";
4
+ import ListHeader from "@/app/layout/ListHeader";
5
+
6
+ const CATTLE = {
7
+ id: "KOR-00212345678",
8
+ farm: "송아지-34567",
9
+ gender: "수",
10
+ birthDate: "2026-03-01",
11
+ breedingType: "비육우",
12
+ status: "활동",
13
+ barn: "우사1",
14
+ farmName: "목장1",
15
+ registrationCategory: "협동",
16
+ registrationNumber: "12345678",
17
+ currentStatus: "상정 중",
18
+ };
19
+
20
+ const HISTORY = [
21
+ { id: 1, date: "2026-04-10", title: "사료 교체", desc: "비육 간등", active: true },
22
+ { id: 2, date: "2026-05-15", title: "예방 접종", desc: "구제역", active: false },
23
+ { id: 3, date: "2026-04-01", title: "정기 체중 측정", desc: "118kg (+30kg/일)", active: false },
24
+ ];
25
+
26
+ const TABS = ["기본 정보", "혈통 정보", "유전 능력", "질병 관리", "체중 관리"];
27
+
28
+ function InfoRow({
29
+ left,
30
+ right,
31
+ }: {
32
+ left: { label: string; value: ReactNode; accent?: boolean };
33
+ right: { label: string; value: ReactNode; accent?: boolean };
34
+ }) {
35
+ return (
36
+ <div className="grid grid-cols-2 border-b border-gray-100 last:border-0">
37
+ <div className="flex items-center gap-3 border-r border-gray-100 px-4 py-3">
38
+ <span className="w-24 shrink-0 text-sm text-gray-500">{left.label}</span>
39
+ <span className={`text-sm font-medium ${left.accent ? "text-green-600" : "text-gray-800"}`}>
40
+ {left.value}
41
+ </span>
42
+ </div>
43
+ <div className="flex items-center gap-3 px-4 py-3">
44
+ <span className="w-20 shrink-0 text-sm text-gray-500">{right.label}</span>
45
+ <span className={`text-sm font-medium ${right.accent ? "text-green-600" : "text-gray-800"}`}>
46
+ {right.value}
47
+ </span>
48
+ </div>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function CattleDetailModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
54
+ const [activeTab, setActiveTab] = useState(TABS[0]);
55
+ const [isMaximized, setIsMaximized] = useState(false);
56
+
57
+ const handleClose = () => {
58
+ setIsMaximized(false);
59
+ onClose();
60
+ };
61
+
62
+ return (
63
+ <Modal
64
+ isOpen={isOpen}
65
+ onClose={handleClose}
66
+ contentClassName={
67
+ isMaximized
68
+ ? "w-screen h-screen max-w-none max-h-none rounded-none bg-white overflow-hidden"
69
+ : "w-200 max-w-[80vw] h-200 max-h-[90vh] rounded-xl bg-white overflow-hidden"
70
+ }
71
+ >
72
+ {/* Header */}
73
+ <div className="border-b border-gray-100 px-6 py-4">
74
+ <div className="flex items-center justify-between gap-4">
75
+ <div className="flex items-center gap-0 overflow-x-auto">
76
+ {/* ID + status */}
77
+ <div className="shrink-0 pr-6">
78
+ <h2 className="text-xl font-bold text-gray-900">{CATTLE.id}</h2>
79
+ <div className="mt-1.5 flex items-center gap-2">
80
+ <span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-700">
81
+ {CATTLE.status}
82
+ </span>
83
+ <span className="flex items-center gap-1 text-xs text-gray-400">
84
+ <span>{CATTLE.barn}</span>
85
+ <ArrowRight className="h-3 w-3" />
86
+ <span>{CATTLE.farmName}</span>
87
+ </span>
88
+ </div>
89
+ </div>
90
+
91
+ {/* Divider */}
92
+ <div className="mx-1 h-10 w-px shrink-0 bg-gray-200" />
93
+
94
+ {/* Meta fields */}
95
+ <div className="flex items-center">
96
+ <div className="px-5">
97
+ <p className="text-xs text-gray-400">목장</p>
98
+ <p className="mt-0.5 text-sm font-semibold text-gray-800">{CATTLE.farm}</p>
99
+ </div>
100
+ <div className="h-8 w-px shrink-0 bg-gray-200" />
101
+ <div className="px-5">
102
+ <p className="text-xs text-gray-400">성별</p>
103
+ <p className="mt-0.5 text-sm font-semibold text-gray-800">{CATTLE.gender}</p>
104
+ </div>
105
+ <div className="h-8 w-px shrink-0 bg-gray-200" />
106
+ <div className="px-5">
107
+ <p className="text-xs text-gray-400">생년월일</p>
108
+ <p className="mt-0.5 text-sm font-semibold text-green-600">{CATTLE.birthDate}</p>
109
+ </div>
110
+ <div className="h-8 w-px shrink-0 bg-gray-200" />
111
+ <div className="px-5">
112
+ <p className="text-xs text-gray-400">사육 구분</p>
113
+ <p className="mt-0.5 text-sm font-bold text-gray-900">{CATTLE.breedingType}</p>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ {/* Action buttons */}
119
+ <div className="flex shrink-0 items-center gap-0.5">
120
+ <Button
121
+ variant="ghost"
122
+ size="icon"
123
+ onClick={() => setIsMaximized((v) => !v)}
124
+ className="rounded p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
125
+ >
126
+ {isMaximized ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
127
+ </Button>
128
+ <Button
129
+ variant="ghost"
130
+ size="icon"
131
+ onClick={handleClose}
132
+ className="rounded p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
133
+ >
134
+ <X className="h-4 w-4" />
135
+ </Button>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ {/* Tabs */}
141
+ <Tab
142
+ tabs={TABS.map((tab) => ({ label: tab, value: tab }))}
143
+ activeTab={activeTab}
144
+ onChange={setActiveTab}
145
+ showContent={false}
146
+ showIndicator
147
+ tabListClassName="px-4"
148
+ tabClassName="p-4 text-body hover:text-blue-500 hover:bg-gray-50"
149
+ indicatorClassName="h-0.5 bg-blue-400"
150
+ />
151
+
152
+ {/* Body */}
153
+ <div className="flex max-h-[500px] border-t-1 border-gray-200">
154
+ {/* Main content */}
155
+ <div className="flex-1 overflow-y-auto p-5">
156
+ {activeTab === "기본 정보" ? (
157
+ <>
158
+ <div className="mb-4 overflow-hidden rounded-xs border border-gray-200">
159
+ <div className="border-b border-gray-200 bg-gray-50 px-4 py-2.5 text-sm font-semibold text-gray-700">
160
+ 상세 정보
161
+ </div>
162
+ <InfoRow
163
+ left={{ label: "개체식별번호", value: CATTLE.id }}
164
+ right={{ label: "목장", value: CATTLE.farm }}
165
+ />
166
+ <InfoRow
167
+ left={{ label: "성별", value: CATTLE.gender }}
168
+ right={{ label: "생년월일", value: CATTLE.birthDate }}
169
+ />
170
+ <InfoRow
171
+ left={{ label: "등록구분", value: CATTLE.registrationCategory }}
172
+ right={{ label: "등록번호", value: CATTLE.registrationNumber }}
173
+ />
174
+ <InfoRow
175
+ left={{ label: "사육구분", value: CATTLE.breedingType }}
176
+ right={{
177
+ label: "현재 상태",
178
+ value: (
179
+ <span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-700">
180
+ {CATTLE.currentStatus}
181
+ </span>
182
+ ),
183
+ }}
184
+ />
185
+ </div>
186
+
187
+ <div className="overflow-hidden rounded-lg border border-gray-200">
188
+ <div className="border-b border-gray-200 bg-gray-50 px-4 py-2.5 text-xs font-semibold uppercase tracking-wider text-gray-500">
189
+ Asset Visualization
190
+ </div>
191
+ <div className="flex h-44 items-center justify-center bg-gray-100">
192
+ <img
193
+ src="https://placehold.co/780x176/d1d5db/9ca3af?text=Cattle+Image"
194
+ alt="cattle visualization"
195
+ className="h-full w-full object-cover"
196
+ />
197
+ </div>
198
+ </div>
199
+ </>
200
+ ) : (
201
+ <div className="flex h-40 items-center justify-center text-sm text-gray-400">준비 중입니다.</div>
202
+ )}
203
+ </div>
204
+
205
+ {/* Recent history sidebar */}
206
+ <div className="w-52 shrink-0 overflow-y-auto border-l border-gray-200 p-4">
207
+ <div className="mb-4 flex items-center gap-2">
208
+ <Clock className="h-4 w-4 text-gray-500" />
209
+ <span className="text-sm font-semibold text-gray-700">최근 이력</span>
210
+ </div>
211
+ <div>
212
+ {HISTORY.map((item, i) => (
213
+ <div key={item.id} className="relative flex gap-3 pb-5 last:pb-0">
214
+ {i < HISTORY.length - 1 && (
215
+ <div className="absolute left-[7px] top-4 h-full w-px bg-gray-200" />
216
+ )}
217
+ <div
218
+ className={`mt-0.5 h-[15px] w-[15px] shrink-0 rounded-full ${
219
+ item.active ? "bg-green-500" : "bg-gray-300"
220
+ }`}
221
+ />
222
+ <div>
223
+ <p className="text-xs text-gray-400">{item.date}</p>
224
+ <p className="mt-0.5 text-sm font-semibold text-gray-800">{item.title}</p>
225
+ <p className="text-xs text-gray-500">{item.desc}</p>
226
+ </div>
227
+ </div>
228
+ ))}
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </Modal>
233
+ );
234
+ }
235
+
236
+ export default function SampleModalPage() {
237
+ const [isOpen, setIsOpen] = useState(false);
238
+
239
+ return (
240
+ <div className="p-6">
241
+ <ListHeader title="샘플 모달" />
242
+ <div className="mt-6 flex items-center justify-center rounded-xl border border-dashed border-gray-300 bg-white p-16">
243
+ <div className="text-center">
244
+ <p className="mb-4 text-sm text-gray-500">아래 버튼을 클릭하여 개체 상세 모달을 확인하세요.</p>
245
+ <Button variant="save" onClick={() => setIsOpen(true)}>
246
+ 개체 상세 보기
247
+ </Button>
248
+ </div>
249
+ </div>
250
+ <CattleDetailModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
251
+ </div>
252
+ );
253
+ }