@farmzone/fz-template-react 0.0.1 → 1.0.1

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 (58) hide show
  1. package/bin/create.js +10 -4
  2. package/package.json +1 -1
  3. package/template/.env.example +5 -0
  4. package/template/eslint.config.js +4 -1
  5. package/template/index.css +15 -2
  6. package/template/index.html +1 -1
  7. package/template/package.json +55 -41
  8. package/template/public/favicon.ico +0 -0
  9. package/template/public/mockServiceWorker.js +349 -0
  10. package/template/src/app/App.tsx +2 -0
  11. package/template/src/app/api/api.ts +178 -0
  12. package/template/src/app/api/queries.ts +321 -0
  13. package/template/src/app/api/queryKey.ts +7 -0
  14. package/template/src/app/api/token.ts +7 -0
  15. package/template/src/app/layout/Layout.tsx +33 -16
  16. package/template/src/app/layout/ListContents.tsx +9 -0
  17. package/template/src/app/layout/ListHeader.tsx +41 -0
  18. package/template/src/app/layout/MultiTabNav.tsx +101 -0
  19. package/template/src/app/layout/Sidebar.tsx +33 -53
  20. package/template/src/app/layout/UserInfo.tsx +94 -0
  21. package/template/src/app/layout/menu.ts +46 -21
  22. package/template/src/app/layout/tabSwitchStore.ts +11 -0
  23. package/template/src/app/router/Router.tsx +54 -28
  24. package/template/src/app/store/index.ts +26 -0
  25. package/template/src/index.tsx +21 -12
  26. package/template/src/mocks/browser.ts +17 -0
  27. package/template/src/mocks/handlers.ts +43 -0
  28. package/template/src/mocks/scenarios.ts +57 -0
  29. package/template/src/pages/dashboard/index.tsx +541 -8
  30. package/template/src/pages/error/Error.tsx +29 -17
  31. package/template/src/pages/error/NotFound.tsx +27 -17
  32. package/template/src/pages/login/index.tsx +317 -0
  33. package/template/src/pages/post/PostFormModal.tsx +128 -0
  34. package/template/src/pages/post/detail/index.tsx +548 -0
  35. package/template/src/pages/post/index.tsx +267 -0
  36. package/template/src/pages/sample/SampleFormModal.tsx +77 -0
  37. package/template/src/pages/sample/detail/index.tsx +424 -0
  38. package/template/src/pages/sample/index.tsx +269 -0
  39. package/template/src/pages/system/log/index.tsx +173 -0
  40. package/template/src/pages/user/config/columns.tsx +109 -0
  41. package/template/src/pages/user/config/schema.ts +54 -0
  42. package/template/src/pages/user/index.tsx +641 -0
  43. package/template/src/shared/components/CommentInput.tsx +243 -0
  44. package/template/src/shared/components/FilePreviewCard.tsx +70 -0
  45. package/template/src/shared/config/text.ts +27 -0
  46. package/template/src/shared/config/type.ts +40 -0
  47. package/template/src/shared/utils/format.ts +11 -0
  48. package/template/src/types/auth.ts +10 -0
  49. package/template/src/types/comment.ts +33 -0
  50. package/template/src/types/common.ts +19 -0
  51. package/template/src/types/dashboard.ts +53 -0
  52. package/template/src/types/index.ts +16 -0
  53. package/template/src/types/log.ts +21 -0
  54. package/template/src/types/post.ts +32 -0
  55. package/template/src/types/sample.ts +28 -0
  56. package/template/src/types/user.ts +51 -0
  57. package/template/src/vite-env.d.ts +10 -0
  58. package/template/gitignore +0 -32
@@ -0,0 +1,243 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+
4
+ import { apiInstance } from "@/app/api/api";
5
+ import { USER_QUERY_KEY } from "@/app/api/queryKey";
6
+ import { usePostComment } from "@/app/api/queries";
7
+ import type { PageResponse, User } from "@/types";
8
+ import { Button } from "@farmzone/fz-react-ui";
9
+
10
+ interface CommentInputProps {
11
+ // create mode
12
+ targetType?: string;
13
+ targetId?: number;
14
+ parentCommentId?: number | null;
15
+ // edit mode — when onSave is provided, calls it instead of POST API
16
+ initialContent?: string;
17
+ initialMentionUserIds?: Array<number>;
18
+ onSave?: (content: string, mentionUserIds: Array<number>) => Promise<void>;
19
+ isSaving?: boolean;
20
+ saveLabel?: string;
21
+ onCancel?: () => void;
22
+ // common
23
+ placeholder?: string;
24
+ onSuccess?: () => void;
25
+ }
26
+
27
+ interface ActiveMention {
28
+ query: string;
29
+ atIndex: number;
30
+ }
31
+
32
+ // Find the @mention word the cursor is currently inside
33
+ function resolveMention(text: string, cursor: number): ActiveMention | null {
34
+ let i = cursor - 1;
35
+ while (i >= 0 && !/\s/.test(text[i])) i--;
36
+ const wordStart = i + 1;
37
+ if (wordStart >= cursor || text[wordStart] !== "@") return null;
38
+ return { query: text.slice(wordStart + 1, cursor), atIndex: wordStart };
39
+ }
40
+
41
+ export default function CommentInput({
42
+ targetType,
43
+ targetId,
44
+ parentCommentId,
45
+ initialContent = "",
46
+ initialMentionUserIds = [],
47
+ onSave,
48
+ isSaving = false,
49
+ saveLabel,
50
+ onCancel,
51
+ placeholder,
52
+ onSuccess,
53
+ }: CommentInputProps) {
54
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
55
+ const [content, setContent] = useState(initialContent);
56
+ const [mentionUserIds, setMentionUserIds] = useState<Array<number>>(initialMentionUserIds);
57
+ const [activeMention, setActiveMention] = useState<ActiveMention | null>(null);
58
+ const [highlightedIdx, setHighlightedIdx] = useState(0);
59
+
60
+ // Fetch users matching the current @query
61
+ const { data } = useQuery({
62
+ queryKey: [USER_QUERY_KEY, "mention", activeMention?.query],
63
+ queryFn: () =>
64
+ apiInstance
65
+ .get<PageResponse<User>>("/users", {
66
+ params: {
67
+ page: 0,
68
+ size: 8,
69
+ ...(activeMention?.query ? { keyword: activeMention.query, keywordType: "name" } : {}),
70
+ },
71
+ })
72
+ .then((r) => r.data),
73
+ enabled: activeMention !== null,
74
+ staleTime: 30_000,
75
+ });
76
+
77
+ const mentionUsers = data?.content ?? [];
78
+ const { mutateAsync: postComment, isPending } = usePostComment();
79
+
80
+ const syncMention = useCallback((text: string, cursor: number) => {
81
+ const next = resolveMention(text, cursor);
82
+ setActiveMention(next);
83
+ if (next) setHighlightedIdx(0);
84
+ }, []);
85
+
86
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
87
+ const { value, selectionStart } = e.target;
88
+ setContent(value);
89
+ syncMention(value, selectionStart ?? value.length);
90
+ };
91
+
92
+ // Also update on cursor position change (arrow keys, mouse click inside textarea)
93
+ const handleSelect = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
94
+ const { value, selectionStart } = e.currentTarget;
95
+ syncMention(value, selectionStart ?? value.length);
96
+ };
97
+
98
+ const selectUser = useCallback(
99
+ (user: User) => {
100
+ if (!activeMention) return;
101
+ const cursor = textareaRef.current?.selectionStart ?? content.length;
102
+ const insertText = `@${user.name} `;
103
+ const newContent = content.slice(0, activeMention.atIndex) + insertText + content.slice(cursor);
104
+
105
+ setContent(newContent);
106
+ setMentionUserIds((ids) => (ids.includes(user.id) ? ids : [...ids, user.id]));
107
+ setActiveMention(null);
108
+
109
+ requestAnimationFrame(() => {
110
+ if (!textareaRef.current) return;
111
+ const newPos = activeMention.atIndex + insertText.length;
112
+ textareaRef.current.focus();
113
+ textareaRef.current.setSelectionRange(newPos, newPos);
114
+ });
115
+ },
116
+ [activeMention, content],
117
+ );
118
+
119
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
120
+ if (!activeMention || mentionUsers.length === 0) return;
121
+
122
+ if (e.key === "ArrowDown") {
123
+ e.preventDefault();
124
+ setHighlightedIdx((i) => (i + 1) % mentionUsers.length);
125
+ } else if (e.key === "ArrowUp") {
126
+ e.preventDefault();
127
+ setHighlightedIdx((i) => (i - 1 + mentionUsers.length) % mentionUsers.length);
128
+ } else if (e.key === "Enter") {
129
+ e.preventDefault();
130
+ selectUser(mentionUsers[highlightedIdx]);
131
+ } else if (e.key === "Escape") {
132
+ e.preventDefault();
133
+ setActiveMention(null);
134
+ }
135
+ };
136
+
137
+ const handleSubmit = async () => {
138
+ const trimmed = content.trim();
139
+ if (!trimmed) return;
140
+ if (onSave) {
141
+ await onSave(trimmed, mentionUserIds);
142
+ } else {
143
+ await postComment({
144
+ targetType: targetType!,
145
+ targetId: targetId!,
146
+ parentCommentId: parentCommentId ?? null,
147
+ content: trimmed,
148
+ mentionUserIds: mentionUserIds.length > 0 ? mentionUserIds : undefined,
149
+ });
150
+ setContent("");
151
+ setMentionUserIds([]);
152
+ }
153
+ onSuccess?.();
154
+ };
155
+
156
+ const isPopoverOpen = activeMention !== null && mentionUsers.length > 0;
157
+
158
+ return (
159
+ <div className="relative">
160
+ {/* @멘션 팝오버 */}
161
+ {isPopoverOpen && (
162
+ <div className="absolute bottom-full left-0 mb-1.5 w-64 bg-white border border-gray-200 rounded-lg shadow-xl z-50 overflow-hidden">
163
+ <div className="px-3 py-1.5 border-b border-gray-100 bg-gray-50">
164
+ <span className="text-[10px] font-semibold text-gray-400 tracking-wide uppercase">
165
+ 사용자 멘션
166
+ </span>
167
+ </div>
168
+
169
+ <ul className="max-h-52 overflow-y-auto">
170
+ {mentionUsers.map((user, idx) => (
171
+ <li key={user.id}>
172
+ <button
173
+ type="button"
174
+ className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors ${
175
+ idx === highlightedIdx ? "bg-blue-50" : "hover:bg-gray-50"
176
+ }`}
177
+ onMouseDown={(e) => e.preventDefault()} // textarea 포커스 유지
178
+ onClick={() => selectUser(user)}
179
+ onMouseEnter={() => setHighlightedIdx(idx)}
180
+ >
181
+ <div className="w-7 h-7 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 text-xs font-bold shrink-0">
182
+ {user.name.slice(0, 1)}
183
+ </div>
184
+ <div className="min-w-0 text-left">
185
+ <div
186
+ className={`font-medium truncate leading-snug ${
187
+ idx === highlightedIdx ? "text-blue-700" : "text-gray-800"
188
+ }`}
189
+ >
190
+ {user.name}
191
+ </div>
192
+ <div className="text-[11px] text-gray-400 truncate leading-snug">{user.userId}</div>
193
+ </div>
194
+ </button>
195
+ </li>
196
+ ))}
197
+ </ul>
198
+
199
+ <div className="px-3 py-1.5 border-t border-gray-100 bg-gray-50">
200
+ <p className="text-[10px] text-gray-400">↑↓ 이동 · Enter 선택 · Esc 닫기</p>
201
+ </div>
202
+ </div>
203
+ )}
204
+
205
+ {/* 입력 영역 */}
206
+ <div className="border border-gray-200 rounded-lg overflow-hidden focus-within:border-[var(--color-main)] transition-colors">
207
+ <textarea
208
+ ref={textareaRef}
209
+ value={content}
210
+ onChange={handleChange}
211
+ onSelect={handleSelect}
212
+ onKeyDown={handleKeyDown}
213
+ placeholder={placeholder ?? "댓글을 입력하세요. @를 입력하면 사용자를 멘션할 수 있습니다."}
214
+ rows={3}
215
+ className="w-full px-3 py-2.5 text-sm resize-none focus:outline-none bg-white"
216
+ />
217
+ <div className="flex items-center justify-between px-3 py-2 border-t border-gray-100 bg-gray-50">
218
+ <span className="text-[11px] text-gray-400">
219
+ {mentionUserIds.length > 0
220
+ ? `${mentionUserIds.length}명 멘션됨`
221
+ : "@를 입력해 사용자를 멘션하세요"}
222
+ </span>
223
+ <div className="flex items-center gap-2">
224
+ {onCancel && (
225
+ <Button type="button" variant="outline" size="sm" onClick={onCancel}>
226
+ 취소
227
+ </Button>
228
+ )}
229
+ <Button
230
+ type="button"
231
+ variant="save"
232
+ size="sm"
233
+ onClick={() => void handleSubmit()}
234
+ disabled={!content.trim() || isPending || isSaving}
235
+ >
236
+ {isPending || isSaving ? `${saveLabel ?? "등록"} 중...` : (saveLabel ?? "등록")}
237
+ </Button>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ );
243
+ }
@@ -0,0 +1,70 @@
1
+ import { FileText } from "lucide-react";
2
+
3
+ import { cn, getPreviewFileType, useStableImageSrc } from "@farmzone/fz-react-ui";
4
+
5
+ interface FilePreviewCardProps {
6
+ src: string;
7
+ fileName: string;
8
+ fileSize?: string;
9
+ onClick?: () => void;
10
+ className?: string;
11
+ }
12
+
13
+ /** 예시용 파일 카드 UI — FilePreviewViewer와 분리, 사용처에서 자유롭게 커스터마이징 */
14
+ export default function FilePreviewCard(props: FilePreviewCardProps) {
15
+ const { src, fileName, fileSize, onClick, className } = props;
16
+
17
+ const fileType = getPreviewFileType(src);
18
+ const isImage = fileType === "image";
19
+ const imageSrc = useStableImageSrc(isImage ? src : undefined);
20
+
21
+ const shortName = fileName.length > 12 ? `${fileName.slice(0, 10)}...` : fileName;
22
+
23
+ if (isImage) {
24
+ return (
25
+ <button
26
+ type="button"
27
+ onClick={onClick}
28
+ className={cn(
29
+ "group flex flex-col w-full h-40 rounded-lg border border-gray-200 overflow-hidden bg-white hover:border-gray-300 hover:shadow-md transition-all duration-200 text-left cursor-pointer",
30
+ className,
31
+ )}
32
+ >
33
+ <div className="flex-1 min-h-0 bg-gray-50 overflow-hidden">
34
+ <img
35
+ src={imageSrc}
36
+ alt={fileName}
37
+ className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
38
+ />
39
+ </div>
40
+ <div className="flex items-center justify-between gap-2 border-t border-gray-200 px-3 py-2 text-xs">
41
+ <span className="truncate text-gray-800">{fileName}</span>
42
+ {fileSize ? <span className="shrink-0 text-gray-400">{fileSize}</span> : null}
43
+ </div>
44
+ </button>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <button
50
+ type="button"
51
+ onClick={onClick}
52
+ className={cn(
53
+ "group flex flex-col w-full h-40 rounded-lg border border-gray-200 overflow-hidden bg-white hover:border-gray-300 hover:shadow-md transition-all duration-200 text-left cursor-pointer",
54
+ className,
55
+ )}
56
+ >
57
+ <div className="flex flex-1 flex-col items-center justify-center gap-2 px-3 min-h-0 bg-gray-50">
58
+ <FileText
59
+ className="size-10 text-gray-400 shrink-0 transition-transform duration-200 group-hover:scale-110 group-hover:text-gray-500"
60
+ strokeWidth={1.5}
61
+ />
62
+ <span className="text-sm text-gray-700 truncate max-w-full text-center">{shortName}</span>
63
+ </div>
64
+ <div className="flex items-center justify-between gap-2 border-t border-gray-200 px-3 py-2 text-xs">
65
+ <span className="truncate text-gray-800">{fileName}</span>
66
+ {fileSize ? <span className="shrink-0 text-gray-400">{fileSize}</span> : null}
67
+ </div>
68
+ </button>
69
+ );
70
+ }
@@ -0,0 +1,27 @@
1
+ export const COMMON_MESSAGES = {
2
+ SAVE_SUCCESS: "정상적으로 저장되었습니다.",
3
+ UPDATE_SUCCESS: "정상적으로 수정되었습니다.",
4
+ DELETE_SUCCESS: "정상적으로 삭제되었습니다.",
5
+ };
6
+
7
+ export const SERVER_ERROR_MESSAGES = {
8
+ SERVER_ERROR_INVALID: "유효하지 않은 요청입니다. 다시 시도해 주세요.",
9
+ SERVER_ERROR_NETWORK_TITLE: "서비스 연결 불가.",
10
+ SERVER_ERROR_NETWORK: "현재 서비스 연결이 원활하지 않습니다.\n잠시 후 다시 시도해 주세요.",
11
+ SERVER_ERROR_TEMP_TITLE: "일시적인 시스템 오류.",
12
+ SERVER_ERROR_TEMP: "일시적인 오류가 발생했습니다.\n다시 시도해 주세요.",
13
+ };
14
+
15
+ export const COMMON_API_MSG_TO_KR: Record<string, string> = {
16
+ // 인증
17
+ "Invalid login credential.": "아이디 또는 비밀번호가 잘못 되었습니다.",
18
+ "Account is disabled.": "계정이 비활성화되어 있습니다. 관리자에게 문의해 주세요.",
19
+ // 파일 업로드
20
+ "The upload file is empty.": "업로드할 파일이 비어 있습니다.",
21
+ "You can upload up to 20 files.": "파일은 최대 20개까지 업로드할 수 있습니다.",
22
+ "This file type is not allowed.": "허용되지 않은 파일 형식입니다.",
23
+ "Image files can be uploaded up to 50MB.": "이미지 파일은 최대 50MB까지 업로드할 수 있습니다.",
24
+ "Document files can be uploaded up to 10MB.": "기타 파일은 최대 10MB까지 업로드할 수 있습니다.",
25
+ // 사용자
26
+ "User ID already in use.": "존재하는 아이디입니다.", //
27
+ };
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface DefaultParamsType {
4
+ page: number;
5
+ size: number;
6
+ keywordType: string;
7
+ keyword: string;
8
+ sortBy: string;
9
+ sortOrder: string;
10
+ }
11
+
12
+ export interface SelectOptionType {
13
+ label: string | ReactNode;
14
+ value: string | boolean;
15
+ }
16
+
17
+ export interface optionType {
18
+ label: string;
19
+ value: string;
20
+ }
21
+
22
+ export type UploadListType = "farm" | "cattle" | "slaughter" | "parentage" | "semen" | "pig";
23
+
24
+ export interface OptionalParamsType {
25
+ // date
26
+ startDate?: string;
27
+ endDate?: string;
28
+ // log
29
+ role?: string;
30
+ pageOption?: string;
31
+ }
32
+
33
+ export interface TotalParamsType extends OptionalParamsType, DefaultParamsType {}
34
+
35
+ export interface ExcelUploadResponseDataType {
36
+ id: number;
37
+ status: "PROCESSED" | "ERROR";
38
+ errorMessage: string | null;
39
+ [key: string]: string | number | null | unknown;
40
+ }
@@ -0,0 +1,11 @@
1
+ import dayjs from "dayjs";
2
+
3
+ export const formatDateTime = (value: string | null | undefined): string => {
4
+ if (!value) return "-";
5
+ return dayjs(value).format("YYYY-MM-DD HH:mm");
6
+ };
7
+
8
+ export const formatDate = (value: string | null | undefined): string => {
9
+ if (!value) return "-";
10
+ return dayjs(value).format("YYYY-MM-DD");
11
+ };
@@ -0,0 +1,10 @@
1
+ export interface LoginResponse {
2
+ id: string;
3
+ userId: string;
4
+ name: string;
5
+ role: string;
6
+ accessToken: string;
7
+ refreshToken: string;
8
+ accessTokenExpiresAt: string;
9
+ refreshTokenExpiresAt: string;
10
+ }
@@ -0,0 +1,33 @@
1
+ export interface CommentReply {
2
+ userId: number;
3
+ }
4
+
5
+ export interface Comment {
6
+ id: number;
7
+ targetType: string;
8
+ targetId: number;
9
+ parentCommentId: number | null;
10
+ userId: number | null;
11
+ userName: string;
12
+ content: string;
13
+ replies?: Array<Comment>;
14
+ createdAt: string;
15
+ updatedAt: string | null;
16
+ deletedAt: string | null;
17
+ mentionUserIds?: Array<number>;
18
+ }
19
+
20
+ export interface CommentForm {
21
+ targetType: string;
22
+ targetId: number;
23
+ parentCommentId: number | null;
24
+ content: string;
25
+ mentionUserIds?: Array<number>;
26
+ }
27
+
28
+ export type CommentTargetType = "SAMPLE" | "POST";
29
+
30
+ export interface CommentEditForm {
31
+ content: string;
32
+ mentionUserIds?: Array<number>;
33
+ }
@@ -0,0 +1,19 @@
1
+ export interface PageResponse<T> {
2
+ content: Array<T>;
3
+ totalElements: number;
4
+ totalPages: number;
5
+ page: number;
6
+ size: number;
7
+ }
8
+
9
+ export interface FileResponse {
10
+ id: number;
11
+ fileName: string;
12
+ filePath: string;
13
+ fileType: string;
14
+ fileSize: number;
15
+ serviceCode: string;
16
+ refId: number;
17
+ createdAt: string;
18
+ path?: string;
19
+ }
@@ -0,0 +1,53 @@
1
+ export interface DashboardSummary {
2
+ totalUsers: number;
3
+ todayJoinedUsers: number;
4
+ recentActiveUsers: number;
5
+ withdrawalRate: number;
6
+ }
7
+
8
+ export interface DashboardTrend {
9
+ labels: string[];
10
+ joinedCounts: number[];
11
+ withdrawnCounts: number[];
12
+ }
13
+
14
+ export interface DashboardGender {
15
+ maleRate: number;
16
+ femaleRate: number;
17
+ unknownRate: number;
18
+ }
19
+
20
+ export interface DashboardAgeGroup {
21
+ label: string;
22
+ count: number;
23
+ rate: number;
24
+ }
25
+
26
+ export interface DashboardDemographics {
27
+ gender: DashboardGender;
28
+ ageGroups: DashboardAgeGroup[];
29
+ }
30
+
31
+ export interface DashboardRecentUser {
32
+ id: number;
33
+ userId: string;
34
+ name: string;
35
+ gender: string;
36
+ age: number;
37
+ createdAt: string;
38
+ role: string;
39
+ active: boolean;
40
+ }
41
+
42
+ export interface DashboardDailyActiveUsers {
43
+ labels: string[];
44
+ counts: number[];
45
+ }
46
+
47
+ export interface UserDashboardResponse {
48
+ summary: DashboardSummary;
49
+ trend: DashboardTrend;
50
+ demographics: DashboardDemographics;
51
+ recentUsers: DashboardRecentUser[];
52
+ dailyActiveUsers: DashboardDailyActiveUsers;
53
+ }
@@ -0,0 +1,16 @@
1
+ export type { PageResponse, FileResponse } from "./common";
2
+ export type { LoginResponse } from "./auth";
3
+ export type { SampleCategory, Sample, GetSamplesParams, SampleForm } from "./sample";
4
+ export type { NoticeCategory, Post, GetPostsParams, PostForm } from "./post";
5
+ export type { CommentReply, Comment, CommentForm, CommentTargetType, CommentEditForm } from "./comment";
6
+ export type { UserRole, User, GetUsersParams, UserForm, UserEditForm } from "./user";
7
+ export type { ActionLog, GetLogsParams } from "./log";
8
+ export type {
9
+ UserDashboardResponse,
10
+ DashboardSummary,
11
+ DashboardTrend,
12
+ DashboardDemographics,
13
+ DashboardAgeGroup,
14
+ DashboardRecentUser,
15
+ DashboardDailyActiveUsers,
16
+ } from "./dashboard";
@@ -0,0 +1,21 @@
1
+ export interface ActionLog {
2
+ id: number;
3
+ userId: string;
4
+ userName: string;
5
+ action: string;
6
+ target: string;
7
+ detail: string;
8
+ createdAt: string;
9
+ }
10
+
11
+ export interface GetLogsParams {
12
+ page: number;
13
+ size: number;
14
+ userRole?: string;
15
+ action?: string;
16
+ pageOption?: string;
17
+ keywordType?: string;
18
+ keyword?: string;
19
+ startDate?: string;
20
+ endDate?: string;
21
+ }
@@ -0,0 +1,32 @@
1
+ import type { FileResponse } from "./common";
2
+
3
+ export type NoticeCategory = "NOTICE";
4
+
5
+ export interface Post {
6
+ id: number;
7
+ title: string;
8
+ content: string;
9
+ noticeCategory: NoticeCategory;
10
+ files?: Array<FileResponse>;
11
+ createdAt: string;
12
+ updatedAt: string;
13
+ writer: string; // User login id
14
+ visible: boolean;
15
+ }
16
+
17
+ export interface GetPostsParams {
18
+ page: number;
19
+ size: number;
20
+ title?: string;
21
+ noticeCategory?: string;
22
+ sortKey?: string;
23
+ sortOrder?: "asc" | "desc";
24
+ }
25
+
26
+ export interface PostForm {
27
+ title: string;
28
+ content: string;
29
+ noticeCategory: NoticeCategory;
30
+ visible: boolean;
31
+ writer: string;
32
+ }
@@ -0,0 +1,28 @@
1
+ export type SampleCategory = "BASIC" | "ADVANCED";
2
+
3
+ export interface Sample {
4
+ id: number;
5
+ name: string;
6
+ description: string;
7
+ category: SampleCategory;
8
+ priority: number;
9
+ active: boolean;
10
+ createdAt: string;
11
+ updatedAt: string;
12
+ }
13
+
14
+ export interface GetSamplesParams {
15
+ page: number;
16
+ size: number;
17
+ name?: string;
18
+ category?: string;
19
+ sortKey?: string;
20
+ sortOrder?: "asc" | "desc";
21
+ }
22
+
23
+ export interface SampleForm {
24
+ name: string;
25
+ description: string;
26
+ category: SampleCategory;
27
+ active: boolean;
28
+ }
@@ -0,0 +1,51 @@
1
+ import type { FileResponse } from "./common";
2
+
3
+ export type UserRole = "ADMIN" | "USER";
4
+
5
+ export interface User {
6
+ id: number;
7
+ userId: string;
8
+ name: string;
9
+ role: UserRole;
10
+ active: boolean;
11
+ gender?: string;
12
+ birthday?: string;
13
+ phone?: string;
14
+ deletedAt?: string | null;
15
+ createdAt: string;
16
+ updatedAt?: string;
17
+ lastLoginAt: string | null;
18
+ files?: Array<FileResponse>;
19
+ }
20
+
21
+ export interface GetUsersParams {
22
+ page: number;
23
+ size: number;
24
+ role?: string;
25
+ keywordType?: string;
26
+ keyword?: string;
27
+ dateType?: string;
28
+ startDate?: string;
29
+ endDate?: string;
30
+ }
31
+
32
+ export interface UserForm {
33
+ userId: string;
34
+ password: string;
35
+ name: string;
36
+ role: UserRole;
37
+ gender?: string;
38
+ birthday?: string;
39
+ phone?: string;
40
+ }
41
+
42
+ export interface UserEditForm {
43
+ name: string;
44
+ role: UserRole;
45
+ active: boolean;
46
+ gender?: string;
47
+ birthday?: string;
48
+ phone?: string;
49
+ password?: string;
50
+ deleteFileIds?: Array<number>;
51
+ }
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_APP_API_HOST: string;
5
+ readonly VITE_APP_API_VERSION: string;
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv;
10
+ }