@farmzone/fz-template-react 1.0.2 → 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 (51) 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 -0
  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/menu.ts +4 -1
  17. package/template/src/app/layout/tabSwitchStore.ts +11 -11
  18. package/template/src/app/router/Router.tsx +56 -54
  19. package/template/src/app/store/index.ts +26 -26
  20. package/template/src/index.tsx +21 -21
  21. package/template/src/mocks/browser.ts +17 -17
  22. package/template/src/mocks/handlers.ts +43 -43
  23. package/template/src/mocks/scenarios.ts +57 -57
  24. package/template/src/pages/dashboard/index.tsx +541 -541
  25. package/template/src/pages/error/Error.tsx +29 -29
  26. package/template/src/pages/error/NotFound.tsx +27 -27
  27. package/template/src/pages/login/index.tsx +317 -317
  28. package/template/src/pages/post/PostFormModal.tsx +128 -128
  29. package/template/src/pages/post/detail/index.tsx +548 -548
  30. package/template/src/pages/post/index.tsx +267 -267
  31. package/template/src/pages/sample/SampleFormModal.tsx +77 -77
  32. package/template/src/pages/sample/detail/index.tsx +424 -424
  33. package/template/src/pages/sample/index.tsx +269 -269
  34. package/template/src/pages/sample/modal/index.tsx +253 -0
  35. package/template/src/pages/system/log/index.tsx +173 -173
  36. package/template/src/pages/user/config/columns.tsx +109 -109
  37. package/template/src/pages/user/config/schema.ts +54 -54
  38. package/template/src/pages/user/index.tsx +641 -641
  39. package/template/src/shared/components/CommentInput.tsx +243 -243
  40. package/template/src/shared/config/text.ts +27 -27
  41. package/template/src/shared/utils/format.ts +11 -11
  42. package/template/src/types/auth.ts +10 -10
  43. package/template/src/types/comment.ts +33 -33
  44. package/template/src/types/common.ts +19 -19
  45. package/template/src/types/dashboard.ts +53 -53
  46. package/template/src/types/index.ts +16 -16
  47. package/template/src/types/log.ts +21 -21
  48. package/template/src/types/post.ts +32 -32
  49. package/template/src/types/sample.ts +28 -28
  50. package/template/src/types/user.ts +51 -51
  51. package/template/src/vite-env.d.ts +10 -10
@@ -1,243 +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
- }
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
+ }
@@ -1,27 +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
- };
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
+ };
@@ -1,11 +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
- };
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
+ };
@@ -1,10 +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
- }
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
+ }
@@ -1,33 +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
- }
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
+ }