vignette/apps/web/src/components/ui/Field.tsx
Yun Chan 24b1b7a6e1 feat: P1 풀빌드 — React 프론트 7화면 + 백엔드 상담루프·평가·음성·RAG
web (Vite+React19+TS, Cloudflare Pages 배포):
- 디자인토큰(세이지틸/테라코타 SSOT), 앱셸, 공통 UI 프리미티브
- 7화면: 로그인/학습자홈/상담세션/회기리뷰/교수자/관리자/설정
- ClientAvatar: SVG 반구상 흉상 4상태 + RMS 립싱크 + 6파라미터 정서
- 회기리뷰는 외부 레퍼런스 디자인을 Vignette 토큰으로 리스킨

api (FastAPI):
- 게이트웨이 /v1/generate·/v1/stream 어댑터(상주풀/EngineSession 보존)
- services: 페르소나 L0~L6 빌더 / 결정론 상태머신 / 가드레일 /
  턴 오케스트레이터 / 회기간 메모리 / 평가AI / 음성 / RAG
- store: DB off 폴백(in-memory), sessions 실구현

검증:
- web: node22 tsc+vite build 통과(node23 segfault 회피), Pages 배포 200
- api: app.main import 통과
- 핫픽스: Topbar initials undefined-safe (undefined.trim 크래시)
- E2E: 서연(P1) 상담 1턴 — 좋은/나쁜 상담에 차등 반응 실증
2026-06-25 23:37:22 +09:00

57 lines
1.6 KiB
TypeScript

import { useId } from "react";
import type { InputHTMLAttributes, ReactNode } from "react";
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
invalid?: boolean;
}
/** Input — radius 6px, 포커스 보더색 + ring. 좌측바 금지. §7.2 */
export function Input({ invalid, className, ...rest }: InputProps) {
const cls = ["vg-input", className ?? ""].filter(Boolean).join(" ");
return (
<input
className={cls}
aria-invalid={invalid ? "true" : undefined}
{...rest}
/>
);
}
export interface FieldProps {
/** 라벨 텍스트 */
label?: ReactNode;
/** 보조 안내 */
hint?: ReactNode;
/** 에러 메시지 (있으면 input invalid) */
error?: ReactNode;
/** input 에 연결할 자식. id 는 자동 연결되지 않으므로 필요 시 htmlFor 직접 사용 */
children: ReactNode;
/** label htmlFor 연결용 (없으면 useId) */
htmlFor?: string;
className?: string;
}
/**
* Field — 라벨 + 입력 + 힌트/에러 묶음.
* children 으로 <Input/> 등을 받는다(제어는 호출부).
*/
export function Field({ label, hint, error, children, htmlFor, className }: FieldProps) {
const autoId = useId();
const fieldId = htmlFor ?? autoId;
const cls = ["vg-field", className ?? ""].filter(Boolean).join(" ");
return (
<div className={cls}>
{label ? (
<label className="vg-field__label" htmlFor={fieldId}>
{label}
</label>
) : null}
{children}
{error ? (
<span className="vg-field__error">{error}</span>
) : hint ? (
<span className="vg-field__hint">{hint}</span>
) : null}
</div>
);
}