← back to kjm99d__MonkeyPlanner

Function bodies 254 total

All specs Real LLM only Function bodies
MultiSelectField function · typescript · L138-L165 (28 LOC)
frontend/src/components/PropertyEditor.tsx
function MultiSelectField({ options, value, onChange }: { options: string[]; value: string[]; onChange: (v: unknown) => void }) {
  const toggle = (opt: string) => {
    const next = value.includes(opt) ? value.filter((v) => v !== opt) : [...value, opt];
    onChange(next.length > 0 ? next : null);
  };

  return (
    <div className="flex flex-wrap gap-1.5">
      {options.map((opt) => {
        const active = value.includes(opt);
        return (
          <button
            key={opt}
            type="button"
            onClick={() => toggle(opt)}
            className={`rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors ${
              active
                ? 'border-brand-500 bg-brand-500/15 text-brand-500'
                : 'border-edge-base text-ink-muted hover:border-brand-500/30 hover:text-ink-secondary'
            }`}
          >
            {opt}
          </button>
        );
      })}
    </div>
  );
}
AddPropertyForm function · typescript · L172-L254 (83 LOC)
frontend/src/components/PropertyEditor.tsx
export function AddPropertyForm({ onAdd }: AddPropertyProps) {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const [type, setType] = useState<PropertyType>('text');
  const [optStr, setOptStr] = useState('');
  const [nameError, setNameError] = useState(false);

  const typeLabels: Record<PropertyType, string> = {
    text: t('property.text'),
    number: t('property.number'),
    select: t('property.select'),
    multi_select: t('property.multi_select'),
    date: t('property.date'),
    checkbox: t('property.checkbox'),
  };

  const submit = () => {
    if (!name.trim()) {
      setNameError(true);
      return;
    }
    setNameError(false);
    const options = (type === 'select' || type === 'multi_select')
      ? optStr.split(',').map((s) => s.trim()).filter(Boolean)
      : [];
    onAdd(name.trim(), type, options);
    setName('');
    setType('text');
    setOptStr('');
    setOpen(false);
  };

  if (!op
SearchDialog function · typescript · L6-L160 (155 LOC)
frontend/src/components/SearchDialog.tsx
export function SearchDialog() {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState('');
  const [selectedIdx, setSelectedIdx] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);
  const navigate = useNavigate();
  const boards = useBoards();
  const issues = useIssues({});

  // Cmd+K / Ctrl+K to open
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        setOpen(prev => !prev);
      }
      if (e.key === 'Escape' && open) {
        setOpen(false);
      }
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [open]);

  // Auto-focus input when opened
  useEffect(() => {
    if (open) {
      setQuery('');
      setTimeout(() => inputRef.current?.focus(), 50);
    }
  }, [open]);

  // Filter results
  const results = useMemo(() => {
    if (!query.trim()) return { bo
go function · typescript · L57-L60 (4 LOC)
frontend/src/components/SearchDialog.tsx
  function go(path: string) {
    navigate(path);
    setOpen(false);
  }
ShortcutRow function · typescript · L16-L32 (17 LOC)
frontend/src/components/ShortcutsDialog.tsx
function ShortcutRow({ shortcut }: { shortcut: Shortcut }) {
  return (
    <div className="flex items-center justify-between py-2">
      <span className="text-sm text-ink-secondary">{shortcut.description}</span>
      <div className="flex items-center gap-1">
        {shortcut.keys.map((key, idx) => (
          <kbd
            key={idx}
            className="inline-flex items-center rounded border border-edge-base bg-surface-muted px-2 py-0.5 text-xs font-medium text-ink-secondary"
          >
            {key}
          </kbd>
        ))}
      </div>
    </div>
  );
}
ShortcutsDialog function · typescript · L34-L90 (57 LOC)
frontend/src/components/ShortcutsDialog.tsx
export function ShortcutsDialog() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      const target = e.target as HTMLElement;
      const isInputFocused =
        target.tagName === 'INPUT' ||
        target.tagName === 'TEXTAREA' ||
        target.isContentEditable;

      if (e.key === '?' && !isInputFocused) {
        e.preventDefault();
        setOpen(prev => !prev);
      }
      if (e.key === 'Escape' && open) {
        setOpen(false);
      }
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [open]);

  if (!open) return null;

  return (
    <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh] p-4">
      <div className="absolute inset-0 bg-black/40" onClick={() => setOpen(false)} aria-hidden />
      <div
        role="dialog"
        aria-modal="true"
        aria-label="Keyboard shortcuts"
        className="
Skeleton function · typescript · L6-L18 (13 LOC)
frontend/src/components/Skeleton.tsx
export function Skeleton({ className = 'h-4 w-full', count = 1 }: Props) {
  return (
    <>
      {Array.from({ length: count }, (_, i) => (
        <div
          key={i}
          className={`animate-pulse rounded-md bg-surface-muted ${className}`}
          aria-hidden
        />
      ))}
    </>
  );
}
Repobility · severity-and-effort ranking · https://repobility.com
CardSkeleton function · typescript · L20-L27 (8 LOC)
frontend/src/components/Skeleton.tsx
export function CardSkeleton() {
  return (
    <div className="flex flex-col gap-3 rounded-lg border border-edge-base bg-surface-subtle p-4">
      <Skeleton className="h-5 w-3/4" />
      <Skeleton className="h-3 w-1/2" />
    </div>
  );
}
BoardSkeleton function · typescript · L29-L41 (13 LOC)
frontend/src/components/Skeleton.tsx
export function BoardSkeleton() {
  return (
    <div className="grid gap-4 lg:grid-cols-4 md:grid-cols-2">
      {[1, 2, 3, 4].map((i) => (
        <div key={i} className="flex min-h-[16rem] flex-col gap-3 rounded-lg border border-edge-base bg-surface-subtle p-3">
          <Skeleton className="h-4 w-20" />
          <CardSkeleton />
          <CardSkeleton />
        </div>
      ))}
    </div>
  );
}
StatusBadge function · typescript · L13-L26 (14 LOC)
frontend/src/components/StatusBadge.tsx
export function StatusBadge({ status }: { status: IssueStatus }) {
  const { t } = useTranslation();
  const label = t(`status.${status}`);
  return (
    <span
      className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium ${classes[status]}`}
      role="status"
      aria-label={label}
    >
      <span aria-hidden className="h-1.5 w-1.5 rounded-full bg-current" />
      {label}
    </span>
  );
}
StatusStepper function · typescript · L21-L88 (68 LOC)
frontend/src/components/StatusStepper.tsx
export function StatusStepper({ current, onSelect, disabled }: Props) {
  const { t } = useTranslation();
  const currentIdx = STEP_STATUSES.indexOf(current);

  return (
    <nav aria-label={t('stepper.moveTo', { label: '' }).trim()} className="flex items-center gap-1">
      {STEP_STATUSES.map((status, i) => {
        const label = t(`status.${status}`);
        const isActive = status === current;
        const isPast = i < currentIdx;
        const canClick =
          !disabled &&
          !isActive &&
          status !== 'Pending' &&
          status !== 'Approved' &&
          current !== 'Pending';

        const hint =
          status === 'Approved'
            ? t('stepper.approveOnly')
            : status === 'Pending'
              ? t('stepper.noPending')
              : t('stepper.moveTo', { label });
        const showHint = !canClick && !isActive;

        return (
          <div key={status} className="flex items-center">
            {i > 0 && (
              <div
getTemplates function · typescript · L21-L25 (5 LOC)
frontend/src/components/TemplateDialog.tsx
function getTemplates(boardId: string): Template[] {
  try {
    return JSON.parse(localStorage.getItem(`mp-templates-${boardId}`) ?? '[]');
  } catch { return []; }
}
saveTemplates function · typescript · L27-L29 (3 LOC)
frontend/src/components/TemplateDialog.tsx
function saveTemplates(boardId: string, templates: Template[]) {
  localStorage.setItem(`mp-templates-${boardId}`, JSON.stringify(templates));
}
TemplateDialog function · typescript · L31-L200 (170 LOC)
frontend/src/components/TemplateDialog.tsx
export function TemplateDialog({ boardId, open, onClose, onSelect }: Props) {
  const { t } = useTranslation();
  const [templates, setTemplates] = useState<Template[]>([]);
  const [showAdd, setShowAdd] = useState(false);
  const [newName, setNewName] = useState('');
  const [newTitle, setNewTitle] = useState('');
  const [newBody, setNewBody] = useState('');
  const [newInstructions, setNewInstructions] = useState('');
  const closeRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (!open) return;
    setTemplates(getTemplates(boardId));
    setShowAdd(false);
    setNewName('');
    setNewTitle('');
    setNewBody('');
    setNewInstructions('');
    closeRef.current?.focus();
    const handler = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [open, boardId, onClose]);

  if (!open) return null;

  function handleDelete(i
handleDelete function · typescript · L59-L63 (5 LOC)
frontend/src/components/TemplateDialog.tsx
  function handleDelete(id: string) {
    const updated = templates.filter((t) => t.id !== id);
    setTemplates(updated);
    saveTemplates(boardId, updated);
  }
Repobility · code-quality intelligence · https://repobility.com
handleAdd function · typescript · L65-L82 (18 LOC)
frontend/src/components/TemplateDialog.tsx
  function handleAdd() {
    if (!newName.trim()) return;
    const tmpl: Template = {
      id: crypto.randomUUID(),
      name: newName.trim(),
      title: newTitle.trim(),
      body: newBody.trim(),
      instructions: newInstructions.trim(),
    };
    const updated = [...templates, tmpl];
    setTemplates(updated);
    saveTemplates(boardId, updated);
    setShowAdd(false);
    setNewName('');
    setNewTitle('');
    setNewBody('');
    setNewInstructions('');
  }
ThemeToggle function · typescript · L5-L18 (14 LOC)
frontend/src/components/ThemeToggle.tsx
export function ThemeToggle() {
  const { mode, toggle } = useTheme();
  const { t } = useTranslation();
  return (
    <button
      type="button"
      onClick={toggle}
      aria-label={mode === 'dark' ? t('theme.toLight') : t('theme.toDark')}
      className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg border border-edge-base text-ink-secondary transition-all duration-200 hover:bg-surface-muted hover:text-brand-500 hover:border-brand-500/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 active:scale-95"
    >
      {mode === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
    </button>
  );
}
ToastProvider function · typescript · L16-L65 (50 LOC)
frontend/src/components/Toast.tsx
export function ToastProvider({ children }: { children: ReactNode }) {
  const [toasts, setToasts] = useState<ToastItem[]>([]);
  const { t } = useTranslation();

  const toast = useCallback((type: ToastType, message: string) => {
    const id = nextId++;
    const duration = type === 'error' ? 8000 : 4000;
    setToasts((prev) => [...prev, { id, type, message }]);
    setTimeout(() => setToasts((prev) => prev.filter((item) => item.id !== id)), duration);
  }, []);

  const dismiss = useCallback((id: number) => {
    setToasts((prev) => prev.filter((item) => item.id !== id));
  }, []);

  return (
    <Ctx.Provider value={{ toast }}>
      {children}
      <div
        aria-live="polite"
        className="fixed bottom-6 right-6 z-50 flex flex-col gap-2"
      >
        {toasts.map((item) => (
          <div
            key={item.id}
            className={`flex items-center gap-2 rounded-lg border px-4 py-3 shadow-lg backdrop-blur-sm transition-all duration-300 animate-in slide-in-fro
detectPlatform function · typescript · L27-L32 (6 LOC)
frontend/src/components/WebhookSettings.tsx
function detectPlatform(url: string): Platform {
  if (url.includes('discord.com')) return 'discord';
  if (url.includes('slack.com')) return 'slack';
  if (url.includes('telegram.org')) return 'telegram';
  return 'custom';
}
PlatformBadge function · typescript · L34-L42 (9 LOC)
frontend/src/components/WebhookSettings.tsx
function PlatformBadge({ url }: { url: string }) {
  const p = PLATFORMS.find((pl) => pl.value === detectPlatform(url))!;
  const Icon = p.icon;
  return (
    <span className={`flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium ${p.color}`}>
      <Icon size={12} /> {p.label}
    </span>
  );
}
WebhookSettings function · typescript · L44-L206 (163 LOC)
frontend/src/components/WebhookSettings.tsx
export function WebhookSettings({ boardId }: { boardId: string }) {
  const { t } = useTranslation();
  const { toast } = useToast();
  const webhooks = useWebhooks(boardId);
  const createWh = useCreateWebhook();
  const deleteWh = useDeleteWebhook();
  const [open, setOpen] = useState(false);
  const [confirmWh, setConfirmWh] = useState<string | null>(null);
  const [platform, setPlatform] = useState<Platform>('discord');
  const [name, setName] = useState('');
  const [url, setUrl] = useState('');
  const [events, setEvents] = useState<WebhookEvent[]>(['issue.approved']);

  const eventKeyMap: Record<WebhookEvent, string> = {
    'issue.created': 'webhook.events.issue_created',
    'issue.approved': 'webhook.events.issue_approved',
    'issue.status_changed': 'webhook.events.issue_status_changed',
    'issue.deleted': 'webhook.events.issue_deleted',
  };
  const ALL_EVENTS: { value: WebhookEvent; label: string }[] = ALL_EVENT_VALUES.map((v) => ({
    value: v,
    label: t(eventKeyM
relativeTime function · typescript · L10-L25 (16 LOC)
frontend/src/features/approval/ApprovalPage.tsx
function relativeTime(iso: string, locale: string): string {
  const diff = Date.now() - new Date(iso).getTime();
  const minutes = Math.floor(diff / 60_000);
  const hours = Math.floor(diff / 3_600_000);
  const days = Math.floor(diff / 86_400_000);
  try {
    const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
    if (days > 0) return rtf.format(-days, 'day');
    if (hours > 0) return rtf.format(-hours, 'hour');
    return rtf.format(-minutes, 'minute');
  } catch {
    if (days > 0) return `${days}d ago`;
    if (hours > 0) return `${hours}h ago`;
    return `${minutes}m ago`;
  }
}
ApprovalPage function · typescript · L27-L228 (202 LOC)
frontend/src/features/approval/ApprovalPage.tsx
export default function ApprovalPage() {
  const { t, i18n } = useTranslation();
  const issues = useIssues({ status: 'Pending' });
  const boards = useBoards();
  const approveIssue = useApproveIssue();

  const [selected, setSelected] = useState<Set<string>>(new Set());
  const [approving, setApproving] = useState<Set<string>>(new Set());

  const boardMap = useMemo(() => {
    const m = new Map<string, string>();
    boards.data?.forEach((b) => m.set(b.id, b.name));
    return m;
  }, [boards.data]);

  const pendingIssues = issues.data ?? [];

  const toggleSelect = (id: string) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const toggleAll = () => {
    if (selected.size === pendingIssues.length) {
      setSelected(new Set());
    } else {
      setSelected(new Set(pendingIssues.map((i) => i.id)));
    }
  };

  const handleApprove = async (id: string) => {
   
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
onCreate function · typescript · L106-L116 (11 LOC)
frontend/src/features/board/BoardPage.tsx
  async function onCreate(e: React.FormEvent) {
    e.preventDefault();
    if (!title.trim() || !boardId) return;
    try {
      await createIssue.mutateAsync({ boardId, title: title.trim() });
      setTitle('');
      toast('success', t('board.issueCreated'));
    } catch (err) {
      toast('error', (err as { message?: string }).message ?? t('board.issueCreateFailed'));
    }
  }
onDragStart function · typescript · L118-L121 (4 LOC)
frontend/src/features/board/BoardPage.tsx
  function onDragStart(e: DragStartEvent) {
    const issue = (issues.data ?? []).find((i) => i.id === String(e.active.id));
    setActiveIssue(issue ?? null);
  }
onDragEnd function · typescript · L123-L149 (27 LOC)
frontend/src/features/board/BoardPage.tsx
  async function onDragEnd(e: DragEndEvent) {
    setActiveIssue(null);
    setErrMsg(null);
    if (!e.over) return;
    const toStatus = e.over.id as IssueStatus;
    const issueId = String(e.active.id);
    const current = (issues.data ?? []).find((i) => i.id === issueId);
    if (!current || current.status === toStatus) return;

    // Optimistic update: immediately move the card in the cache
    const issuesKey = ['issues', { boardId }];
    const previousIssues = qc.getQueryData(issuesKey);
    qc.setQueryData(issuesKey, (old: any) =>
      old?.map((i: any) => i.id === issueId ? { ...i, status: toStatus } : i)
    );

    try {
      await updateIssue.mutateAsync({ id: issueId, patch: { status: toStatus } });
      toast('success', t('board.statusChanged', { status: toStatus }));
    } catch (err) {
      // Rollback on error
      qc.setQueryData(issuesKey, previousIssues);
      const msg = t('board.statusChangeFailed');
      setErrMsg(msg);
      toast('error', msg);
    }
 
handleReorder function · typescript · L151-L177 (27 LOC)
frontend/src/features/board/BoardPage.tsx
  async function handleReorder(status: IssueStatus, issueId: string, direction: 'up' | 'down') {
    if (!boardId) return;
    const columnIssues = grouped[status];
    const idx = columnIssues.findIndex((i) => i.id === issueId);
    if (idx === -1) return;
    const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
    if (swapIdx < 0 || swapIdx >= columnIssues.length) return;

    // Build the new order for this column
    const newColumnOrder = [...columnIssues];
    [newColumnOrder[idx], newColumnOrder[swapIdx]] = [newColumnOrder[swapIdx], newColumnOrder[idx]];

    // Optimistic update: reorder in cache preserving issues from other columns
    const issuesKey = ['issues', { boardId }];
    const previousIssues = qc.getQueryData(issuesKey);
    const allIssues: Issue[] = (issues.data ?? []);
    const otherIssues = allIssues.filter((i) => i.status !== status);
    qc.setQueryData(issuesKey, [...otherIssues, ...newColumnOrder]);

    try {
      const allColumnIds = newColumnOrder.m
BoardsListPage function · typescript · L57-L136 (80 LOC)
frontend/src/features/board/BoardsListPage.tsx
export default function BoardsListPage() {
  const { t } = useTranslation();
  const [name, setName] = useState('');
  const boards = useBoards();
  const createBoard = useCreateBoard();
  const { toast } = useToast();

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!name.trim()) return;
    try {
      await createBoard.mutateAsync({ name: name.trim() });
      setName('');
      toast('success', t('board.issueCreated'));
    } catch {
      toast('error', t('board.issueCreateFailed'));
    }
  };

  return (
    <section className="flex flex-col gap-6">
      <header className="fade-up flex items-end justify-between">
        <div>
          <h1 className="text-2xl font-bold">{t('board.title')}</h1>
          <p className="mt-1 text-ink-secondary">{t('board.description')}</p>
        </div>
      </header>

      <form onSubmit={submit} className="fade-up flex gap-2" style={{ animationDelay: '60ms' }}>
        <Input
          placeholder={t('board.n
elapsedTime function · typescript · L10-L16 (7 LOC)
frontend/src/features/board/IssueCard.tsx
function elapsedTime(dateStr: string): string {
  const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
  if (diff < 60) return 'just now';
  if (diff < 3600) return `${Math.floor(diff / 60)}m`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
  return `${Math.floor(diff / 86400)}d`;
}
IssueCard function · typescript · L23-L145 (123 LOC)
frontend/src/features/board/IssueCard.tsx
export function IssueCard({ issue, boardProperties = [] }: Props) {
  const { t } = useTranslation();
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
    id: issue.id,
  });
  const approve = useApproveIssue();
  const updateIssue = useUpdateIssue();
  const deleteIssue = useDeleteIssue();
  const navigate = useNavigate();
  const [editing, setEditing] = useState(false);
  const [editTitle, setEditTitle] = useState('');
  const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null);

  // DragOverlay handles the visual ghost — hide the original card during drag
  const style: React.CSSProperties | undefined = isDragging
    ? { opacity: 0.3, pointerEvents: 'none' }
    : undefined;

  // 속성 값 추출 (비어있지 않은 것만)
  const propValues = boardProperties
    .map((p) => ({ prop: p, value: issue.properties?.[p.id] }))
    .filter((pv) => pv.value !== undefined && pv.value !== null && pv.value !== '');

  return (
    <article
      ref={setNodeRef}
    
PropertyPill function · typescript · L147-L183 (37 LOC)
frontend/src/features/board/IssueCard.tsx
function PropertyPill({ prop, value }: { prop: BoardProperty; value: unknown }) {
  if (prop.type === 'checkbox') {
    if (!value) return null;
    return (
      <span className="inline-flex items-center gap-0.5 rounded bg-status-done/10 px-1.5 py-0.5 text-[11px] text-status-done">
        <Check size={10} /> {prop.name}
      </span>
    );
  }

  if (prop.type === 'date') {
    return (
      <span className="inline-flex items-center gap-0.5 rounded bg-status-inProgress/10 px-1.5 py-0.5 text-[11px] text-status-inProgress">
        <Calendar size={10} /> {String(value)}
      </span>
    );
  }

  if (prop.type === 'multi_select' && Array.isArray(value)) {
    return (
      <>
        {value.map((v: string) => (
          <span key={v} className="inline-flex items-center gap-0.5 rounded bg-brand-500/10 px-1.5 py-0.5 text-[11px] text-brand-500">
            <Tag size={9} /> {v}
          </span>
        ))}
      </>
    );
  }

  // select, text, number
  return (
    <span classNa
Powered by Repobility — scan your code at https://repobility.com
KanbanColumn function · typescript · L26-L111 (86 LOC)
frontend/src/features/board/KanbanColumn.tsx
export function KanbanColumn({ status, title, issues, boardProperties, onCreateIssue, onReorder }: Props) {
  const { t } = useTranslation();
  const { setNodeRef, isOver } = useDroppable({ id: status });
  const [adding, setAdding] = useState(false);
  const [newTitle, setNewTitle] = useState('');

  return (
    <section
      ref={setNodeRef}
      aria-label={t('kanban.columnLabel', { title })}
      className={`min-w-[260px] flex-1 flex min-h-[24rem] flex-col gap-2.5 rounded-lg border border-t-[3px] ${topAccent[status]} border-edge-base bg-surface-subtle p-3 transition-colors ${
        isOver ? 'border-brand-500 bg-brand-500/5' : ''
      }`}
    >
      <header className="flex items-center justify-between">
        <h2 className="text-sm font-semibold text-ink-secondary">{title}</h2>
        <span className="rounded-full bg-surface-muted px-2 py-0.5 text-xs tabular-nums text-ink-secondary">
          {issues.length}
        </span>
      </header>
      <div className="flex flex
pad function · typescript · L10-L12 (3 LOC)
frontend/src/features/calendar/CalendarPage.tsx
function pad(n: number): string {
  return n.toString().padStart(2, '0');
}
ymd function · typescript · L14-L16 (3 LOC)
frontend/src/features/calendar/CalendarPage.tsx
function ymd(d: Date): string {
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
startOfMonth function · typescript · L18-L20 (3 LOC)
frontend/src/features/calendar/CalendarPage.tsx
function startOfMonth(y: number, m: number): Date {
  return new Date(y, m - 1, 1);
}
daysInMonth function · typescript · L22-L24 (3 LOC)
frontend/src/features/calendar/CalendarPage.tsx
function daysInMonth(y: number, m: number): number {
  return new Date(y, m, 0).getDate();
}
CalendarPage function · typescript · L26-L143 (118 LOC)
frontend/src/features/calendar/CalendarPage.tsx
export default function CalendarPage() {
  const { t } = useTranslation();
  const today = useMemo(() => new Date(), []);
  const [year, setYear] = useState(today.getFullYear());
  const [month, setMonth] = useState(today.getMonth() + 1);
  const [selected, setSelected] = useState<string>(ymd(today));

  const monthStats = useMonthStats(year, month);
  const dayStats = useDayStats(selected);

  const countsByDate = useMemo(() => {
    const map = new Map<string, DayCount>();
    (monthStats.data ?? []).forEach((d) => {
      const key = d.date.slice(0, 10);
      map.set(key, d);
    });
    return map;
  }, [monthStats.data]);

  const firstOfMonth = startOfMonth(year, month);
  const firstWeekday = firstOfMonth.getDay(); // 0=일
  const total = daysInMonth(year, month);

  const cells: Array<{ day: number | null; dateStr?: string }> = [];
  for (let i = 0; i < firstWeekday; i++) cells.push({ day: null });
  for (let d = 1; d <= total; d++) {
    cells.push({ day: d, dateStr: `${year}-
shift function · typescript · L56-L60 (5 LOC)
frontend/src/features/calendar/CalendarPage.tsx
  function shift(delta: number) {
    const d = new Date(year, month - 1 + delta, 1);
    setYear(d.getFullYear());
    setMonth(d.getMonth() + 1);
  }
DaySection function · typescript · L145-L173 (29 LOC)
frontend/src/features/calendar/CalendarPage.tsx
function DaySection({ title, issues }: { title: string; issues: Issue[] }) {
  const { t } = useTranslation();
  return (
    <section aria-label={t('calendar.issuesLabel', { title })}>
      <h3 className="mb-2 text-sm font-medium text-ink-secondary">
        {title} <span className="text-ink-muted">({issues.length})</span>
      </h3>
      {issues.length === 0 ? (
        <div className="flex items-center gap-2 rounded-md border border-dashed border-edge-base py-3 px-3">
          <span className="text-xs text-ink-muted">{t('calendar.none')}</span>
        </div>
      ) : (
        <ul className="flex flex-col gap-1">
          {issues.map((iss) => (
            <li key={iss.id}>
              <Link
                to={`/issues/${iss.id}`}
                className="flex items-center justify-between gap-2 rounded-md bg-surface-base px-2 py-1.5 text-sm hover:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
              >
               
Repobility · severity-and-effort ranking · https://repobility.com
AgentMetrics function · typescript · L6-L96 (91 LOC)
frontend/src/features/home/AgentMetrics.tsx
export function AgentMetrics() {
  const { t } = useTranslation();
  const issues = useIssues({});

  const metrics = useMemo(() => {
    const all = issues.data ?? [];
    const total = all.length;
    const done = all.filter(i => i.status === 'Done').length;
    const rejected = all.filter(i => i.status === 'Rejected').length;
    const approved = all.filter(i => i.status === 'Approved').length;
    const inProgress = all.filter(i => i.status === 'InProgress').length;
    const pending = all.filter(i => i.status === 'Pending').length;

    // Completion rate
    const completionRate = total > 0 ? Math.round((done / total) * 100) : 0;

    // Approval rate (approved + inProgress + done out of total non-pending)
    const reviewed = total - pending;
    const approvalRate = reviewed > 0 ? Math.round(((approved + inProgress + done) / reviewed) * 100) : 0;

    // Average cycle time (created → done, for completed issues)
    const completedIssues = all.filter(i => i.status === 'Done');
 
MetricCard function · typescript · L98-L109 (12 LOC)
frontend/src/features/home/AgentMetrics.tsx
function MetricCard({ icon, label, value, sub }: { icon: React.ReactNode; label: string; value: string; sub: string }) {
  return (
    <div className="flex flex-col gap-1 rounded-lg border border-edge-base bg-gradient-to-br from-surface-subtle to-surface-muted p-3">
      <div className="flex items-center gap-1.5">
        {icon}
        <span className="text-xs font-medium text-ink-muted">{label}</span>
      </div>
      <span className="text-2xl font-bold tabular-nums text-ink-primary">{value}</span>
      <span className="text-xs text-ink-muted tabular-nums">{sub}</span>
    </div>
  );
}
HeroBanner function · typescript · L5-L26 (22 LOC)
frontend/src/features/home/HeroBanner.tsx
export default function HeroBanner() {
  const { t } = useTranslation();
  return (
    <div className="flex items-center justify-between gap-4 rounded-xl border border-edge-base bg-gradient-to-r from-brand-500/8 to-surface-subtle px-5 py-4">
      <div className="flex items-center gap-3">
        <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-brand-500/15 text-brand-500">
          <Rocket size={20} />
        </div>
        <div>
          <p className="text-sm font-semibold text-ink-primary">{t('app.tagline')}</p>
          <p className="text-xs text-ink-secondary">{t('app.description')}</p>
        </div>
      </div>
      <Link
        to="/boards"
        className="hidden sm:flex items-center gap-1 rounded-lg bg-brand-500 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-colors hover:bg-brand-600"
      >
        {t('nav.boards')} <ArrowRight size={12} />
      </Link>
    </div>
  );
}
todayString function · typescript · L11-L14 (4 LOC)
frontend/src/features/home/HomePage.tsx
function todayString(): string {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
relativeTime function · typescript · L16-L25 (10 LOC)
frontend/src/features/home/HomePage.tsx
function relativeTime(dateStr: string): string {
  const now = Date.now();
  const then = new Date(dateStr).getTime();
  const diff = Math.floor((now - then) / 1000);
  if (diff < 60) return 'just now';
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
  if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
  return new Date(dateStr).toLocaleDateString();
}
HomePage function · typescript · L27-L166 (140 LOC)
frontend/src/features/home/HomePage.tsx
export default function HomePage() {
  const date = useMemo(todayString, []);
  const { t } = useTranslation();
  const day = useDayStats(date);
  const approved = useIssues({ status: 'Approved' });
  const inProgress = useIssues({ status: 'InProgress' });
  const boards = useBoards();

  const created = day.data?.created.length ?? 0;
  const approvedCount = day.data?.approved.length ?? 0;
  const completed = day.data?.completed.length ?? 0;

  return (
    <section className="flex flex-col gap-6">
      <header className="fade-up">
        <h1 className="text-2xl font-bold tracking-tight">{t('home.title')}</h1>
        <p className="mt-1 text-sm text-ink-secondary">
          {t('home.summary', { date })}
        </p>
      </header>

      {/* 통계 카드 3분할 */}
      <div className="grid gap-3 sm:grid-cols-3">
        {day.isLoading ? (
          <>
            <Skeleton className="h-24 rounded-lg" />
            <Skeleton className="h-24 rounded-lg" />
            <Skeleton className="h
StatCard function · typescript · L168-L184 (17 LOC)
frontend/src/features/home/HomePage.tsx
function StatCard({ label, value, hue, accent, delay }: { label: string; value: number; hue: string; accent: string; delay: number }) {
  return (
    <div
      className="fade-up relative overflow-hidden rounded-lg border border-edge-base bg-gradient-to-br from-surface-subtle to-surface-muted p-4 shadow-sm transition-shadow hover:shadow-md"
      style={{ animationDelay: `${delay * 60}ms` }}
    >
      <div className={`absolute left-0 top-0 h-full w-1 rounded-l-lg ${accent}`} />
      <div className="flex items-end justify-between pl-2">
        <div className="flex flex-col gap-0.5">
          <span className="text-xs font-medium text-ink-muted">{label}</span>
          <span className={`text-3xl font-bold tabular-nums tracking-tight ${hue}`}>{value}</span>
        </div>
        <span className="text-xs text-ink-muted tabular-nums">{relativeTime(new Date().toISOString())}</span>
      </div>
    </div>
  );
}
getLast7Days function · typescript · L16-L27 (12 LOC)
frontend/src/features/home/WeeklyChart.tsx
function getLast7Days(): string[] {
  const days: string[] = [];
  const now = new Date();
  for (let i = 6; i >= 0; i--) {
    const d = new Date(now);
    d.setDate(d.getDate() - i);
    days.push(
      `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`,
    );
  }
  return days;
}
Repobility · code-quality intelligence · https://repobility.com
shortDay function · typescript · L29-L33 (5 LOC)
frontend/src/features/home/WeeklyChart.tsx
function shortDay(dateStr: string, locale: string): string {
  const d = new Date(dateStr + 'T00:00:00');
  const dayName = new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(d);
  return `${d.getDate()}(${dayName})`;
}
WeeklyChart function · typescript · L42-L156 (115 LOC)
frontend/src/features/home/WeeklyChart.tsx
export function WeeklyChart() {
  const { t, i18n } = useTranslation();
  const now = new Date();
  const stats = useMonthStats(now.getFullYear(), now.getMonth() + 1);

  const days = useMemo(getLast7Days, []);

  const data = useMemo(() => {
    const map = new Map<string, { created: number; approved: number; completed: number }>();
    (stats.data ?? []).forEach((d) => {
      const key = d.date.slice(0, 10);
      map.set(key, { created: d.created, approved: d.approved, completed: d.completed });
    });
    return days.map((day) => ({
      name: shortDay(day, i18n.language),
      [t('home.created')]: map.get(day)?.created ?? 0,
      [t('home.approved')]: map.get(day)?.approved ?? 0,
      [t('home.done')]: map.get(day)?.completed ?? 0,
    }));
  }, [stats.data, days, t, i18n.language]);

  const hasData = data.some((d) =>
    Object.values(d).some((v) => typeof v === 'number' && v > 0),
  );

  /* Empty state — show a placeholder instead of hiding entirely */
  if (!hasData && 
IssuePage function · typescript · L27-L313 (287 LOC)
frontend/src/features/issue/IssuePage.tsx
export default function IssuePage() {
  const { t } = useTranslation();
  const { issueId } = useParams<{ issueId: string }>();
  const navigate = useNavigate();
  const query = useIssue(issueId);
  const boards = useBoards();
  const update = useUpdateIssue();
  const approve = useApproveIssue();
  const remove = useDeleteIssue();
  const updateProps = useUpdateIssueProperties();
  const boardProps = useBoardProperties(query.data?.issue.boardId);

  const addDep = useAddDependency();
  const removeDep = useRemoveDependency();

  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const [instructions, setInstructions] = useState('');
  const [criteria, setCriteria] = useState<Criterion[]>([]);
  const [saveErr, setSaveErr] = useState<string | null>(null);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [depInput, setDepInput] = useState('');

  useEffect(() => {
    if (query.data?.issue) {
      setTitle(query.data.issue.title);
     
‹ prevpage 5 / 6next ›