refactor(insta): instaUtils(fmtDate)+usePollTask 훅+StatusBadge/TaskStatusBox 추출
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
11
src/pages/insta/components/StatusBadge.jsx
Normal file
11
src/pages/insta/components/StatusBadge.jsx
Normal 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;
|
||||||
20
src/pages/insta/components/TaskStatusBox.jsx
Normal file
20
src/pages/insta/components/TaskStatusBox.jsx
Normal 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;
|
||||||
41
src/pages/insta/hooks/usePollTask.js
Normal file
41
src/pages/insta/hooks/usePollTask.js
Normal 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); },
|
||||||
|
};
|
||||||
|
}
|
||||||
10
src/pages/insta/instaUtils.js
Normal file
10
src/pages/insta/instaUtils.js
Normal 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
15
src/pages/insta/instaUtils.test.js
Normal file
15
src/pages/insta/instaUtils.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user