diff --git a/frontend/src/components/common/FormField.jsx b/frontend/src/components/common/FormField.jsx
new file mode 100644
index 0000000..62d2193
--- /dev/null
+++ b/frontend/src/components/common/FormField.jsx
@@ -0,0 +1,31 @@
+/**
+ * 관리자 폼 공용 필드 래퍼
+ *
+ *
+ *
+ */
+export default function FormField({ label, hint, error, required, children }) {
+ return (
+
{isEdit && (
diff --git a/frontend/src/features/boss-crystal/pc/admin/BossForm.jsx b/frontend/src/features/boss-crystal/pc/admin/BossForm.jsx
index 13b3574..798e303 100644
--- a/frontend/src/features/boss-crystal/pc/admin/BossForm.jsx
+++ b/frontend/src/features/boss-crystal/pc/admin/BossForm.jsx
@@ -5,33 +5,12 @@ import { api } from '../../../../api/client'
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
import Checkbox from '../../../../components/common/Checkbox'
import Select from '../../../../components/common/Select'
+import FormField, { formInputClass, formInputStyle } from '../../../../components/common/FormField'
import { useAuthStore } from '../../../../stores/auth'
import { DIFFICULTIES, formatMeso, getDifficultyImageUrl } from './constants'
const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` }))
-function Field({ label, hint, error, required, children }) {
- return (
-
-
-
- {hint && {hint}}
-
- {children}
- {error &&
{error}
}
-
- )
-}
-
-const inputCls = 'w-full rounded-lg border px-3 py-2 text-sm outline-none focus:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]'
-const inputStyle = {
- background: 'var(--input-bg)',
- borderColor: 'var(--input-border)',
- color: 'var(--text-strong)',
-}
-
function emptyDifficultyState() {
const obj = {}
DIFFICULTIES.forEach((d) => {
@@ -197,28 +176,28 @@ export default function BossForm() {
>
{/* 이름 + 최대 인원 */}
-
+
setName(e.target.value)}
placeholder="예: 검은 마법사"
- className={inputCls}
- style={inputStyle}
+ className={formInputClass}
+ style={formInputStyle}
/>
-
-
+
+
-
+
{/* 이미지 */}
-
+
-
+
{/* 난이도 */}
-
+
{DIFFICULTIES.map((d) => {
const v = difficulties[d.key]
@@ -326,7 +305,7 @@ export default function BossForm() {
)
})}
-
+
{isEdit && (
diff --git a/frontend/src/features/boss-crystal/pc/admin/constants.js b/frontend/src/features/boss-crystal/pc/admin/constants.js
index 2117c8b..a65519d 100644
--- a/frontend/src/features/boss-crystal/pc/admin/constants.js
+++ b/frontend/src/features/boss-crystal/pc/admin/constants.js
@@ -32,15 +32,8 @@ export function getDifficultyBadgeStyle(key) {
}
}
-export function formatMeso(n) {
- if (!n || n < 10000) return (n || 0).toLocaleString()
- if (n >= 100_000_000) {
- const uk = Math.floor(n / 100_000_000)
- const man = Math.floor((n % 100_000_000) / 10_000)
- return man > 0 ? `${uk}억 ${man.toLocaleString()}만` : `${uk}억`
- }
- return `${Math.floor(n / 10_000).toLocaleString()}만`
-}
+// formatMeso는 utils/formatting 에서 재-export (모든 기능 공통)
+export { formatMeso } from '../../../../utils/formatting'
// difficulty 이미지 URL (S3)
export const DIFFICULTY_IMAGE_BASE = 'https://s3.caadiq.co.kr/maplestory/crystal/difficulty'
diff --git a/frontend/src/features/symbol/pc/admin/SymbolForm.jsx b/frontend/src/features/symbol/pc/admin/SymbolForm.jsx
index 3bde41d..cc82319 100644
--- a/frontend/src/features/symbol/pc/admin/SymbolForm.jsx
+++ b/frontend/src/features/symbol/pc/admin/SymbolForm.jsx
@@ -4,7 +4,9 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../../../../api/client'
import Select from '../../../../components/common/Select'
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
+import FormField, { formInputClass, formInputStyle } from '../../../../components/common/FormField'
import { useAuthStore } from '../../../../stores/auth'
+import { formatMeso } from '../../../../utils/formatting'
const TYPE_OPTIONS = [
{ value: '아케인', label: '아케인' },
@@ -12,27 +14,9 @@ const TYPE_OPTIONS = [
{ value: '그랜드 어센틱', label: '그랜드 어센틱' },
]
-const inputCls = 'w-full rounded-lg border px-3 py-2 text-sm outline-none focus:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]'
-const inputStyle = {
- background: 'var(--input-bg)',
- borderColor: 'var(--input-border)',
- color: 'var(--text-strong)',
-}
-
-function formatMesoKorean(n) {
- if (!n || n <= 0) return ''
- const eok = Math.floor(n / 100_000_000)
- const man = Math.floor((n % 100_000_000) / 10_000)
- const parts = []
- if (eok) parts.push(`${eok}억`)
- if (man) parts.push(`${man.toLocaleString()}만`)
- if (!parts.length) return `${n.toLocaleString()}`
- return parts.join(' ')
-}
-
function MesoInput({ value, onChange, ...rest }) {
const display = value === '' || value == null ? '' : Number(String(value).replace(/[^\d]/g, '')).toLocaleString()
- const korean = formatMesoKorean(Number(String(value).replace(/[^\d]/g, '')) || 0)
+ const korean = formatMeso(Number(String(value).replace(/[^\d]/g, '')) || 0)
return (
)
}
-function Field({ label, hint, error, required, children }) {
- return (
-
-
-
- {hint && {hint}}
-
- {children}
- {error &&
{error}
}
-
- )
-}
-
export default function SymbolForm() {
const navigate = useNavigate()
const queryClient = useQueryClient()
@@ -216,7 +185,7 @@ export default function SymbolForm() {
@@ -329,8 +298,8 @@ export default function SymbolForm() {
type="number"
value={l.required_count}
onChange={(e) => updateLevel(idx, 'required_count', e.target.value)}
- className={`${inputCls} max-w-36`}
- style={inputStyle}
+ className={`${formInputClass} max-w-36`}
+ style={formInputStyle}
placeholder="0"
/>
diff --git a/frontend/src/utils/formatting.js b/frontend/src/utils/formatting.js
new file mode 100644
index 0000000..fb055c2
--- /dev/null
+++ b/frontend/src/utils/formatting.js
@@ -0,0 +1,20 @@
+/**
+ * 메소를 "N억 N,NNN만" 형식의 한국어 문자열로 반환
+ * formatMeso(123456789) → "1억 2,345만"
+ * formatMeso(10000) → "1만"
+ * formatMeso(500) → "500"
+ * formatMeso(0) → "0"
+ */
+export function formatMeso(n) {
+ const v = Number(n) || 0
+ if (v <= 0) return '0'
+ const eok = Math.floor(v / 100_000_000)
+ const man = Math.floor((v % 100_000_000) / 10_000)
+ const parts = []
+ if (eok) parts.push(`${eok.toLocaleString()}억`)
+ if (man) parts.push(`${man.toLocaleString()}만`)
+ return parts.length ? parts.join(' ') : v.toLocaleString()
+}
+
+// 과거 이름 alias (symbol에서 formatMesoKorean으로 쓰던 것)
+export const formatMesoKorean = formatMeso