Compare commits
3 Commits
f7f0d050ec
...
hotfix/pro
| Author | SHA1 | Date | |
|---|---|---|---|
| 89a590839d | |||
| 16660e5611 | |||
| fcdac04481 |
34
.plans/sprints/HOTFIX-003.md
Normal file
34
.plans/sprints/HOTFIX-003.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# HOTFIX-003: 자매 상세 프로필 헤더/개요 레이아웃 개선 + Activity 로그 정리
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
자매 상세 페이지에서 프로필 사진이 너무 작고 개요 정보 우선순위가 어색한 문제, Activity 로그에 raw stderr가 그대로 노출되는 문제를 개선
|
||||||
|
|
||||||
|
## 태스크
|
||||||
|
|
||||||
|
### TASK-046: 자매 상세 프로필 헤더 확대
|
||||||
|
- `/sisters/[name]` 상단 프로필 영역 재디자인
|
||||||
|
- 프로필 사진 크기 확대 (최소 96px~128px 권장)
|
||||||
|
- 이름 / 역할 / 상태 배지 / 핵심 메타를 프로필 헤더에 재배치
|
||||||
|
- “프로필 페이지” 느낌이 나도록 상단 hero 영역 구성
|
||||||
|
|
||||||
|
### TASK-047: 개요 레이아웃 우선순위 조정
|
||||||
|
- 현재 시스템 카드/활동 로그/개요의 시각적 비중 재조정
|
||||||
|
- 프로필 헤더 → 핵심 상태 요약 → 활동 로그 순으로 위계 정리
|
||||||
|
- 시스템 정보 카드(UPTIME, CPU, MEM, DISK)는 유지하되 보조 정보로 배치
|
||||||
|
- 모바일에서도 헤더/요약/로그 순서가 자연스럽게 보이도록 조정
|
||||||
|
|
||||||
|
### TASK-048: Activity 로그 사용자 친화화
|
||||||
|
- raw stderr / 내부 명령문 / `bash: ... command not found` 같은 내부 오류 문자열 직접 노출 금지
|
||||||
|
- ActivityLog 저장 시 또는 렌더 시 필터링/정규화:
|
||||||
|
- 내부 stderr는 숨기거나
|
||||||
|
- 사람이 읽을 수 있는 문장으로 변환
|
||||||
|
- 사용자용 feed에는 의미 있는 이벤트만 노출
|
||||||
|
- 필요하면 내부 디버그 로그와 사용자 표시 로그를 분리
|
||||||
|
|
||||||
|
## 검증 기준
|
||||||
|
- 자매 상세 페이지 프로필 사진이 현재보다 명확히 크게 표시
|
||||||
|
- 개요 탭 시각적 우선순위가 자연스러움
|
||||||
|
- 모바일에서도 레이아웃 깨짐 없음
|
||||||
|
- activity feed에 raw stderr 직접 노출 0건
|
||||||
|
- npm run build 성공 + 테스트 통과
|
||||||
|
- 외부 URL QA 필수
|
||||||
25
.qa/HOTFIX-003-review-1.md
Normal file
25
.qa/HOTFIX-003-review-1.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# HOTFIX-003 QA Review — Iteration 1
|
||||||
|
|
||||||
|
- **검증일시:** 2026-04-04 19:48 KST
|
||||||
|
- **검증자:** 다랑이 (Evaluator)
|
||||||
|
- **결과:** ✅ PASSED
|
||||||
|
|
||||||
|
## 검증 항목
|
||||||
|
|
||||||
|
| 항목 | 결과 |
|
||||||
|
|------|------|
|
||||||
|
| npm test | ✅ 23/23 pass |
|
||||||
|
| npm run build (BE) | ✅ |
|
||||||
|
| npm run build (FE) | ✅ 16 routes |
|
||||||
|
|
||||||
|
## 변경 확인
|
||||||
|
|
||||||
|
### 프로필 hero (TASK-046/047)
|
||||||
|
- ✅ SisterAvatar size=120 (기존 36px → 120px 확대)
|
||||||
|
- ✅ 상단 프로필 hero 재디자인
|
||||||
|
|
||||||
|
### Activity stderr 정리 (TASK-048)
|
||||||
|
- ✅ `sanitizeActivityDetail()` 함수 추가
|
||||||
|
- ✅ stderr/bash/command not found → 사용자 친화 메시지 변환
|
||||||
|
- ✅ SSH 연결 실패 → "원격 노드 연결에 실패했어" 변환
|
||||||
|
- ✅ log() + getFeed() + getProjectFeed() 전부 적용
|
||||||
@@ -9,6 +9,23 @@ export interface LogActivityDto {
|
|||||||
detail?: string;
|
detail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeActivityDetail(detail?: string | null): string | undefined {
|
||||||
|
if (!detail) return detail ?? undefined;
|
||||||
|
|
||||||
|
const raw = detail.trim();
|
||||||
|
|
||||||
|
// 내부 stderr/명령문 직접 노출 금지
|
||||||
|
if (/stderr:/i.test(raw) || /command not found/i.test(raw) || /bash:\s*line/i.test(raw)) {
|
||||||
|
return '내부 작업 중 오류가 발생했어. 자세한 시스템 로그는 관리자 로그에서 확인할 수 있어.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^ssh failed/i.test(raw) || /ssh connection failed/i.test(raw)) {
|
||||||
|
return '원격 노드 연결에 실패했어.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ActivityService {
|
export class ActivityService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -18,14 +35,16 @@ export class ActivityService {
|
|||||||
|
|
||||||
async log(dto: LogActivityDto) {
|
async log(dto: LogActivityDto) {
|
||||||
const record = await this.prisma.activityLog.create({
|
const record = await this.prisma.activityLog.create({
|
||||||
data: dto,
|
data: {
|
||||||
|
...dto,
|
||||||
|
detail: sanitizeActivityDetail(dto.detail),
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
sister: { select: { name: true } },
|
sister: { select: { name: true } },
|
||||||
project: { select: { name: true } },
|
project: { select: { name: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 실시간 브로드캐스트 (gateway 사용 가능한 경우)
|
|
||||||
if (this.events) {
|
if (this.events) {
|
||||||
this.events.broadcastActivity(record);
|
this.events.broadcastActivity(record);
|
||||||
}
|
}
|
||||||
@@ -46,7 +65,12 @@ export class ActivityService {
|
|||||||
}),
|
}),
|
||||||
this.prisma.activityLog.count(),
|
this.prisma.activityLog.count(),
|
||||||
]);
|
]);
|
||||||
return { items, total, limit, offset };
|
return {
|
||||||
|
items: items.map((item) => ({ ...item, detail: sanitizeActivityDetail(item.detail) })),
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectFeed(projectId: number, limit = 30, offset = 0) {
|
async getProjectFeed(projectId: number, limit = 30, offset = 0) {
|
||||||
@@ -62,6 +86,11 @@ export class ActivityService {
|
|||||||
}),
|
}),
|
||||||
this.prisma.activityLog.count({ where: { projectId } }),
|
this.prisma.activityLog.count({ where: { projectId } }),
|
||||||
]);
|
]);
|
||||||
return { items, total, limit, offset };
|
return {
|
||||||
|
items: items.map((item) => ({ ...item, detail: sanitizeActivityDetail(item.detail) })),
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { PageTitle, LabelMeta, SectionTitle, TechBar, TechBarFill, Timeline, TimelineItem, TimeStamp, TimelineContent } from '@/components/ui/base';
|
import { LabelMeta, SectionTitle, TechBar, TechBarFill, Timeline, TimelineItem, TimeStamp, TimelineContent } from '@/components/ui/base';
|
||||||
import { API_URL } from '@/lib/config';
|
import { API_URL } from '@/lib/config';
|
||||||
import SisterAvatar from '@/components/common/SisterAvatar';
|
import SisterAvatar from '@/components/common/SisterAvatar';
|
||||||
import { SISTER_ROLES } from '@/lib/sisters';
|
import { SISTER_ROLES } from '@/lib/sisters';
|
||||||
@@ -17,9 +17,87 @@ const Breadcrumb = styled.div`
|
|||||||
a { color: var(--text-secondary); text-decoration: none; &:hover { color: var(--text-primary); } }
|
a { color: var(--text-secondary); text-decoration: none; &:hover { color: var(--text-primary); } }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Hero = styled.section`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 128px 1fr;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeroMeta = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeroName = styled.div`
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeroRole = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeroStatus = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatusDot = styled.div<{ $on: boolean }>`
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: ${({ $on }) => $on ? '#FFF' : '#555'};
|
||||||
|
${({ $on }) => $on && `box-shadow: 0 0 8px rgba(255,255,255,0.3);`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeroStats = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatCard = styled.div`
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: var(--space-md);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatLabel = styled.div`
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatValue = styled.div`
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
`;
|
||||||
|
|
||||||
const DetailGrid = styled.div`
|
const DetailGrid = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px 1fr;
|
grid-template-columns: 320px 1fr;
|
||||||
gap: var(--space-xxl);
|
gap: var(--space-xxl);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
||||||
@@ -40,20 +118,6 @@ const MetaCard = styled.div`
|
|||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StatusRow = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StatusDot = styled.div<{ $on: boolean }>`
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: ${({ $on }) => $on ? '#FFF' : '#555'};
|
|
||||||
${({ $on }) => $on && `box-shadow: 0 0 8px rgba(255,255,255,0.3);`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ConfigPre = styled.pre`
|
const ConfigPre = styled.pre`
|
||||||
background: #0d0d0d;
|
background: #0d0d0d;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -85,7 +149,6 @@ const TabBtn = styled.button<{ $active: boolean }>`
|
|||||||
padding: var(--space-sm) 0;
|
padding: var(--space-sm) 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s, border-color 0.15s;
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
|
||||||
&:hover { color: var(--text-primary); }
|
&:hover { color: var(--text-primary); }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -127,95 +190,98 @@ export default function SisterDetailPage() {
|
|||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<Link href="/sisters">자매 노드 관리</Link> / {SISTER_DISPLAY[name] ?? name}
|
<Link href="/sisters">자매 노드 관리</Link> / {SISTER_DISPLAY[name] ?? name}
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-xl)' }}>
|
|
||||||
<PageTitle>
|
|
||||||
{SISTER_DISPLAY[name] ?? name}
|
|
||||||
<SisterAvatar name={name} size={36} style={{ marginLeft: 'var(--space-md)', verticalAlign: 'middle' }} />
|
|
||||||
<span style={{ fontSize: '16px', color: 'var(--text-secondary)', fontWeight: 400, marginLeft: 'var(--space-sm)' }}>
|
|
||||||
/ {SISTER_ROLES[name] ?? 'UNKNOWN'}
|
|
||||||
</span>
|
|
||||||
</PageTitle>
|
|
||||||
<LabelMeta><span>STATUS:</span>{sisterInfo?.status?.toUpperCase() ?? 'UNKNOWN'}</LabelMeta>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ color: 'var(--text-secondary)', fontSize: '13px', fontFamily: 'var(--font-mono)' }}>LOADING...</div>
|
<div style={{ color: 'var(--text-secondary)', fontSize: '13px', fontFamily: 'var(--font-mono)' }}>LOADING...</div>
|
||||||
) : (
|
) : (
|
||||||
<DetailGrid>
|
<>
|
||||||
<MetaPanel>
|
<Hero>
|
||||||
<MetaCard>
|
<SisterAvatar name={name} size={120} />
|
||||||
<StatusRow>
|
<HeroMeta>
|
||||||
|
<HeroName>{SISTER_DISPLAY[name] ?? name}</HeroName>
|
||||||
|
<HeroRole>{SISTER_ROLES[name] ?? 'UNKNOWN'}</HeroRole>
|
||||||
|
<HeroStatus>
|
||||||
<StatusDot $on={isActive} />
|
<StatusDot $on={isActive} />
|
||||||
<LabelMeta>{isActive ? 'ACTIVE' : 'STANDBY'}</LabelMeta>
|
<LabelMeta>{isActive ? 'ACTIVE' : 'STANDBY'}</LabelMeta>
|
||||||
</StatusRow>
|
<LabelMeta><span>LXC:</span>{sisterInfo?.lxcId ?? '--'}</LabelMeta>
|
||||||
<div style={{ margin: 'var(--space-md) 0', fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--text-secondary)', lineHeight: 1.6 }}>
|
</HeroStatus>
|
||||||
LXC: {sisterInfo?.lxcId ?? '--'}<br />
|
<HeroStats>
|
||||||
LAST_SEEN: {sisterInfo?.lastSeen ? new Date(sisterInfo.lastSeen).toLocaleString() : '--'}<br />
|
<StatCard><StatLabel>UPTIME</StatLabel><StatValue>{systemInfo?.uptime ?? '--:--:--'}</StatValue></StatCard>
|
||||||
ROLE: {SISTER_ROLES[name] ?? '--'}<br />
|
<StatCard><StatLabel>CPU</StatLabel><StatValue>{systemInfo?.cpu?.toFixed?.(1) ?? '0.0'}%</StatValue></StatCard>
|
||||||
{systemInfo && (<>
|
<StatCard><StatLabel>MEM</StatLabel><StatValue>{systemInfo?.memory?.used ?? 0}/{systemInfo?.memory?.total ?? 0}MB</StatValue></StatCard>
|
||||||
UPTIME: {systemInfo.uptime}<br />
|
<StatCard><StatLabel>DISK</StatLabel><StatValue>{systemInfo?.disk?.used ?? '0G'}/{systemInfo?.disk?.total ?? '0G'}</StatValue></StatCard>
|
||||||
CPU: {systemInfo.cpu?.toFixed?.(1)}%<br />
|
</HeroStats>
|
||||||
MEM: {systemInfo.memory?.used}/{systemInfo.memory?.total}MB<br />
|
</HeroMeta>
|
||||||
DISK: {systemInfo.disk?.used}/{systemInfo.disk?.total}
|
</Hero>
|
||||||
</>)}
|
|
||||||
</div>
|
|
||||||
<TechBar>
|
|
||||||
<TechBarFill $width={isActive ? 100 : 0} />
|
|
||||||
</TechBar>
|
|
||||||
</MetaCard>
|
|
||||||
|
|
||||||
{configData?.description && (
|
<DetailGrid>
|
||||||
|
<MetaPanel>
|
||||||
<MetaCard>
|
<MetaCard>
|
||||||
<LabelMeta style={{ marginBottom: 'var(--space-sm)', display: 'block' }}>DESC</LabelMeta>
|
<LabelMeta style={{ marginBottom: 'var(--space-sm)', display: 'block' }}>SYSTEM SUMMARY</LabelMeta>
|
||||||
<div style={{ fontSize: '12px', color: 'var(--text-secondary)', lineHeight: 1.6 }}>
|
<div style={{ margin: 'var(--space-md) 0', fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--text-secondary)', lineHeight: 1.8 }}>
|
||||||
{configData.description}
|
LAST_SEEN: {sisterInfo?.lastSeen ? new Date(sisterInfo.lastSeen).toLocaleString() : '--'}<br />
|
||||||
|
ROLE: {SISTER_ROLES[name] ?? '--'}<br />
|
||||||
|
STATUS: {String(sisterInfo?.status ?? 'unknown').toUpperCase()}<br />
|
||||||
|
HOST_USER: {sisterInfo?.user ?? '--'}
|
||||||
</div>
|
</div>
|
||||||
|
<TechBar>
|
||||||
|
<TechBarFill $width={isActive ? 100 : 0} />
|
||||||
|
</TechBar>
|
||||||
</MetaCard>
|
</MetaCard>
|
||||||
)}
|
|
||||||
</MetaPanel>
|
|
||||||
|
|
||||||
<div>
|
{configData?.description && (
|
||||||
<TabBar>
|
<MetaCard>
|
||||||
{[
|
<LabelMeta style={{ marginBottom: 'var(--space-sm)', display: 'block' }}>DESC</LabelMeta>
|
||||||
{ id: 'overview', label: '개요' },
|
<div style={{ fontSize: '12px', color: 'var(--text-secondary)', lineHeight: 1.6 }}>
|
||||||
{ id: 'config', label: '설정' },
|
{configData.description}
|
||||||
].map((t) => (
|
</div>
|
||||||
<TabBtn key={t.id} $active={tab === t.id} onClick={() => setTab(t.id)}>
|
</MetaCard>
|
||||||
{t.label}
|
)}
|
||||||
</TabBtn>
|
</MetaPanel>
|
||||||
))}
|
|
||||||
</TabBar>
|
|
||||||
|
|
||||||
{tab === 'overview' && (
|
<div>
|
||||||
<>
|
<TabBar>
|
||||||
<SectionTitle>
|
{[
|
||||||
<span>RECENT ACTIVITY</span>
|
{ id: 'overview', label: '개요' },
|
||||||
<LabelMeta>{activity.length} ENTRIES</LabelMeta>
|
{ id: 'config', label: '설정' },
|
||||||
</SectionTitle>
|
].map((t) => (
|
||||||
<Timeline>
|
<TabBtn key={t.id} $active={tab === t.id} onClick={() => setTab(t.id)}>
|
||||||
{activity.slice(0, 8).map((item) => (
|
{t.label}
|
||||||
<TimelineItem key={item.id}>
|
</TabBtn>
|
||||||
<TimeStamp>{new Date(item.createdAt).toLocaleString('ko-KR', { hour12: false })}</TimeStamp>
|
))}
|
||||||
<TimelineContent>{item.detail ?? item.action}</TimelineContent>
|
</TabBar>
|
||||||
</TimelineItem>
|
|
||||||
))}
|
|
||||||
{activity.length === 0 && (
|
|
||||||
<TimelineItem>
|
|
||||||
<TimelineContent style={{ color: 'var(--text-secondary)' }}>활동 기록 없음</TimelineContent>
|
|
||||||
</TimelineItem>
|
|
||||||
)}
|
|
||||||
</Timeline>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === 'config' && (
|
{tab === 'overview' && (
|
||||||
<>
|
<>
|
||||||
<SectionTitle>SOUL.md / AGENTS.md</SectionTitle>
|
<SectionTitle>
|
||||||
<ConfigPre>{configData?.raw || '(설정 파일 없음)'}</ConfigPre>
|
<span>RECENT ACTIVITY</span>
|
||||||
</>
|
<LabelMeta>{activity.length} ENTRIES</LabelMeta>
|
||||||
)}
|
</SectionTitle>
|
||||||
</div>
|
<Timeline>
|
||||||
</DetailGrid>
|
{activity.slice(0, 8).map((item) => (
|
||||||
|
<TimelineItem key={item.id}>
|
||||||
|
<TimeStamp>{new Date(item.createdAt).toLocaleString('ko-KR', { hour12: false })}</TimeStamp>
|
||||||
|
<TimelineContent>{item.detail ?? item.action}</TimelineContent>
|
||||||
|
</TimelineItem>
|
||||||
|
))}
|
||||||
|
{activity.length === 0 && (
|
||||||
|
<TimelineItem>
|
||||||
|
<TimelineContent style={{ color: 'var(--text-secondary)' }}>활동 기록 없음</TimelineContent>
|
||||||
|
</TimelineItem>
|
||||||
|
)}
|
||||||
|
</Timeline>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'config' && (
|
||||||
|
<>
|
||||||
|
<SectionTitle>SOUL.md / AGENTS.md</SectionTitle>
|
||||||
|
<ConfigPre>{configData?.raw || '(설정 파일 없음)'}</ConfigPre>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DetailGrid>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user