refactor(insta): instaUtils(fmtDate)+usePollTask 훅+StatusBadge/TaskStatusBox 추출

This commit is contained in:
2026-07-10 13:31:23 +09:00
parent 25a8be6e23
commit dfe621362d
6 changed files with 101 additions and 75 deletions

View File

@@ -21,81 +21,10 @@ import {
putInstaPreferences, putInstaPreferences,
} from '../../api'; } from '../../api';
import './InstaCards.css'; import './InstaCards.css';
import { fmtDate } from './instaUtils';
/* ────────────────────── 유틸 ────────────────────── */ import usePollTask from './hooks/usePollTask';
function fmtDate(iso) { import StatusBadge from './components/StatusBadge';
if (!iso) return ''; import TaskStatusBox from './components/TaskStatusBox';
return new Date(iso).toLocaleDateString('ko-KR', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
function StatusBadge({ status }) {
return (
<span className={`ic-status-badge ic-status-badge--${status || 'draft'}`}>
{status || 'draft'}
</span>
);
}
/* ────────────────────── 폴링 훅 ────────────────────── */
function usePollTask(onDone) {
const [taskId, setTaskId] = useState(null);
const [task, setTask] = useState(null);
const timer = useRef(null);
useEffect(() => {
if (!taskId) return;
let cancelled = false;
const poll = async () => {
try {
const t = await getInstaTask(taskId);
if (cancelled) return;
setTask(t);
if (t.status === 'succeeded' || t.status === 'failed') {
setTaskId(null);
onDone?.(t);
} else {
timer.current = setTimeout(poll, 3000);
}
} catch {
if (!cancelled) timer.current = setTimeout(poll, 3000);
}
};
poll();
return () => {
cancelled = true;
clearTimeout(timer.current);
};
}, [taskId]); // eslint-disable-line react-hooks/exhaustive-deps
return {
taskId,
task,
start: setTaskId,
clear: () => { setTaskId(null); setTask(null); },
};
}
/* ────────────────────── TaskStatusBox ────────────────────── */
function TaskStatusBox({ task }) {
if (!task) return null;
const pct = task.progress != null ? task.progress : (task.status === 'succeeded' ? 100 : 0);
return (
<div className="ic-task-status">
<div className="ic-task-status__label">
{task.status === 'succeeded' ? '완료' : task.status === 'failed' ? '실패' : '진행 중'}
</div>
<div className="ic-task-status__msg">{task.message || task.error || ''}</div>
<div className="ic-task-status__progress">
<div className="ic-task-status__fill" style={{ width: `${pct}%` }} />
</div>
</div>
);
}
/* ══════════════════════ Trends 탭 패널 1: AccountFocusPanel ══════════════ */ /* ══════════════════════ Trends 탭 패널 1: AccountFocusPanel ══════════════ */
function AccountFocusPanel() { function AccountFocusPanel() {

View File

@@ -0,0 +1,11 @@
import React from 'react';
function StatusBadge({ status }) {
return (
<span className={`ic-status-badge ic-status-badge--${status || 'draft'}`}>
{status || 'draft'}
</span>
);
}
export default StatusBadge;

View File

@@ -0,0 +1,20 @@
import React from 'react';
/* ────────────────────── TaskStatusBox ────────────────────── */
function TaskStatusBox({ task }) {
if (!task) return null;
const pct = task.progress != null ? task.progress : (task.status === 'succeeded' ? 100 : 0);
return (
<div className="ic-task-status">
<div className="ic-task-status__label">
{task.status === 'succeeded' ? '완료' : task.status === 'failed' ? '실패' : '진행 중'}
</div>
<div className="ic-task-status__msg">{task.message || task.error || ''}</div>
<div className="ic-task-status__progress">
<div className="ic-task-status__fill" style={{ width: `${pct}%` }} />
</div>
</div>
);
}
export default TaskStatusBox;

View File

@@ -0,0 +1,41 @@
import { useState, useEffect, useRef } from 'react';
import { getInstaTask } from '../../../api';
/* ────────────────────── 폴링 훅 ────────────────────── */
export default function usePollTask(onDone) {
const [taskId, setTaskId] = useState(null);
const [task, setTask] = useState(null);
const timer = useRef(null);
useEffect(() => {
if (!taskId) return;
let cancelled = false;
const poll = async () => {
try {
const t = await getInstaTask(taskId);
if (cancelled) return;
setTask(t);
if (t.status === 'succeeded' || t.status === 'failed') {
setTaskId(null);
onDone?.(t);
} else {
timer.current = setTimeout(poll, 3000);
}
} catch {
if (!cancelled) timer.current = setTimeout(poll, 3000);
}
};
poll();
return () => {
cancelled = true;
clearTimeout(timer.current);
};
}, [taskId]); // eslint-disable-line react-hooks/exhaustive-deps
return {
taskId,
task,
start: setTaskId,
clear: () => { setTaskId(null); setTask(null); },
};
}

View File

@@ -0,0 +1,10 @@
// src/pages/insta/instaUtils.js
export function fmtDate(iso) {
if (!iso) return '';
return new Date(iso).toLocaleDateString('ko-KR', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}

View File

@@ -0,0 +1,15 @@
import { describe, it, expect } from 'vitest';
import { fmtDate } from './instaUtils';
describe('instaUtils.fmtDate', () => {
it('returns empty string for falsy input', () => {
expect(fmtDate('')).toBe('');
expect(fmtDate(null)).toBe('');
expect(fmtDate(undefined)).toBe('');
});
it('returns a non-empty string for a valid ISO date (locale/tz-dependent format)', () => {
const out = fmtDate('2026-07-10T09:30:00Z');
expect(typeof out).toBe('string');
expect(out.length).toBeGreaterThan(0);
});
});