리팩토링 1단계: 공용 utils/FormField 추출
- utils/formatting.js 신설 (formatMeso, formatMesoKorean 통합) - components/common/FormField.jsx 신설 (label+hint+error 공용 래퍼 + formInputClass/formInputStyle 상수) - 중복 정의 제거: * BossForm, SymbolForm, AdminMenuForm의 Field 로컬 정의 삭제 * boss-crystal constants.js의 formatMeso → utils re-export * SymbolForm의 formatMesoKorean 로컬 정의 삭제 * 3개 폼의 inputCls/inputStyle 상수 삭제 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4da16abc10
commit
c6ac3366cc
6 changed files with 105 additions and 134 deletions
31
frontend/src/components/common/FormField.jsx
Normal file
31
frontend/src/components/common/FormField.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* 관리자 폼 공용 필드 래퍼
|
||||||
|
* <FormField label="제목" required hint="설명" error={errors.title}>
|
||||||
|
* <input ... />
|
||||||
|
* </FormField>
|
||||||
|
*/
|
||||||
|
export default function FormField({ label, hint, error, required, children }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<label className="text-sm font-medium" style={{ color: 'var(--text-emphasis)' }}>
|
||||||
|
{label} {required && <span style={{ color: 'var(--danger-text)' }}>*</span>}
|
||||||
|
</label>
|
||||||
|
{hint && <span className="text-xs" style={{ color: 'var(--text-dim)' }}>{hint}</span>}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
{error && <div className="text-[11px]" style={{ color: 'var(--danger-text)' }}>{error}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 폼 공용 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)',
|
||||||
|
}
|
||||||
|
|
@ -4,28 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { api } from '../../../api/client'
|
import { api } from '../../../api/client'
|
||||||
import ImagePicker from './components/ImagePicker'
|
import ImagePicker from './components/ImagePicker'
|
||||||
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
||||||
|
import FormField, { formInputClass, formInputStyle } from '../../../components/common/FormField'
|
||||||
function Field({ label, hint, error, required, children }) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-emphasis)' }}>
|
|
||||||
{label} {required && <span style={{ color: 'var(--danger-text)' }}>*</span>}
|
|
||||||
</label>
|
|
||||||
{hint && <span className="text-xs" style={{ color: 'var(--text-dim)' }}>{hint}</span>}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
{error && <div className="text-[11px]" style={{ color: 'var(--danger-text)' }}>{error}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdminMenuForm() {
|
export default function AdminMenuForm() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -175,29 +154,29 @@ export default function AdminMenuForm() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field label="제목" required error={errors.title}>
|
<FormField label="제목" required error={errors.title}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={form.title}
|
value={form.title}
|
||||||
onChange={(e) => update({ title: e.target.value })}
|
onChange={(e) => update({ title: e.target.value })}
|
||||||
placeholder="예: 주간 보스 수익 계산기"
|
placeholder="예: 주간 보스 수익 계산기"
|
||||||
className={inputCls}
|
className={formInputClass}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
|
|
||||||
<Field label="설명" hint="카드에 표시되는 부가 설명">
|
<FormField label="설명" hint="카드에 표시되는 부가 설명">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={form.description}
|
value={form.description}
|
||||||
onChange={(e) => update({ description: e.target.value })}
|
onChange={(e) => update({ description: e.target.value })}
|
||||||
placeholder="예: 캐릭터별 보스 결정석 수익을 계산합니다"
|
placeholder="예: 캐릭터별 보스 결정석 수익을 계산합니다"
|
||||||
className={inputCls}
|
className={formInputClass}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
|
|
||||||
<Field label="경로" required error={errors.slug}>
|
<FormField label="경로" required error={errors.slug}>
|
||||||
<div
|
<div
|
||||||
className="flex items-stretch rounded-lg border focus-within:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]"
|
className="flex items-stretch rounded-lg border focus-within:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -234,9 +213,9 @@ export default function AdminMenuForm() {
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</FormField>
|
||||||
|
|
||||||
<Field label="아이콘 이미지" hint="선택사항">
|
<FormField label="아이콘 이미지" hint="선택사항">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -271,7 +250,7 @@ export default function AdminMenuForm() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 pt-2">
|
<div className="flex items-center gap-2 pt-2">
|
||||||
{isEdit && (
|
{isEdit && (
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,12 @@ import { api } from '../../../../api/client'
|
||||||
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
|
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
|
||||||
import Checkbox from '../../../../components/common/Checkbox'
|
import Checkbox from '../../../../components/common/Checkbox'
|
||||||
import Select from '../../../../components/common/Select'
|
import Select from '../../../../components/common/Select'
|
||||||
|
import FormField, { formInputClass, formInputStyle } from '../../../../components/common/FormField'
|
||||||
import { useAuthStore } from '../../../../stores/auth'
|
import { useAuthStore } from '../../../../stores/auth'
|
||||||
import { DIFFICULTIES, formatMeso, getDifficultyImageUrl } from './constants'
|
import { DIFFICULTIES, formatMeso, getDifficultyImageUrl } from './constants'
|
||||||
|
|
||||||
const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` }))
|
const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` }))
|
||||||
|
|
||||||
function Field({ label, hint, error, required, children }) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-emphasis)' }}>
|
|
||||||
{label} {required && <span style={{ color: 'var(--danger-text)' }}>*</span>}
|
|
||||||
</label>
|
|
||||||
{hint && <span className="text-xs" style={{ color: 'var(--text-dim)' }}>{hint}</span>}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
{error && <div className="text-[11px]" style={{ color: 'var(--danger-text)' }}>{error}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
function emptyDifficultyState() {
|
||||||
const obj = {}
|
const obj = {}
|
||||||
DIFFICULTIES.forEach((d) => {
|
DIFFICULTIES.forEach((d) => {
|
||||||
|
|
@ -197,28 +176,28 @@ export default function BossForm() {
|
||||||
>
|
>
|
||||||
{/* 이름 + 최대 인원 */}
|
{/* 이름 + 최대 인원 */}
|
||||||
<div className="grid grid-cols-[1fr_auto] gap-3">
|
<div className="grid grid-cols-[1fr_auto] gap-3">
|
||||||
<Field label="보스 이름" required error={errors.name}>
|
<FormField label="보스 이름" required error={errors.name}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="예: 검은 마법사"
|
placeholder="예: 검은 마법사"
|
||||||
className={inputCls}
|
className={formInputClass}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
<Field label="최대 인원">
|
<FormField label="최대 인원">
|
||||||
<Select
|
<Select
|
||||||
value={maxPartySize}
|
value={maxPartySize}
|
||||||
onChange={setMaxPartySize}
|
onChange={setMaxPartySize}
|
||||||
options={PARTY_OPTIONS}
|
options={PARTY_OPTIONS}
|
||||||
className="w-24"
|
className="w-24"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 이미지 */}
|
{/* 이미지 */}
|
||||||
<Field label="보스 이미지" required={!isEdit} error={errors.image}>
|
<FormField label="보스 이미지" required={!isEdit} error={errors.image}>
|
||||||
<label
|
<label
|
||||||
className="flex items-center gap-4 rounded-xl border-2 border-dashed p-4 cursor-pointer hover:border-[var(--selected-border)]"
|
className="flex items-center gap-4 rounded-xl border-2 border-dashed p-4 cursor-pointer hover:border-[var(--selected-border)]"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -256,10 +235,10 @@ export default function BossForm() {
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</FormField>
|
||||||
|
|
||||||
{/* 난이도 */}
|
{/* 난이도 */}
|
||||||
<Field label="난이도별 결정 정보" required error={errors.difficulties} hint="활성화한 난이도만 저장됩니다">
|
<FormField label="난이도별 결정 정보" required error={errors.difficulties} hint="활성화한 난이도만 저장됩니다">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{DIFFICULTIES.map((d) => {
|
{DIFFICULTIES.map((d) => {
|
||||||
const v = difficulties[d.key]
|
const v = difficulties[d.key]
|
||||||
|
|
@ -326,7 +305,7 @@ export default function BossForm() {
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 pt-2">
|
<div className="flex items-center gap-2 pt-2">
|
||||||
{isEdit && (
|
{isEdit && (
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,8 @@ export function getDifficultyBadgeStyle(key) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMeso(n) {
|
// formatMeso는 utils/formatting 에서 재-export (모든 기능 공통)
|
||||||
if (!n || n < 10000) return (n || 0).toLocaleString()
|
export { formatMeso } from '../../../../utils/formatting'
|
||||||
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()}만`
|
|
||||||
}
|
|
||||||
|
|
||||||
// difficulty 이미지 URL (S3)
|
// difficulty 이미지 URL (S3)
|
||||||
export const DIFFICULTY_IMAGE_BASE = 'https://s3.caadiq.co.kr/maplestory/crystal/difficulty'
|
export const DIFFICULTY_IMAGE_BASE = 'https://s3.caadiq.co.kr/maplestory/crystal/difficulty'
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { api } from '../../../../api/client'
|
import { api } from '../../../../api/client'
|
||||||
import Select from '../../../../components/common/Select'
|
import Select from '../../../../components/common/Select'
|
||||||
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
|
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
|
||||||
|
import FormField, { formInputClass, formInputStyle } from '../../../../components/common/FormField'
|
||||||
import { useAuthStore } from '../../../../stores/auth'
|
import { useAuthStore } from '../../../../stores/auth'
|
||||||
|
import { formatMeso } from '../../../../utils/formatting'
|
||||||
|
|
||||||
const TYPE_OPTIONS = [
|
const TYPE_OPTIONS = [
|
||||||
{ value: '아케인', label: '아케인' },
|
{ value: '아케인', label: '아케인' },
|
||||||
|
|
@ -12,27 +14,9 @@ const TYPE_OPTIONS = [
|
||||||
{ value: '그랜드 어센틱', label: '그랜드 어센틱' },
|
{ 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 }) {
|
function MesoInput({ value, onChange, ...rest }) {
|
||||||
const display = value === '' || value == null ? '' : Number(String(value).replace(/[^\d]/g, '')).toLocaleString()
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
|
|
@ -43,35 +27,20 @@ function MesoInput({ value, onChange, ...rest }) {
|
||||||
const digits = e.target.value.replace(/[^\d]/g, '')
|
const digits = e.target.value.replace(/[^\d]/g, '')
|
||||||
onChange(digits)
|
onChange(digits)
|
||||||
}}
|
}}
|
||||||
className={`${inputCls} tabular-nums text-right`}
|
className={`${formInputClass} tabular-nums text-right`}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="text-sm mt-1 text-right tabular-nums min-h-[18px]"
|
className="text-sm mt-1 text-right tabular-nums min-h-[18px]"
|
||||||
style={{ color: 'var(--warning-text-bright)' }}
|
style={{ color: 'var(--warning-text-bright)' }}
|
||||||
>
|
>
|
||||||
{korean || '\u00A0'}
|
{korean === '0' ? '\u00A0' : korean}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Field({ label, hint, error, required, children }) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<label className="text-sm font-medium" style={{ color: 'var(--text-emphasis)' }}>
|
|
||||||
{label} {required && <span style={{ color: 'var(--danger-text)' }}>*</span>}
|
|
||||||
</label>
|
|
||||||
{hint && <span className="text-xs" style={{ color: 'var(--text-dim)' }}>{hint}</span>}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
{error && <div className="text-[11px]" style={{ color: 'var(--danger-text)' }}>{error}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SymbolForm() {
|
export default function SymbolForm() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
@ -216,7 +185,7 @@ export default function SymbolForm() {
|
||||||
<div className="rounded-2xl border p-6 space-y-5" style={panelStyle}>
|
<div className="rounded-2xl border p-6 space-y-5" style={panelStyle}>
|
||||||
<div className="text-sm font-semibold" style={{ color: 'var(--accent-bright)' }}>기본 정보</div>
|
<div className="text-sm font-semibold" style={{ color: 'var(--accent-bright)' }}>기본 정보</div>
|
||||||
|
|
||||||
<Field label="심볼 이미지" required={!isEdit}>
|
<FormField label="심볼 이미지" required={!isEdit}>
|
||||||
<label
|
<label
|
||||||
className="flex items-center gap-4 rounded-xl border-2 border-dashed p-4 cursor-pointer hover:border-[var(--selected-border)]"
|
className="flex items-center gap-4 rounded-xl border-2 border-dashed p-4 cursor-pointer hover:border-[var(--selected-border)]"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -248,54 +217,54 @@ export default function SymbolForm() {
|
||||||
</div>
|
</div>
|
||||||
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleFile} className="hidden" />
|
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleFile} className="hidden" />
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</FormField>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Field label="심볼 종류" required>
|
<FormField label="심볼 종류" required>
|
||||||
<Select value={type} onChange={setType} options={TYPE_OPTIONS} />
|
<Select value={type} onChange={setType} options={TYPE_OPTIONS} />
|
||||||
</Field>
|
</FormField>
|
||||||
<Field label="지역 이름" required hint="예: 소멸의 여로">
|
<FormField label="지역 이름" required hint="예: 소멸의 여로">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={region}
|
value={region}
|
||||||
onChange={(e) => setRegion(e.target.value)}
|
onChange={(e) => setRegion(e.target.value)}
|
||||||
className={inputCls}
|
className={formInputClass}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
placeholder="소멸의 여로"
|
placeholder="소멸의 여로"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<Field label="만렙" required>
|
<FormField label="만렙" required>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={maxLevel}
|
value={maxLevel}
|
||||||
onChange={(e) => { setMaxLevel(e.target.value); adjustLevelRows(e.target.value) }}
|
onChange={(e) => { setMaxLevel(e.target.value); adjustLevelRows(e.target.value) }}
|
||||||
className={inputCls}
|
className={formInputClass}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
min="2"
|
min="2"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
<Field label="기본 일퀘 획득량">
|
<FormField label="기본 일퀘 획득량">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={dailyDefault}
|
value={dailyDefault}
|
||||||
onChange={(e) => setDailyDefault(e.target.value)}
|
onChange={(e) => setDailyDefault(e.target.value)}
|
||||||
className={inputCls}
|
className={formInputClass}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
<Field label="기본 주간퀘 획득량">
|
<FormField label="기본 주간퀘 획득량">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={weeklyDefault}
|
value={weeklyDefault}
|
||||||
onChange={(e) => setWeeklyDefault(e.target.value)}
|
onChange={(e) => setWeeklyDefault(e.target.value)}
|
||||||
className={inputCls}
|
className={formInputClass}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -329,8 +298,8 @@ export default function SymbolForm() {
|
||||||
type="number"
|
type="number"
|
||||||
value={l.required_count}
|
value={l.required_count}
|
||||||
onChange={(e) => updateLevel(idx, 'required_count', e.target.value)}
|
onChange={(e) => updateLevel(idx, 'required_count', e.target.value)}
|
||||||
className={`${inputCls} max-w-36`}
|
className={`${formInputClass} max-w-36`}
|
||||||
style={inputStyle}
|
style={formInputStyle}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
20
frontend/src/utils/formatting.js
Normal file
20
frontend/src/utils/formatting.js
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Reference in a new issue