feat(saju): useSajuForm + SajuInputForm + ActionCard
This commit is contained in:
28
src/pages/saju/components/ActionCard.jsx
Normal file
28
src/pages/saju/components/ActionCard.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const ICON = {
|
||||||
|
today: '☀',
|
||||||
|
heart: '♥',
|
||||||
|
book: '📖',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ActionCard({ to, icon, title, desc, variant = 'saju', disabled = false }) {
|
||||||
|
const cls = `saju-action-card saju-action-card--${variant}`;
|
||||||
|
if (disabled) {
|
||||||
|
return (
|
||||||
|
<span className={cls} aria-disabled="true">
|
||||||
|
<span className="saju-action-card__icon">{ICON[icon] || '✦'}</span>
|
||||||
|
<span className="saju-action-card__title">{title}</span>
|
||||||
|
<span className="saju-action-card__desc">{desc || '준비 중'}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link to={to} className={cls}>
|
||||||
|
<span className="saju-action-card__icon">{ICON[icon] || '✦'}</span>
|
||||||
|
<span className="saju-action-card__title">{title}</span>
|
||||||
|
<span className="saju-action-card__desc">{desc}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/pages/saju/components/SajuInputForm.jsx
Normal file
42
src/pages/saju/components/SajuInputForm.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function SajuInputForm({ form, onChange, onSubmit, loading, error }) {
|
||||||
|
return (
|
||||||
|
<form className="saju-form" onSubmit={onSubmit}>
|
||||||
|
<h3 className="saju-h3" style={{ color: 'var(--saju-cream)', marginBottom: '0.5rem' }}>
|
||||||
|
사주풀이 시작하기
|
||||||
|
</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="이름 (선택)"
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => onChange('name', e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.5rem' }}>
|
||||||
|
<input type="number" placeholder="년 (1900-2100)" value={form.year}
|
||||||
|
onChange={(e) => onChange('year', e.target.value)} disabled={loading} min="1900" max="2100" />
|
||||||
|
<input type="number" placeholder="월" value={form.month}
|
||||||
|
onChange={(e) => onChange('month', e.target.value)} disabled={loading} min="1" max="12" />
|
||||||
|
<input type="number" placeholder="일" value={form.day}
|
||||||
|
onChange={(e) => onChange('day', e.target.value)} disabled={loading} min="1" max="31" />
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.5rem' }}>
|
||||||
|
<input type="number" placeholder="시 (선택, 0-23)" value={form.hour}
|
||||||
|
onChange={(e) => onChange('hour', e.target.value)} disabled={loading} min="0" max="23" />
|
||||||
|
<select value={form.gender} onChange={(e) => onChange('gender', e.target.value)} disabled={loading}>
|
||||||
|
<option value="male">남</option>
|
||||||
|
<option value="female">여</option>
|
||||||
|
</select>
|
||||||
|
<select value={form.calendar_type} onChange={(e) => onChange('calendar_type', e.target.value)} disabled={loading}>
|
||||||
|
<option value="solar">양력</option>
|
||||||
|
<option value="lunar">음력</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{error && <div className="saju-form__error">{error}</div>}
|
||||||
|
<button type="submit" disabled={loading}>
|
||||||
|
{loading ? '호령이 풀어보는 중...' : '사주풀이 시작하기 ✦'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/pages/saju/hooks/useSajuForm.js
Normal file
64
src/pages/saju/hooks/useSajuForm.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { sajuInterpret } from '../../../api';
|
||||||
|
|
||||||
|
const INITIAL_FORM = {
|
||||||
|
name: '',
|
||||||
|
year: '',
|
||||||
|
month: '',
|
||||||
|
day: '',
|
||||||
|
hour: '',
|
||||||
|
gender: 'male',
|
||||||
|
calendar_type: 'solar',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useSajuForm() {
|
||||||
|
const [form, setForm] = useState(INITIAL_FORM);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleChange = useCallback((field, value) => {
|
||||||
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async (e) => {
|
||||||
|
if (e?.preventDefault) e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (!form.year || !form.month || !form.day) {
|
||||||
|
setError('생년월일을 모두 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const year = parseInt(form.year, 10);
|
||||||
|
const month = parseInt(form.month, 10);
|
||||||
|
const day = parseInt(form.day, 10);
|
||||||
|
if (year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1 || day > 31) {
|
||||||
|
setError('올바른 생년월일을 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const body = {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
gender: form.gender,
|
||||||
|
calendar_type: form.calendar_type,
|
||||||
|
};
|
||||||
|
if (form.hour !== '') {
|
||||||
|
body.hour = parseInt(form.hour, 10);
|
||||||
|
}
|
||||||
|
const result = await sajuInterpret(body);
|
||||||
|
navigate(`/saju/result?rid=${result.reading_id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('사주 분석 실패', err);
|
||||||
|
setError(err.message || '잠시 후 다시 시도해주세요.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [form, navigate]);
|
||||||
|
|
||||||
|
return { form, handleChange, handleSubmit, loading, error };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user