@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.
- package/package.json +1 -1
- package/template/.env.example +5 -5
- package/template/package.json +55 -55
- package/template/pnpm-lock.yaml +4214 -4214
- package/template/public/mockServiceWorker.js +349 -349
- package/template/src/app/api/api.ts +178 -178
- package/template/src/app/api/queries.ts +321 -321
- package/template/src/app/api/queryKey.ts +7 -7
- package/template/src/app/api/token.ts +7 -7
- package/template/src/app/layout/Layout.tsx +33 -33
- package/template/src/app/layout/ListContents.tsx +9 -9
- package/template/src/app/layout/ListHeader.tsx +41 -41
- package/template/src/app/layout/MultiTabNav.tsx +101 -101
- package/template/src/app/layout/Sidebar.tsx +33 -33
- package/template/src/app/layout/UserInfo.tsx +94 -94
- package/template/src/app/layout/tabSwitchStore.ts +11 -11
- package/template/src/app/router/Router.tsx +56 -56
- package/template/src/app/store/index.ts +26 -26
- package/template/src/index.tsx +21 -21
- package/template/src/mocks/browser.ts +17 -17
- package/template/src/mocks/handlers.ts +43 -43
- package/template/src/mocks/scenarios.ts +57 -57
- package/template/src/pages/dashboard/index.tsx +541 -541
- package/template/src/pages/error/Error.tsx +29 -29
- package/template/src/pages/error/NotFound.tsx +27 -27
- package/template/src/pages/login/index.tsx +317 -317
- package/template/src/pages/post/PostFormModal.tsx +128 -128
- package/template/src/pages/post/detail/index.tsx +548 -548
- package/template/src/pages/post/index.tsx +267 -267
- package/template/src/pages/sample/SampleFormModal.tsx +77 -77
- package/template/src/pages/sample/detail/index.tsx +424 -424
- package/template/src/pages/sample/index.tsx +269 -269
- package/template/src/pages/sample/modal/index.tsx +253 -253
- package/template/src/pages/system/log/index.tsx +173 -173
- package/template/src/pages/user/config/columns.tsx +109 -109
- package/template/src/pages/user/config/schema.ts +54 -54
- package/template/src/pages/user/index.tsx +641 -641
- package/template/src/shared/components/CommentInput.tsx +243 -243
- package/template/src/shared/config/text.ts +27 -27
- package/template/src/shared/utils/format.ts +11 -11
- package/template/src/types/auth.ts +10 -10
- package/template/src/types/comment.ts +33 -33
- package/template/src/types/common.ts +19 -19
- package/template/src/types/dashboard.ts +53 -53
- package/template/src/types/index.ts +16 -16
- package/template/src/types/log.ts +21 -21
- package/template/src/types/post.ts +32 -32
- package/template/src/types/sample.ts +28 -28
- package/template/src/types/user.ts +51 -51
- 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
|
+
}
|