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 ( +
+
+ + {hint && {hint}} +
+ {children} + {error &&
{error}
} +
+ ) +} + +/** + * 관리자 폼 공용 input 스타일 + */ +export const formInputClass = '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)]' + +export const formInputStyle = { + background: 'var(--input-bg)', + borderColor: 'var(--input-border)', + color: 'var(--text-strong)', +} diff --git a/frontend/src/features/admin/pc/AdminMenuForm.jsx b/frontend/src/features/admin/pc/AdminMenuForm.jsx index 9502d7e..fa07f76 100644 --- a/frontend/src/features/admin/pc/AdminMenuForm.jsx +++ b/frontend/src/features/admin/pc/AdminMenuForm.jsx @@ -4,28 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '../../../api/client' import ImagePicker from './components/ImagePicker' import ConfirmDialog from '../../../components/common/ConfirmDialog' - -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)', -} +import FormField, { formInputClass, formInputStyle } from '../../../components/common/FormField' export default function AdminMenuForm() { const navigate = useNavigate() @@ -175,29 +154,29 @@ export default function AdminMenuForm() { - + update({ title: e.target.value })} placeholder="예: 주간 보스 수익 계산기" - className={inputCls} - style={inputStyle} + className={formInputClass} + style={formInputStyle} /> - + - + update({ description: e.target.value })} placeholder="예: 캐릭터별 보스 결정석 수익을 계산합니다" - className={inputCls} - style={inputStyle} + className={formInputClass} + style={formInputStyle} /> - + - +
)} -
+ - +
-
+
{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} /> - - + +
- {korean || '\u00A0'} + {korean === '0' ? '\u00A0' : korean}
) } -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() {
기본 정보
- + - +
- + setRegion(e.target.value)} - className={inputCls} - style={inputStyle} + className={formInputClass} + style={formInputStyle} placeholder="소멸의 여로" /> - +
- + { setMaxLevel(e.target.value); adjustLevelRows(e.target.value) }} - className={inputCls} - style={inputStyle} + className={formInputClass} + style={formInputStyle} min="2" /> - - + + setDailyDefault(e.target.value)} - className={inputCls} - style={inputStyle} + className={formInputClass} + style={formInputStyle} /> - - + + setWeeklyDefault(e.target.value)} - className={inputCls} - style={inputStyle} + className={formInputClass} + style={formInputStyle} /> - +
@@ -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