feat(portfolio): 기술 스택을 SimpleIcons 로고 무한 캐러셀로 표시
LogoLoop 컴포넌트를 추가해 카테고리별(언어/프레임워크/인프라/도구) 4줄 가로 스크롤 캐러셀로 skill 로고를 표시. simpleicons CDN을 사용하며 매칭 안 되는 항목(APScheduler, KIS Open API)은 텍스트 칩으로 자동 fallback. 편집 모드에서는 기존 칩 UI를 유지해 편집·삭제 가능. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -378,6 +378,39 @@
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
/* ── Skill Logo Loop ─────────────────────────────────────────────────── */
|
||||
|
||||
.pf-skill-loop {
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pf-skill-logo {
|
||||
height: 36px;
|
||||
width: auto;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pf-skill-fallback {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text-bright);
|
||||
background: rgba(6, 182, 212, 0.12);
|
||||
border: 1px solid rgba(6, 182, 212, 0.35);
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Filter Bar ──────────────────────────────────────────────────────── */
|
||||
|
||||
.pf-filter-bar {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import LogoLoop from '../../components/LogoLoop';
|
||||
|
||||
const CAREER_CATEGORIES = { company: '회사', education: '교육', etc: '기타' };
|
||||
const SKILL_CATEGORIES = { language: '언어', framework: '프레임워크', infra: '인프라', tool: '도구' };
|
||||
@@ -6,6 +7,53 @@ const SKILL_CATEGORIES = { language: '언어', framework: '프레임워크', inf
|
||||
const emptyCareer = { category: 'company', organization: '', role: '', description: '', start_date: '', end_date: '', sort_order: 0 };
|
||||
const emptySkill = { category: 'language', name: '', level: 3, sort_order: 0 };
|
||||
|
||||
const SKILL_LOGO_SLUGS = {
|
||||
Python: 'python',
|
||||
JavaScript: 'javascript',
|
||||
SQL: 'mysql',
|
||||
'HTML/CSS': 'html5',
|
||||
FastAPI: 'fastapi',
|
||||
React: 'react',
|
||||
Vite: 'vite',
|
||||
Docker: 'docker',
|
||||
'Synology NAS': 'synology',
|
||||
Nginx: 'nginx',
|
||||
Gitea: 'gitea',
|
||||
SQLite: 'sqlite',
|
||||
Linux: 'linux',
|
||||
Git: 'git',
|
||||
'Claude API': 'anthropic',
|
||||
Ollama: 'ollama',
|
||||
'Suno API': 'suno',
|
||||
};
|
||||
|
||||
function SkillLogoNode({ name, slug }) {
|
||||
const [error, setError] = useState(false);
|
||||
if (!slug || error) {
|
||||
return <span className="pf-skill-fallback">{name}</span>;
|
||||
}
|
||||
return (
|
||||
<img
|
||||
className="pf-skill-logo"
|
||||
src={`https://cdn.simpleicons.org/${slug}`}
|
||||
alt={name}
|
||||
title={name}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function buildSkillLogos(items) {
|
||||
return items.map((s) => ({
|
||||
node: <SkillLogoNode name={s.name} slug={SKILL_LOGO_SLUGS[s.name]} />,
|
||||
ariaLabel: s.name,
|
||||
title: s.name,
|
||||
}));
|
||||
}
|
||||
|
||||
export default function ProfileTab({ data, editing, api, onRefresh }) {
|
||||
const { profile, careers, skills } = data;
|
||||
const [editingProfile, setEditingProfile] = useState(null);
|
||||
@@ -180,19 +228,33 @@ export default function ProfileTab({ data, editing, api, onRefresh }) {
|
||||
items.length > 0 && (
|
||||
<div key={cat} className="pf-skill-group">
|
||||
<h4 className="pf-skill-group__title">{SKILL_CATEGORIES[cat]}</h4>
|
||||
<div className="pf-skill-group__tags">
|
||||
{items.map((s) => (
|
||||
<span key={s.id} className="pf-skill-tag" data-level={s.level}>
|
||||
{s.name}
|
||||
{editing && (
|
||||
{editing ? (
|
||||
<div className="pf-skill-group__tags">
|
||||
{items.map((s) => (
|
||||
<span key={s.id} className="pf-skill-tag" data-level={s.level}>
|
||||
{s.name}
|
||||
<span className="pf-skill-tag__actions">
|
||||
<button onClick={() => setSkillForm({...s})}>✎</button>
|
||||
<button onClick={() => deleteSkill(s.id)}>×</button>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="pf-skill-loop">
|
||||
<LogoLoop
|
||||
logos={buildSkillLogos(items)}
|
||||
speed={40}
|
||||
logoHeight={36}
|
||||
gap={48}
|
||||
pauseOnHover
|
||||
fadeOut
|
||||
fadeOutColor="var(--bg-primary, #0b0b0b)"
|
||||
scaleOnHover
|
||||
ariaLabel={`${SKILL_CATEGORIES[cat]} 기술 스택`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user