임동욱 임동욱 08-10
feat: 폼 입력 공용 컴포넌트 추가
Co-Authored-By: Claude Opus 5 
@2f39258d763ce2ca30dc77170da006d25830020f
 
components/ui/button.tsx (added)
+++ components/ui/button.tsx
@@ -0,0 +1,44 @@
+import type { ComponentProps } from "react";
+
+type ButtonVariant = "primary" | "secondary" | "success" | "ghost";
+type ButtonSize = "sm" | "md";
+
+const VARIANT_CLASSES: Record<ButtonVariant, string> = {
+  primary: "bg-primary text-on-primary hover:opacity-90",
+  secondary: "border border-border bg-surface text-foreground hover:bg-surface-muted",
+  // on-success 토큰이 아직 없어(§10.4 대상) primary 필렁 버튼과 동일하게 on-primary를
+  // 재사용한다 — 강조색(엑셀 다운로드 등) 액션용.
+  success: "bg-success text-on-primary hover:opacity-90",
+  ghost: "text-foreground-muted hover:bg-surface-muted hover:text-foreground",
+};
+
+const SIZE_CLASSES: Record<ButtonSize, string> = {
+  sm: "h-8 px-3 text-body-sm",
+  md: "h-10 px-4 text-body-md",
+};
+
+interface ButtonProps extends ComponentProps<"button"> {
+  variant?: ButtonVariant;
+  size?: ButtonSize;
+}
+
+/**
+ * 공용 버튼. 상태를 가지지 않는 프레젠테이션 컴포넌트 — 클릭 동작·로딩 처리는
+ * 호출부(feature) 책임이다. `type` 기본값은 `button`(폼 안에서 의도치 않은
+ * submit 방지) — 제출 버튼은 명시적으로 `type="submit"`을 전달한다.
+ */
+export function Button({
+  variant = "primary",
+  size = "md",
+  type = "button",
+  className,
+  ...props
+}: ButtonProps) {
+  return (
+    <button
+      type={type}
+      className={`inline-flex items-center justify-center gap-2 rounded-md transition-colors duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:pointer-events-none disabled:opacity-50 ${VARIANT_CLASSES[variant]} ${SIZE_CLASSES[size]} ${className ?? ""}`}
+      {...props}
+    />
+  );
+}
 
components/ui/input.tsx (added)
+++ components/ui/input.tsx
@@ -0,0 +1,16 @@
+import type { ComponentProps } from "react";
+
+type InputProps = ComponentProps<"input">;
+
+/** 기본 텍스트 입력. `readOnly`일 때 배경 톤 + 기본 커서로 편집 불가 상태를 시각 구분한다. */
+export function Input({ className, readOnly, ...props }: InputProps) {
+  return (
+    <input
+      readOnly={readOnly}
+      className={`h-10 w-full rounded-md border border-border px-3 text-body-md text-foreground placeholder:text-foreground-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:pointer-events-none disabled:opacity-50 ${
+        readOnly ? "cursor-default bg-surface-muted" : "bg-surface"
+      } ${className ?? ""}`}
+      {...props}
+    />
+  );
+}
 
components/ui/select.tsx (added)
+++ components/ui/select.tsx
@@ -0,0 +1,13 @@
+import type { ComponentProps } from "react";
+
+type SelectProps = ComponentProps<"select">;
+
+/** 네이티브 `<select>` 래퍼(라이브러리 미사용) — 브라우저 기본 표시(화살표 등)를 그대로 쓴다. */
+export function Select({ className, ...props }: SelectProps) {
+  return (
+    <select
+      className={`h-10 rounded-md border border-border bg-surface px-3 text-body-md text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:pointer-events-none disabled:opacity-50 ${className ?? ""}`}
+      {...props}
+    />
+  );
+}
Add a comment
List