Refine program metadata layout

This commit is contained in:
2026-06-25 17:47:00 +09:00
parent 79cda65da9
commit ea3009e922

View File

@@ -10,7 +10,6 @@ import {
Layers3,
Map,
Maximize2,
Minimize2,
Mountain,
Plus,
Route,
@@ -222,6 +221,7 @@ const defaultContent = {
steps: cheonjiinFlow.map(({ title, feature, note }) => ({ title, feature, note })),
deliverables: cheonjiinDeliverables,
format: 'glb',
engine: '',
programType: 'internal',
programNote: '',
predecessors: [],
@@ -233,6 +233,7 @@ const defaultContent = {
description: '설계조건 입력부터 기본설계 성과물 생성',
steps: wayPrimalFlow.map(({ title, feature, note }) => ({ title, feature, note })),
format: '',
engine: '',
deliverables: ['기본설계 모델'],
programType: 'internal',
programNote: '',
@@ -253,6 +254,7 @@ function normalizeStoredContent(parsed) {
...(parsed.cheonjiin ?? {}),
programType: getProgramType(parsed.cheonjiin?.programType ?? defaultContent.cheonjiin.programType),
programNote: parsed.cheonjiin?.programNote ?? '',
engine: parsed.cheonjiin?.engine ?? '',
predecessors: parsed.cheonjiin?.predecessors ?? [],
successors: parsed.cheonjiin?.successors ?? [],
mergeGroup: parsed.cheonjiin?.mergeGroup ?? '',
@@ -266,6 +268,7 @@ function normalizeStoredContent(parsed) {
...(parsed.wayPrimal ?? {}),
programType: getProgramType(parsed.wayPrimal?.programType ?? defaultContent.wayPrimal.programType),
programNote: parsed.wayPrimal?.programNote ?? '',
engine: parsed.wayPrimal?.engine ?? '',
predecessors: parsed.wayPrimal?.predecessors ?? [],
successors: parsed.wayPrimal?.successors ?? [],
mergeGroup: parsed.wayPrimal?.mergeGroup ?? '',
@@ -283,6 +286,7 @@ function normalizeStoredContent(parsed) {
...program,
programType: getProgramType(program.programType),
programNote: program.programNote ?? '',
engine: program.engine ?? '',
predecessors: program.predecessors ?? [],
successors: program.successors ?? [],
mergeGroup: program.mergeGroup ?? '',
@@ -568,6 +572,8 @@ function FlowRow({
onStepChange,
format,
onFormatChange,
engine = '',
onEngineChange,
deliverables = [],
onDeliverableChange,
onLabelChange,
@@ -676,8 +682,11 @@ function FlowRow({
isRowClickable ? 'cursor-pointer transition hover:-translate-y-0.5 hover:shadow-[0_22px_60px_rgba(15,23,42,0.12)]' : ''
}`}
>
<div className="mb-5 flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<div
className="mb-5 flex flex-col gap-2 md:grid md:items-start md:justify-start md:gap-5"
style={{ gridTemplateColumns: '720px minmax(0, 1fr)' }}
>
<div className="max-w-none">
<div className="flex flex-wrap items-center gap-2">
{isEditing && onLabelChange ? (
<input
@@ -740,7 +749,7 @@ function FlowRow({
value={description}
onChange={(event) => onDescriptionChange(event.target.value)}
rows={2}
className="mt-1 w-full min-w-[360px] resize-none rounded-xl border border-slate-200 bg-white/85 px-3 py-1.5 text-sm font-bold tracking-tight text-slate-800 outline-none focus:border-teal-400"
className="mt-1 w-full resize-none rounded-xl border border-slate-200 bg-white/85 px-3 py-1.5 text-sm font-bold tracking-tight text-slate-800 outline-none focus:border-teal-400"
/>
) : (
<h2 className="mt-1 whitespace-pre-line text-sm font-bold tracking-tight text-slate-800">{description}</h2>
@@ -752,54 +761,77 @@ function FlowRow({
onChange={(event) => onProgramNoteChange(event.target.value)}
rows={2}
placeholder="프로그램 노트를 입력하세요"
className="mt-2 w-full min-w-[360px] resize-none rounded-xl border border-amber-100 bg-amber-50/70 px-3 py-2 text-sm font-bold leading-5 text-slate-800 outline-none focus:border-amber-300"
className="mt-2 w-full resize-none rounded-xl border border-amber-100 bg-amber-50/70 px-3 py-2 text-sm font-bold leading-5 text-slate-800 outline-none focus:border-amber-300"
/>
) : programNote ? (
<div className="mt-2 max-w-3xl rounded-2xl bg-amber-50/80 px-3 py-2 text-sm font-semibold leading-5 text-amber-900 ring-1 ring-amber-100">
<div className="mt-2 rounded-2xl bg-amber-50/80 px-3 py-2 text-sm font-semibold leading-5 text-amber-900 ring-1 ring-amber-100">
<span className="mr-2 text-[11px] font-black uppercase tracking-wide text-amber-600">NOTE</span>
<span className="whitespace-pre-line">{programNote}</span>
</div>
) : null}
</div>
<div className="flex flex-col items-start gap-2 md:items-end">
<div className="grid w-full gap-1.5 md:pt-1">
{onFormatChange && (
<div className="flex items-center gap-2">
<span className="text-[11px] font-extrabold text-slate-400">포맷</span>
{isEditing ? (
<input
value={format}
onChange={(event) => onFormatChange(event.target.value)}
placeholder="예: glb"
className="h-8 w-28 rounded-full border border-slate-200 bg-white/85 px-3 text-[12px] font-extrabold text-slate-700 outline-none focus:border-teal-400"
/>
) : (
<span className="min-w-16 rounded-full bg-slate-100 px-3 py-1.5 text-center text-[12px] font-extrabold text-slate-700">
{format || '-'}
</span>
)}
<div className="grid w-full grid-cols-[62px_minmax(0,1fr)] items-center gap-2">
<span className="text-left text-[11px] font-extrabold text-slate-400">포맷</span>
<div className="flex flex-wrap justify-start gap-1.5">
{isEditing ? (
<input
value={format}
onChange={(event) => onFormatChange(event.target.value)}
placeholder="예: glb"
className="h-8 w-32 rounded-full border border-slate-200 bg-white/85 px-3 text-[12px] font-extrabold text-slate-700 outline-none focus:border-teal-400"
/>
) : (
<span className="min-w-16 rounded-full bg-slate-100 px-3 py-1.5 text-center text-[12px] font-extrabold text-slate-700">
{format || '-'}
</span>
)}
</div>
</div>
)}
{(deliverables.length > 0 || isEditing) && (
<div className="flex max-w-[720px] flex-wrap items-center justify-start gap-1.5 md:justify-end">
<span className={`mr-1 text-[11px] font-extrabold ${accent.labelText}`}>성과물</span>
{(deliverables.length > 0 ? deliverables : ['']).map((item, index) => (
isEditing ? (
<div className="grid w-full grid-cols-[62px_minmax(0,1fr)] items-start gap-2">
<span className={`pt-1 text-left text-[11px] font-extrabold ${accent.labelText}`}>성과물</span>
<div className="flex flex-wrap justify-start gap-1.5">
{(deliverables.length > 0 ? deliverables : ['']).map((item, index) => (
isEditing ? (
<input
key={index}
value={item}
onChange={(event) => onDeliverableChange?.(index, event.target.value)}
placeholder="성과물"
className="h-7 w-28 rounded-full border border-slate-200 bg-white/85 px-3 text-[11px] font-extrabold text-slate-700 outline-none focus:border-teal-400"
/>
) : splitDeliverableText(item).map((deliverable, deliverableIndex) => (
<span
key={`${deliverable}-${index}-${deliverableIndex}`}
className="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-extrabold text-slate-700"
>
{deliverable}
</span>
))
))}
</div>
</div>
)}
{onEngineChange && (
<div className="grid w-full grid-cols-[62px_minmax(0,1fr)] items-center gap-2">
<span className="text-left text-[11px] font-extrabold text-slate-400">사용엔진</span>
<div className="flex flex-wrap justify-start gap-1.5">
{isEditing ? (
<input
key={index}
value={item}
onChange={(event) => onDeliverableChange?.(index, event.target.value)}
placeholder="성과물"
className="h-7 w-28 rounded-full border border-slate-200 bg-white/85 px-3 text-[11px] font-extrabold text-slate-700 outline-none focus:border-teal-400"
value={engine}
onChange={(event) => onEngineChange(event.target.value)}
placeholder="예: hmeg, gsim"
className="h-8 w-40 rounded-full border border-slate-200 bg-white/85 px-3 text-[12px] font-extrabold text-slate-700 outline-none focus:border-teal-400"
/>
) : splitDeliverableText(item).map((deliverable, deliverableIndex) => (
<span
key={`${deliverable}-${index}-${deliverableIndex}`}
className="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-extrabold text-slate-700"
>
{deliverable}
</span>
))
))}
) : (
<span className="min-w-16 rounded-full bg-slate-100 px-3 py-1.5 text-center text-[12px] font-extrabold text-slate-700">
{engine || '-'}
</span>
)}
</div>
</div>
)}
{isEditing && onAddStep && (
@@ -1597,16 +1629,30 @@ function ProgramComparePopup({ programs, comparisons, initialPair, onComparisonS
</span>
</div>
<p className="mt-2 text-sm font-bold leading-5 text-slate-700">{program.description}</p>
<div className="mt-3 flex flex-wrap gap-1.5">
{(program.deliverables ?? []).map((deliverable) => (
<span key={deliverable} className="rounded-full bg-white/80 px-2.5 py-1 text-[11px] font-black text-slate-600 ring-1 ring-white">
{deliverable}
</span>
))}
<div className="mt-3 grid gap-2 text-[12px] font-extrabold text-slate-500">
<div className="grid grid-cols-[64px_minmax(0,1fr)] items-start gap-2">
<span className="text-right text-slate-400">포맷</span>
<span className="text-slate-800">{program.format || '-'}</span>
</div>
<div className="grid grid-cols-[64px_minmax(0,1fr)] items-start gap-2">
<span className="text-right text-slate-400">성과물</span>
<div className="flex flex-wrap gap-1.5">
{(program.deliverables ?? []).length > 0 ? (
(program.deliverables ?? []).map((deliverable) => (
<span key={deliverable} className="rounded-full bg-white/80 px-2.5 py-1 text-[11px] font-black text-slate-600 ring-1 ring-white">
{deliverable}
</span>
))
) : (
<span className="text-slate-800">-</span>
)}
</div>
</div>
<div className="grid grid-cols-[64px_minmax(0,1fr)] items-start gap-2">
<span className="text-right text-slate-400">사용엔진</span>
<span className="text-slate-800">{program.engine || '-'}</span>
</div>
</div>
<p className="mt-3 text-[12px] font-extrabold text-slate-500">
포맷: <span className="text-slate-800">{program.format || '-'}</span>
</p>
</div>
);
};
@@ -2083,8 +2129,7 @@ function RelationTreePanel({
const miniRowGap = fullPage ? 96 : 72;
const miniPadding = 34;
const miniSideLaneWidth = 96;
const sidebarSizes = [420, 560, 720];
const resolvedSidebarWidth = sidebarWidth ?? sidebarSizes[0];
const resolvedSidebarWidth = sidebarWidth ?? 420;
const miniViewportWidth = Math.max(290, fullPage ? viewportSize.width - 64 : resolvedSidebarWidth - 24);
const miniMaxLevelCount = Math.max(1, ...graphLevels.map((level) => level?.length ?? 0));
const miniGraphWidth = Math.max(
@@ -2138,11 +2183,6 @@ function RelationTreePanel({
const miniScaledWidth = miniGraphWidth * miniGraphScale;
const mapOffset = fullPage ? 0 : Math.max(0, Math.round((miniScaledWidth - miniViewportWidth) / 2 / miniGraphScale));
const mapTranslateX = fullPage ? Math.max(0, Math.round((miniViewportWidth - miniScaledWidth) / 2)) : -mapOffset;
const canShrinkSidebar = resolvedSidebarWidth > sidebarSizes[0];
const setSidebarSize = (size) => {
onSidebarWidthChange?.(size);
};
useEffect(() => {
if (!fullPage) return undefined;
const updateViewportSize = () => {
@@ -2332,29 +2372,19 @@ function RelationTreePanel({
)}
</div>
{!fullPage && (
<div className="flex items-center justify-between gap-2">
<div className="flex shrink-0 items-center gap-1">
<button
type="button"
disabled={!canShrinkSidebar}
onClick={() => setSidebarSize(sidebarSizes[0])}
className="flex h-7 w-7 items-center justify-center rounded-full bg-white text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
aria-label="연결도 영역 최소"
title="연결도 영역 최소"
>
<Minimize2 className="h-3.5 w-3.5" />
</button>
<button
type="button"
onClick={onOpenMapWindow}
className="flex h-7 w-7 items-center justify-center rounded-full bg-white text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
aria-label="연결도 새창으로 보기"
title="연결도 새창으로 보기"
>
<Maximize2 className="h-3.5 w-3.5" />
</button>
<div className="flex items-center justify-between gap-2">
<div className="flex shrink-0 items-center gap-1">
<button
type="button"
onClick={onOpenMapWindow}
className="flex h-7 w-7 items-center justify-center rounded-full bg-white text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50"
aria-label="연결도 새창으로 보기"
title="연결도 새창으로 보기"
>
<Maximize2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
</div>
)}
</div>
{relations.length > 0 ? (
@@ -2655,6 +2685,7 @@ export default function App() {
programNote: content.cheonjiin.programNote ?? '',
steps: editableCheonjiinFlow,
format: content.cheonjiin.format,
engine: content.cheonjiin.engine ?? '',
programType: getProgramType(content.cheonjiin.programType),
predecessors: content.cheonjiin.predecessors ?? [],
successors: content.cheonjiin.successors ?? [],
@@ -2673,6 +2704,7 @@ export default function App() {
programNote: content.wayPrimal.programNote ?? '',
steps: editableWayPrimalFlow,
format: content.wayPrimal.format,
engine: content.wayPrimal.engine ?? '',
programType: getProgramType(content.wayPrimal.programType),
predecessors: content.wayPrimal.predecessors ?? [],
successors: content.wayPrimal.successors ?? [],
@@ -2691,6 +2723,7 @@ export default function App() {
programNote: program.programNote ?? '',
steps: normalizeProgramSteps(program),
format: program.format,
engine: program.engine ?? '',
programType: getProgramType(program.programType),
predecessors: program.predecessors ?? [],
successors: program.successors ?? [],
@@ -3455,6 +3488,7 @@ export default function App() {
}
],
format: '',
engine: '',
deliverables: ['성과물'],
programType: 'internal',
programNote: '',
@@ -3709,7 +3743,17 @@ export default function App() {
return (
<main className="min-h-screen bg-white text-slate-900">
<div className="mx-auto max-w-[1760px] space-y-4 px-3 py-4">
<div className="flex justify-end gap-2">
<div className="sticky top-0 z-50 -mx-3 flex justify-end gap-2 border-b border-slate-100 bg-white/95 px-3 py-3 backdrop-blur">
{isEditing && (
<button
type="button"
onClick={addProgram}
className="flex items-center gap-2 rounded-full bg-blue-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-blue-700"
>
<Plus className="h-4 w-4" />
프로그램 추가
</button>
)}
<button
type="button"
onClick={toggleProgramEdit}
@@ -3748,26 +3792,6 @@ export default function App() {
/>
<section className="min-w-0 space-y-5">
{isEditing && (
<div className="flex items-center justify-between rounded-[24px] border border-white/70 bg-white/70 px-5 py-4 shadow-sm backdrop-blur">
<div>
<p className="text-[12px] font-extrabold uppercase tracking-wide text-blue-700">
프로그램 편집
</p>
<h2 className="mt-1 text-lg font-extrabold text-slate-950">
프로그램을 추가하거나 기존 내용을 수정합니다
</h2>
</div>
<button
type="button"
onClick={addProgram}
className="flex items-center gap-2 rounded-full bg-blue-600 px-4 py-2 text-sm font-bold text-white shadow-sm hover:bg-blue-700"
>
<Plus className="h-4 w-4" />
프로그램 추가
</button>
</div>
)}
<FlowRow
label={content.cheonjiin.name}
@@ -3785,6 +3809,8 @@ export default function App() {
onProgramTypeChange={(value) => updateProgramTitle('cheonjiin', 'programType', value)}
format={content.cheonjiin.format}
onFormatChange={(value) => updateFormat('cheonjiin', value)}
engine={content.cheonjiin.engine ?? ''}
onEngineChange={(value) => updateProgramTitle('cheonjiin', 'engine', value)}
deliverables={content.cheonjiin.deliverables}
onDeliverableChange={(index, value) => updateProgramDeliverable('cheonjiin', index, value)}
onAddStep={() => addStep('cheonjiin')}
@@ -3819,6 +3845,8 @@ export default function App() {
onProgramTypeChange={(value) => updateProgramTitle('wayPrimal', 'programType', value)}
format={content.wayPrimal.format}
onFormatChange={(value) => updateFormat('wayPrimal', value)}
engine={content.wayPrimal.engine ?? ''}
onEngineChange={(value) => updateProgramTitle('wayPrimal', 'engine', value)}
deliverables={content.wayPrimal.deliverables ?? []}
onDeliverableChange={(index, value) => updateProgramDeliverable('wayPrimal', index, value)}
onAddStep={() => addStep('wayPrimal')}
@@ -3907,6 +3935,8 @@ export default function App() {
onProgramTypeChange={(value) => updateProgram(programIndex, 'programType', value)}
format={program.format}
onFormatChange={(value) => updateProgram(programIndex, 'format', value)}
engine={program.engine ?? ''}
onEngineChange={(value) => updateProgram(programIndex, 'engine', value)}
deliverables={program.deliverables ?? []}
onDeliverableChange={(index, value) => updateProgramDeliverable(program.id, index, value)}
onAddStep={() => addStep(program.id)}