Add program type classification
This commit is contained in:
82
src/App.jsx
82
src/App.jsx
@@ -176,6 +176,25 @@ const contentStorageKey = 'program-flow-content';
|
||||
const programStateStorageKey = 'program-flow-gates';
|
||||
const serverStateEndpoint = '/api/state';
|
||||
const disabledFlowStep = '__disabled__';
|
||||
const programTypeOptions = {
|
||||
internal: {
|
||||
label: '사내프로그램',
|
||||
shortLabel: '사내',
|
||||
rowClass: 'border-emerald-100 bg-emerald-50/55',
|
||||
badgeClass: 'bg-emerald-100 text-emerald-700 ring-emerald-200',
|
||||
dotClass: 'bg-emerald-500'
|
||||
},
|
||||
commercial: {
|
||||
label: '상용프로그램',
|
||||
shortLabel: '상용',
|
||||
rowClass: 'border-violet-100 bg-violet-50/55',
|
||||
badgeClass: 'bg-violet-100 text-violet-700 ring-violet-200',
|
||||
dotClass: 'bg-violet-500'
|
||||
}
|
||||
};
|
||||
|
||||
const getProgramType = (type) => (type === 'commercial' ? 'commercial' : 'internal');
|
||||
const getProgramTypeMeta = (type) => programTypeOptions[getProgramType(type)];
|
||||
|
||||
const defaultContent = {
|
||||
cheonjiin: {
|
||||
@@ -184,6 +203,7 @@ const defaultContent = {
|
||||
steps: cheonjiinFlow.map(({ title, feature, note }) => ({ title, feature, note })),
|
||||
deliverables: cheonjiinDeliverables,
|
||||
format: 'glb',
|
||||
programType: 'internal',
|
||||
predecessors: [],
|
||||
successors: []
|
||||
},
|
||||
@@ -193,6 +213,7 @@ const defaultContent = {
|
||||
steps: wayPrimalFlow.map(({ title, feature, note }) => ({ title, feature, note })),
|
||||
format: '',
|
||||
deliverables: ['기본설계 모델'],
|
||||
programType: 'internal',
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
linkLabel: '천지인 산출 모델을 WayPrimal 설계 입력으로 연계'
|
||||
@@ -206,6 +227,7 @@ function normalizeStoredContent(parsed) {
|
||||
cheonjiin: {
|
||||
...defaultContent.cheonjiin,
|
||||
...(parsed.cheonjiin ?? {}),
|
||||
programType: getProgramType(parsed.cheonjiin?.programType ?? defaultContent.cheonjiin.programType),
|
||||
predecessors: parsed.cheonjiin?.predecessors ?? [],
|
||||
successors: parsed.cheonjiin?.successors ?? [],
|
||||
format:
|
||||
@@ -216,6 +238,7 @@ function normalizeStoredContent(parsed) {
|
||||
wayPrimal: {
|
||||
...defaultContent.wayPrimal,
|
||||
...(parsed.wayPrimal ?? {}),
|
||||
programType: getProgramType(parsed.wayPrimal?.programType ?? defaultContent.wayPrimal.programType),
|
||||
predecessors: parsed.wayPrimal?.predecessors ?? [],
|
||||
successors: parsed.wayPrimal?.successors ?? [],
|
||||
format:
|
||||
@@ -225,6 +248,7 @@ function normalizeStoredContent(parsed) {
|
||||
},
|
||||
extraPrograms: (parsed.extraPrograms ?? []).map((program, index, programs) => ({
|
||||
...program,
|
||||
programType: getProgramType(program.programType),
|
||||
predecessors: program.predecessors ?? [],
|
||||
successors: program.successors ?? [],
|
||||
linkLabel: program.linkLabel ?? `이전 프로그램 산출물을 ${program.name} 입력으로 연계`
|
||||
@@ -508,6 +532,8 @@ function FlowRow({
|
||||
onDeliverableChange,
|
||||
onLabelChange,
|
||||
onDescriptionChange,
|
||||
programType,
|
||||
onProgramTypeChange,
|
||||
onAddStep,
|
||||
onRemoveStep,
|
||||
onMoveStep,
|
||||
@@ -523,6 +549,8 @@ function FlowRow({
|
||||
return 'disabled';
|
||||
};
|
||||
const isRowClickable = Boolean(onLabelClick) && !isEditing;
|
||||
const programTypeKey = getProgramType(programType);
|
||||
const programTypeMeta = getProgramTypeMeta(programTypeKey);
|
||||
const maxVisibleSteps = 4;
|
||||
const [stepWindowStart, setStepWindowStart] = useState(0);
|
||||
const maxWindowStart = Math.max(0, steps.length - maxVisibleSteps);
|
||||
@@ -591,7 +619,7 @@ function FlowRow({
|
||||
return (
|
||||
<section
|
||||
onClick={isRowClickable ? onLabelClick : undefined}
|
||||
className={`rounded-[28px] border border-white/70 bg-white/75 p-5 shadow-[0_18px_50px_rgba(15,23,42,0.08)] backdrop-blur ${
|
||||
className={`rounded-[28px] border p-5 shadow-[0_18px_50px_rgba(15,23,42,0.08)] backdrop-blur ${programTypeMeta.rowClass} ${
|
||||
isRowClickable ? 'cursor-pointer transition hover:-translate-y-0.5 hover:shadow-[0_22px_60px_rgba(15,23,42,0.12)]' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -628,6 +656,31 @@ function FlowRow({
|
||||
프로그램 삭제
|
||||
</button>
|
||||
)}
|
||||
{isEditing && onProgramTypeChange ? (
|
||||
<div className="flex rounded-full bg-white/80 p-1 text-[11px] font-black shadow-sm ring-1 ring-slate-200">
|
||||
{Object.entries(programTypeOptions).map(([typeKey, typeMeta]) => (
|
||||
<button
|
||||
key={typeKey}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onProgramTypeChange(typeKey);
|
||||
}}
|
||||
className={`rounded-full px-2.5 py-1 transition ${
|
||||
programTypeKey === typeKey
|
||||
? typeMeta.badgeClass
|
||||
: 'text-slate-400 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{typeMeta.shortLabel}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className={`rounded-full px-2.5 py-1 text-[11px] font-black ring-1 ${programTypeMeta.badgeClass}`}>
|
||||
{programTypeMeta.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isEditing && onDescriptionChange ? (
|
||||
<textarea
|
||||
@@ -1285,10 +1338,11 @@ function RelationPopup({ programs, onToggleRelation, onClose }) {
|
||||
{graphPrograms.map((program) => {
|
||||
const position = nodePositions[program.id];
|
||||
if (!position) return null;
|
||||
const typeMeta = getProgramTypeMeta(program.programType);
|
||||
return (
|
||||
<div
|
||||
key={program.id}
|
||||
className="absolute flex flex-col items-center justify-center rounded-2xl border border-blue-100 bg-gradient-to-br from-white to-blue-50 px-3 text-center shadow-sm"
|
||||
className={`absolute flex flex-col items-center justify-center rounded-2xl border px-3 text-center shadow-sm ${typeMeta.rowClass}`}
|
||||
style={{
|
||||
left: `${(position.x / graphWidth) * 100}%`,
|
||||
top: `${(position.y / graphHeight) * 100}%`,
|
||||
@@ -1297,6 +1351,9 @@ function RelationPopup({ programs, onToggleRelation, onClose }) {
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-extrabold text-slate-950">{program.name}</p>
|
||||
<span className={`mt-1 rounded-full px-2 py-0.5 text-[10px] font-black ring-1 ${typeMeta.badgeClass}`}>
|
||||
{typeMeta.shortLabel}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1420,6 +1477,7 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup }) {
|
||||
const isExpanded = expandedProgramIds.has(program.id);
|
||||
const isRepeated = visitedIds.has(program.id);
|
||||
const hasMultipleInputs = (program.predecessors ?? []).length > 1;
|
||||
const typeMeta = getProgramTypeMeta(program.programType);
|
||||
const nextVisitedIds = new Set(visitedIds);
|
||||
nextVisitedIds.add(program.id);
|
||||
|
||||
@@ -1454,13 +1512,19 @@ function RelationTreePanel({ programs, onProgramClick, onOpenRelationPopup }) {
|
||||
onClick={() => onProgramClick(program.id)}
|
||||
className="min-w-0 flex-1 overflow-hidden rounded-xl bg-white/85 px-3 py-2 text-left shadow-sm ring-1 ring-slate-100 transition hover:-translate-y-0.5 hover:bg-white hover:shadow"
|
||||
>
|
||||
<span className="block truncate text-[13px] font-black text-slate-900">
|
||||
{program.name}
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<span className={`h-2 w-2 shrink-0 rounded-full ${typeMeta.dotClass}`} />
|
||||
<span className="block truncate text-[13px] font-black text-slate-900">
|
||||
{program.name}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[11px] font-semibold text-slate-500">
|
||||
{program.description}
|
||||
</span>
|
||||
</button>
|
||||
<span className={`rounded-full px-2 py-1 text-[10px] font-black ring-1 ${typeMeta.badgeClass}`}>
|
||||
{typeMeta.shortLabel}
|
||||
</span>
|
||||
{hasMultipleInputs && (
|
||||
<span className="rounded-full bg-violet-50 px-2 py-1 text-[10px] font-black text-violet-600 ring-1 ring-violet-100">
|
||||
다중
|
||||
@@ -1521,6 +1585,7 @@ export default function App() {
|
||||
description: content.cheonjiin.description,
|
||||
steps: editableCheonjiinFlow,
|
||||
format: content.cheonjiin.format,
|
||||
programType: getProgramType(content.cheonjiin.programType),
|
||||
predecessors: content.cheonjiin.predecessors ?? [],
|
||||
successors: content.cheonjiin.successors ?? [],
|
||||
accent: {
|
||||
@@ -1536,6 +1601,7 @@ export default function App() {
|
||||
description: content.wayPrimal.description,
|
||||
steps: editableWayPrimalFlow,
|
||||
format: content.wayPrimal.format,
|
||||
programType: getProgramType(content.wayPrimal.programType),
|
||||
predecessors: content.wayPrimal.predecessors ?? [],
|
||||
successors: content.wayPrimal.successors ?? [],
|
||||
accent: {
|
||||
@@ -1551,6 +1617,7 @@ export default function App() {
|
||||
description: program.description,
|
||||
steps: normalizeProgramSteps(program),
|
||||
format: program.format,
|
||||
programType: getProgramType(program.programType),
|
||||
predecessors: program.predecessors ?? [],
|
||||
successors: program.successors ?? [],
|
||||
accent: {
|
||||
@@ -1939,6 +2006,7 @@ export default function App() {
|
||||
],
|
||||
format: '',
|
||||
deliverables: ['성과물'],
|
||||
programType: 'internal',
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
linkLabel: '이전 프로그램 산출물을 새 프로그램 입력으로 연계'
|
||||
@@ -2202,6 +2270,8 @@ export default function App() {
|
||||
onStepChange={(index, field, value) => updateStep('cheonjiin', index, field, value)}
|
||||
onLabelChange={(value) => updateProgramTitle('cheonjiin', 'name', value)}
|
||||
onDescriptionChange={(value) => updateProgramTitle('cheonjiin', 'description', value)}
|
||||
programType={content.cheonjiin.programType}
|
||||
onProgramTypeChange={(value) => updateProgramTitle('cheonjiin', 'programType', value)}
|
||||
format={content.cheonjiin.format}
|
||||
onFormatChange={(value) => updateFormat('cheonjiin', value)}
|
||||
deliverables={content.cheonjiin.deliverables}
|
||||
@@ -2244,6 +2314,8 @@ export default function App() {
|
||||
onStepChange={(index, field, value) => updateStep('wayPrimal', index, field, value)}
|
||||
onLabelChange={(value) => updateProgramTitle('wayPrimal', 'name', value)}
|
||||
onDescriptionChange={(value) => updateProgramTitle('wayPrimal', 'description', value)}
|
||||
programType={content.wayPrimal.programType}
|
||||
onProgramTypeChange={(value) => updateProgramTitle('wayPrimal', 'programType', value)}
|
||||
format={content.wayPrimal.format}
|
||||
onFormatChange={(value) => updateFormat('wayPrimal', value)}
|
||||
deliverables={content.wayPrimal.deliverables ?? []}
|
||||
@@ -2333,6 +2405,8 @@ export default function App() {
|
||||
onStepChange={(index, field, value) => updateProgramStep(programIndex, index, field, value)}
|
||||
onLabelChange={(value) => updateProgramTitle(program.id, 'name', value)}
|
||||
onDescriptionChange={(value) => updateProgramTitle(program.id, 'description', value)}
|
||||
programType={program.programType}
|
||||
onProgramTypeChange={(value) => updateProgram(programIndex, 'programType', value)}
|
||||
format={program.format}
|
||||
onFormatChange={(value) => updateProgram(programIndex, 'format', value)}
|
||||
deliverables={program.deliverables ?? []}
|
||||
|
||||
Reference in New Issue
Block a user