{% extends "base.html" %}

{% block title %}프로젝트 정보{% endblock %}

{% block head_extra %}
<style>
    .page-hero {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        gap: 18px;
        flex-wrap: wrap;
    }

    .hero-copy {
        display: grid;
        gap: 8px;
    }

    .hero-copy p {
        color: var(--muted);
        line-height: 1.7;
        max-width: 920px;
    }

    .top-actions {
        display: flex;
        gap: 8px;
        align-items: center;
        flex-wrap: wrap;
    }

    .overview-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        margin-bottom: 10px;
    }

    .overview-title-row {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        flex-wrap: wrap;
    }

    .overview-year-select {
        min-width: 156px;
    }

    .metric-grid {
        display: grid;
        grid-template-columns: 1fr;
        gap: 8px;
        margin-top: 8px;
    }

    .overview-row {
        display: grid;
        grid-template-columns: minmax(230px, 0.23fr) minmax(0, 0.77fr);
        gap: 16px;
        align-items: stretch;
    }

    .metric-note-grid {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 14px;
        margin-top: 14px;
    }

    .metric-note {
        background: rgba(255, 255, 255, 0.92);
        border: 1px solid var(--line);
        border-radius: 16px;
        padding: 14px 16px;
        color: var(--muted);
        line-height: 1.7;
        font-size: 14px;
    }

    .metric-note strong {
        color: var(--ink);
    }

    .chart-stage {
        background:
            linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 247, 249, 0.98)),
            radial-gradient(circle at top left, rgba(17, 17, 17, 0.045), transparent 34%);
        border: 1px solid var(--line);
        border-radius: 16px;
        padding: 16px;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
    }

    .chart-header {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        gap: 14px;
        margin-bottom: 10px;
        flex-wrap: wrap;
    }

    .chart-header p {
        color: var(--muted);
        line-height: 1.6;
        font-size: 14px;
    }

    .chart-legend {
        display: flex;
        flex-wrap: wrap;
        gap: 10px 14px;
        align-items: center;
    }

    .legend-item {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        color: #363b44;
        font-size: 13px;
        font-weight: 700;
        background: rgba(255, 255, 255, 0.9);
        border: 1px solid var(--line);
        border-radius: 10px;
        padding: 6px 10px;
        line-height: 1.3;
    }

    .legend-swatch {
        width: 10px;
        height: 10px;
        border-radius: 999px;
        display: inline-block;
    }

    .chart-svg {
        width: 100%;
        height: auto;
        aspect-ratio: 1120 / 330;
        min-height: 290px;
        display: block;
    }

    .chart-note {
        margin-top: 10px;
        color: var(--muted);
        font-size: 13px;
        line-height: 1.7;
    }

    .search-layout {
        display: grid;
        gap: 16px;
    }

    .analysis-shell {
        display: grid;
        gap: 16px;
        overflow: visible;
    }

    .analysis-toolbar {
        display: grid;
        gap: 12px;
        align-items: start;
        position: relative;
        z-index: 120;
    }

    .analysis-main {
        display: grid;
        gap: 16px;
        overflow: visible;
    }

    .analysis-summary-list {
        display: none;
    }

    .toolbar-field {
        background: rgba(255, 255, 255, 0.95);
        border: 1px solid var(--line);
        border-radius: 16px;
        padding: 14px;
        position: relative;
        overflow: visible;
        z-index: 2;
    }

    .toolbar-head {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        margin-bottom: 10px;
    }

    .toolbar-title-group {
        display: flex;
        align-items: center;
        gap: 10px;
        flex-wrap: wrap;
        min-width: 0;
        flex: 1;
    }

    .toolbar-field h3 {
        font-size: 16px;
        margin-bottom: 0;
    }

    .toolbar-head-actions {
        display: inline-flex;
        align-items: center;
        gap: 8px;
    }

    .project-quick-link-bar {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        align-items: center;
        min-width: 0;
    }

    .project-quick-link {
        display: inline-flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        min-width: 0;
        max-width: 190px;
        padding: 8px 12px;
        border: 1px solid var(--line);
        border-radius: 999px;
        background: rgba(255, 255, 255, 0.96);
        color: var(--ink);
        font-size: 12px;
        font-weight: 700;
        line-height: 1.3;
        cursor: pointer;
        transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
    }

    .project-quick-link:hover {
        border-color: #c8d7e5;
        box-shadow: 0 10px 18px rgba(22, 33, 50, 0.08);
        transform: translateY(-1px);
    }

    .project-quick-link span {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .project-quick-link-remove {
        border: none;
        background: transparent;
        padding: 0;
        margin: 0;
        color: var(--muted);
        display: inline-flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        flex: 0 0 auto;
    }

    .project-quick-link-remove svg {
        width: 14px;
        height: 14px;
        stroke: currentColor;
        stroke-width: 2;
        fill: none;
    }

    .toggle-analysis-button svg {
        transition: transform 0.16s ease;
    }

    .toggle-analysis-button.is-open svg {
        transform: rotate(180deg);
    }

    .project-search-controls {
        display: grid;
        grid-template-columns: minmax(0, 1fr) 170px 210px;
        gap: 12px;
        align-items: end;
    }

    .project-search-field {
        position: relative;
        display: grid;
        gap: 8px;
        z-index: 180;
    }

    .project-search-dropdown {
        display: none;
        position: absolute;
        top: calc(100% + 8px);
        left: 0;
        right: 0;
        z-index: 4000;
        border: 1px solid var(--line);
        border-radius: 14px;
        background: rgba(255, 255, 255, 0.98);
        box-shadow: 0 14px 24px rgba(20, 24, 31, 0.08);
        overflow: hidden;
    }

    .project-search-dropdown.open {
        display: grid;
    }

    .analysis-content {
        display: grid;
        gap: 16px;
    }

    .analysis-content.is-hidden {
        display: none;
    }

    .explorer-list {
        display: grid;
        gap: 0;
        max-height: 280px;
        overflow: auto;
    }

    .explorer-item {
        display: grid;
        gap: 3px;
        border: 0;
        border-bottom: 1px solid #edf1f4;
        border-radius: 0;
        padding: 11px 14px;
        background: rgba(255, 255, 255, 0.98);
        cursor: pointer;
        transition: background 0.16s ease, color 0.16s ease;
        text-align: left;
    }

    .explorer-item.active {
        background: #f3f8fc;
    }

    .explorer-head {
        display: block;
    }

    .explorer-title {
        display: grid;
        gap: 4px;
    }

    .explorer-title strong {
        font-size: 14px;
        line-height: 1.45;
    }

    .explorer-title span {
        color: var(--muted);
        font-size: 12px;
        line-height: 1.5;
    }

    .explorer-tags {
        display: none;
    }

    .project-search-empty {
        padding: 11px 14px;
        color: var(--muted);
        font-size: 13px;
        text-align: left;
    }

    .mini-tag {
        display: inline-flex;
        align-items: center;
        border: 1px solid #dce5ed;
        border-radius: 999px;
        padding: 4px 8px;
        font-size: 11px;
        color: #35526a;
        background: #f8fbfe;
        white-space: nowrap;
    }

    .analysis-hero {
        display: grid;
        gap: 18px;
        align-items: stretch;
    }

    .analysis-hero-header {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 14px;
        align-items: start;
    }

    .analysis-title {
        display: grid;
        gap: 8px;
        min-width: 0;
        width: 100%;
    }

    .analysis-title h3 {
        font-size: 28px;
        line-height: 1.2;
        letter-spacing: -0.03em;
    }

    .analysis-title p {
        color: var(--muted);
        font-size: 13px;
        line-height: 1.6;
    }

    .analysis-meta-tags {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        margin-top: 4px;
    }

    .analysis-stat-grid {
        display: grid;
        grid-template-columns: repeat(5, minmax(0, 1fr));
        gap: 12px;
    }

    .analysis-stat {
        border: 1px solid #dfe6ee;
        border-radius: 14px;
        background: rgba(255,255,255,0.94);
        padding: 14px 16px;
        display: grid;
        gap: 5px;
    }

    .analysis-stat span {
        color: var(--muted);
        font-size: 12px;
    }

    .analysis-stat strong {
        font-size: 22px;
        line-height: 1.2;
    }

    .analysis-panel {
        background: rgba(255,255,255,0.98);
        border: 1px solid var(--line);
        border-radius: 18px;
        padding: 18px;
        display: grid;
        gap: 14px;
    }

    .analysis-panel h4 {
        font-size: 16px;
    }

    .related-trigger-button {
        margin-top: 2px;
    }

    .analysis-related-bar {
        display: grid;
        gap: 10px;
    }

    .analysis-related-bar:empty {
        display: none;
    }

    .related-tag-row {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        align-items: center;
    }

    .related-tag-label {
        color: var(--muted);
        font-size: 12px;
        font-weight: 700;
    }

    .related-project-tag {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        border: 1px solid #d7e2eb;
        background: #f7fbfe;
        color: #29485f;
        border-radius: 999px;
        padding: 5px 10px;
        font-size: 12px;
        font-weight: 700;
        line-height: 1.2;
    }

    .related-project-tag button {
        width: 18px;
        height: 18px;
        min-width: 18px;
        padding: 0;
        border-radius: 999px;
        border: 0;
        background: rgba(41, 72, 95, 0.08);
        color: #29485f;
        box-shadow: none;
    }

    .related-project-tag button:hover {
        transform: none;
        background: rgba(41, 72, 95, 0.14);
    }

    .related-modal-card {
        width: min(680px, 100%);
        max-height: min(70vh, 760px);
        overflow: auto;
        background: #ffffff;
        border-radius: 16px;
        border: 1px solid var(--line);
        box-shadow: 0 24px 64px rgba(21, 24, 29, 0.18);
        padding: 14px;
        display: grid;
        gap: 12px;
    }

    .related-modal-head {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
    }

    .related-modal-title {
        font-size: 16px;
        font-weight: 800;
    }

    .related-modal-body {
        display: grid;
        gap: 10px;
    }

    .related-search-results {
        display: grid;
        gap: 0;
        border: 1px solid #e5ebf0;
        border-radius: 14px;
        overflow: hidden;
        background: #fff;
    }

    .related-search-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        padding: 11px 14px;
        border-top: 1px solid #eef2f5;
    }

    .related-search-item:first-child {
        border-top: 0;
    }

    .related-search-copy {
        display: grid;
        gap: 4px;
        text-align: left;
        min-width: 0;
    }

    .related-search-copy strong {
        font-size: 13px;
        line-height: 1.45;
    }

    .related-search-copy span {
        font-size: 12px;
        color: var(--muted);
        line-height: 1.45;
    }

    .comparison-main-row {
        cursor: pointer;
    }

    .comparison-main-row td:first-child {
        font-weight: 700;
    }

    .comparison-toggle {
        display: inline-flex;
        align-items: center;
        gap: 8px;
    }

    .comparison-chevron {
        width: 16px;
        height: 16px;
        display: inline-block;
        transition: transform 0.16s ease;
    }

    .comparison-main-row.is-open .comparison-chevron {
        transform: rotate(90deg);
    }

    .comparison-detail-row {
        display: none;
        background: #fbfcfd;
    }

    .comparison-detail-row.is-open {
        display: table-row;
    }

    .comparison-detail-cell {
        padding: 0;
    }

    .comparison-detail-box {
        padding: 12px 14px 14px;
        display: grid;
        gap: 10px;
    }

    .comparison-detail-grid {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 12px;
    }

    .comparison-detail-group {
        border: 1px solid #e5ebf0;
        border-radius: 12px;
        background: #fff;
        overflow: hidden;
    }

    .comparison-detail-group h5 {
        font-size: 12px;
        color: #547085;
        background: #f5f8fb;
        padding: 8px 10px;
    }

    .comparison-detail-list {
        display: grid;
        gap: 0;
    }

    .comparison-detail-split {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px;
    }

    .comparison-detail-split .comparison-detail-group {
        min-width: 0;
    }

    .comparison-detail-item {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 10px;
        padding: 8px 10px;
        border-top: 1px solid #eef2f5;
        font-size: 12px;
        text-align: left;
        align-items: center;
        min-height: 38px;
        box-sizing: border-box;
    }

    .comparison-detail-item:first-child {
        border-top: 0;
    }

    .comparison-detail-item span:first-child {
        color: var(--muted);
        line-height: 1.45;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .comparison-detail-item strong {
        font-size: 12px;
        text-align: right;
    }

    .comparison-detail-item.is-subtotal,
    .comparison-detail-item.is-total {
        background: #f8fafc;
        font-weight: 800;
    }

    .comparison-detail-item.is-total {
        background: #f3f6f9;
    }

    .comparison-detail-item.is-placeholder {
        visibility: hidden;
    }

    .comparison-detail-item.is-warning {
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 8px 10px;
        min-height: 38px;
    }

    .comparison-detail-warning-box {
        border: 2px solid #dc2626;
        border-radius: 12px;
        padding: 10px 14px;
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        text-align: center;
        gap: 4px;
        max-width: 100%;
        box-sizing: border-box;
        background: #fff;
    }

    .comparison-detail-warning-box span:first-child {
        color: #dc2626;
        font-size: 15px;
        font-weight: 900;
        line-height: 1.25;
    }

    .comparison-detail-warning-box strong {
        color: #dc2626;
        font-size: 12px;
        font-weight: 800;
        line-height: 1.35;
        text-align: center;
    }

    .comparison-project-block {
        display: grid;
        gap: 0;
    }

    .comparison-project-detail-area {
        display: grid;
        gap: 0;
        position: relative;
        align-content: start;
    }

    .comparison-project-detail-area.is-empty {
        position: relative;
    }

    .comparison-project-warning-anchor {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        pointer-events: none;
        padding: 8px 10px;
        box-sizing: border-box;
    }

    .analysis-kpis {
        display: grid;
        grid-template-columns: repeat(6, minmax(0, 1fr));
        gap: 10px;
        align-items: stretch;
    }

    .analysis-kpi {
        border: 1px solid #dfe6ee;
        border-radius: 14px;
        padding: 12px 10px;
        display: grid;
        gap: 4px;
        background: #fbfcfd;
        min-width: 0;
    }

    .analysis-kpi span {
        color: var(--muted);
        font-size: 12px;
    }

    .analysis-kpi strong {
        font-size: 17px;
        line-height: 1.2;
        word-break: break-word;
    }

    .analysis-table {
        width: 100%;
        border-collapse: collapse;
        table-layout: fixed;
    }

    .analysis-table th,
    .analysis-table td {
        padding: 10px 12px;
        border-bottom: 1px solid #e7edf2;
        font-size: 13px;
        text-align: left;
        vertical-align: middle;
        word-break: break-word;
    }

    .analysis-table th {
        font-size: 12px;
        color: #547085;
        background: #f5f8fb;
    }

    .comparison-note-input {
        width: 100%;
        min-height: 24px;
        padding: 6px 8px;
        border: 1px solid #d7dfe7;
        border-radius: 10px;
        background: #fff;
        font: inherit;
        color: var(--ink);
        resize: vertical;
    }

    .comparison-note-input::placeholder {
        color: #9aa8b5;
    }

    .delta-positive {
        color: #15803d;
        font-weight: 800;
    }

    .delta-negative {
        color: #c2410c;
        font-weight: 800;
    }

    .analysis-note-grid {
        display: grid;
        grid-template-columns: 1fr;
        gap: 10px;
    }

    .analysis-note-grid .detail-block h4 {
        display: none;
    }

    .analysis-note-grid .detail-block {
        padding-top: 16px;
    }

    .contract-state-badge {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 24px;
        padding: 3px 8px;
        border-radius: 999px;
        font-size: 11px;
        font-weight: 800;
        line-height: 1.2;
        width: fit-content;
    }

    .contract-state-badge.is-contracted {
        color: #155e75;
        background: #ecfeff;
        border: 1px solid #b6eff7;
    }

    .contract-state-badge.is-uncontracted {
        color: #7c2d12;
        background: #fff7ed;
        border: 1px solid #fed7aa;
    }

    .contract-state-badge.is-uncontracted-cost {
        color: #991b1b;
        background: #fef2f2;
        border: 1px solid #fecaca;
    }

    .uncontracted-dashboard {
        display: grid;
        gap: 16px;
    }

    #projectUncontractedDashboard {
        padding: 20px;
        position: relative;
        z-index: 1;
    }

    .analysis-root-panel {
        position: relative;
        z-index: 40;
        overflow: visible;
    }

    .project-uncontracted-panel {
        position: relative;
        z-index: 1;
    }

    .uncontracted-toolbar {
        display: grid;
        grid-template-columns: repeat(2, 180px) auto;
        gap: 12px;
        align-items: end;
    }

    .uncontracted-summary-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
        gap: 10px;
    }

    .uncontracted-stat {
        border: 1px solid #dfe6ee;
        border-radius: 14px;
        padding: 12px;
        background: #fbfcfd;
        display: grid;
        gap: 4px;
        text-align: left;
        align-content: start;
        justify-items: start;
        min-width: 0;
        min-height: 96px;
        box-sizing: border-box;
        box-shadow: none;
    }

    .uncontracted-stat.is-clickable {
        cursor: pointer;
        transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
    }

    .uncontracted-stat.is-clickable:hover,
    .uncontracted-stat.is-clickable.is-active {
        border-color: #c8d7e5;
        box-shadow: 0 12px 22px rgba(22, 33, 50, 0.08);
        transform: translateY(-1px);
    }

    .uncontracted-stat span {
        color: var(--muted);
        font-size: 12px;
        display: block;
        line-height: 1.35;
        white-space: normal;
        word-break: keep-all;
    }

    .uncontracted-stat strong {
        color: var(--ink);
        font-size: 20px;
        line-height: 1.2;
        word-break: break-word;
        display: block;
    }

    .uncontracted-stat em {
        color: var(--muted);
        font-size: 12px;
        font-style: normal;
        line-height: 1.4;
        font-weight: 700;
        text-align: left;
        display: block;
        white-space: normal;
        word-break: keep-all;
    }

    .uncontracted-stat-copy {
        display: grid;
        gap: 4px;
        min-width: 0;
        width: 100%;
        text-align: left;
        align-content: start;
        justify-items: start;
    }

    .uncontracted-detail-summary {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 10px;
        margin-bottom: 10px;
    }

    .uncontracted-dashboard-grid {
        display: grid;
        grid-template-columns: 1.2fr 1fr;
        gap: 12px;
    }

    .uncontracted-dashboard-grid .detail-block {
        height: 100%;
    }

    .compact-table {
        width: 100%;
        border-collapse: collapse;
        table-layout: fixed;
    }

    .compact-table th,
    .compact-table td {
        padding: 9px 10px;
        border-bottom: 1px solid #e7edf2;
        font-size: 12px;
        text-align: left;
        vertical-align: middle;
        word-break: break-word;
    }

    .compact-table th {
        font-size: 11px;
        color: #547085;
        background: #f5f8fb;
    }

    .compact-table td:last-child,
    .compact-table th:last-child {
        text-align: right;
    }

    .compact-table .classification-cell,
    .compact-table th.classification-cell {
        text-align: center;
        width: 180px;
    }

    .detail-classification-select {
        width: 100%;
        min-width: 0;
        padding: 8px 10px;
        font-size: 12px;
    }

    .compact-table .interactive-row {
        cursor: pointer;
    }

    .compact-table .interactive-row:hover {
        background: #f8fbfe;
    }

    .compact-table .interactive-row.is-active {
        background: #eef6fb;
    }

    .search-bar {
        display: grid;
        grid-template-columns: minmax(280px, 1fr) auto;
        gap: 12px;
        align-items: end;
    }

    .search-helper {
        color: var(--muted);
        font-size: 13px;
        line-height: 1.7;
    }

    .search-helper:empty,
    .empty-state:empty {
        display: none;
    }

    .result-list {
        display: grid;
        gap: 10px;
    }

    .result-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        background: rgba(255, 255, 255, 0.94);
        border: 1px solid var(--line);
        border-radius: 14px;
        padding: 14px 16px;
    }

    .result-title {
        display: grid;
        gap: 4px;
    }

    .result-title strong {
        font-size: 15px;
    }

    .result-title span {
        color: var(--muted);
        font-size: 13px;
    }

    .compare-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 16px;
    }

    .compare-card {
        position: relative;
        background:
            linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 248, 250, 0.96)),
            radial-gradient(circle at top right, rgba(17, 17, 17, 0.045), transparent 36%);
        border: 1px solid rgba(216, 222, 229, 0.95);
        border-radius: 16px;
        padding: 16px;
        box-shadow: 0 12px 24px rgba(21, 24, 29, 0.05);
    }

    .compare-head {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        gap: 12px;
        margin-bottom: 14px;
    }

    .compare-head h3 {
        font-size: 18px;
        line-height: 1.45;
        margin-bottom: 4px;
    }

    .compare-head p {
        color: var(--muted);
        font-size: 13px;
    }

    .remove-button {
        min-width: 32px;
        width: 32px;
        height: 32px;
        padding: 0;
        border-radius: 10px;
        background: #e7f0f5;
        color: #204257;
        box-shadow: none;
    }

    .remove-button:hover {
        background: #d8e7ef;
    }

    .remove-button svg {
        width: 14px;
        height: 14px;
        stroke: currentColor;
        fill: none;
        stroke-width: 1.8;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    .info-metrics {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 10px;
        margin-bottom: 14px;
    }

    .info-chip {
        background: rgba(248, 249, 251, 0.98);
        border: 1px solid var(--line);
        border-radius: 14px;
        padding: 12px 14px;
    }

    .info-chip .label {
        color: var(--muted);
        font-size: 12px;
        margin-bottom: 6px;
    }

    .info-chip .value {
        font-size: 20px;
        font-weight: 800;
        line-height: 1.2;
        word-break: break-word;
    }

    .detail-stack {
        display: grid;
        gap: 12px;
    }

    .detail-block {
        background: rgba(255, 255, 255, 0.94);
        border: 1px solid var(--line);
        border-radius: 16px;
        padding: 14px;
    }

    .detail-block h4 {
        font-size: 14px;
        margin-bottom: 10px;
    }

    .detail-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px 12px;
    }

    .detail-item {
        display: grid;
        gap: 4px;
    }

    .detail-item span {
        color: var(--muted);
        font-size: 12px;
    }

    .detail-item strong {
        font-size: 15px;
        line-height: 1.5;
        word-break: break-word;
    }

    .empty-state {
        padding: 28px 20px;
        text-align: center;
        color: var(--muted);
        background: rgba(255, 255, 255, 0.88);
        border: 1px dashed #c4ccd5;
        border-radius: 16px;
        line-height: 1.8;
    }

    .inline-actions {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
        align-items: center;
        margin-top: 12px;
    }

    .modal-backdrop {
        position: fixed;
        inset: 0;
        background: rgba(20, 33, 47, 0.5);
        display: none;
        align-items: center;
        justify-content: center;
        padding: 20px;
        z-index: 50;
    }

    .modal-backdrop.open {
        display: flex;
    }

    .modal-card {
        width: min(1280px, 100%);
        max-height: 90vh;
        overflow: auto;
        background: #ffffff;
        border-radius: 14px;
        border: 1px solid var(--line);
        box-shadow: 0 24px 64px rgba(21, 24, 29, 0.18);
        padding: 12px;
    }

    .modal-form {
        display: grid;
        gap: 12px;
    }

    .modal-card label {
        font-size: 11px;
    }

    .modal-card input[type="text"],
    .modal-card input[type="number"],
    .modal-card input[type="date"],
    .modal-card select,
    .modal-card textarea {
        padding: 6px 8px;
        border-radius: 8px;
        min-height: 34px;
        font-size: 12px;
        line-height: 1.2;
    }

    .modal-card textarea {
        min-height: 68px;
    }

    .form-section-block {
        border: 1px solid var(--line);
        border-radius: 12px;
        padding: 12px;
        background: rgba(250, 251, 252, 0.92);
        display: grid;
        gap: 10px;
    }

    .form-section-head {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 10px;
        flex-wrap: wrap;
    }

    .form-section-head h3 {
        font-size: 14px;
    }

    .section-help {
        color: var(--muted);
        font-size: 11px;
        line-height: 1.4;
    }

    .import-review-banner {
        display: none;
        align-items: center;
        gap: 10px;
        padding: 10px 12px;
        border-radius: 12px;
        background: #fff7e7;
        border: 1px solid #f2d188;
        color: #7b5708;
        font-size: 12px;
        line-height: 1.5;
    }

    .import-review-banner.is-visible {
        display: flex;
    }

    .import-review-banner strong {
        color: #5f4102;
    }

    .section-grid {
        display: grid;
        grid-template-columns: repeat(5, minmax(0, 1fr));
        gap: 8px;
    }

    .lookup-grid {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
    }

    .readonly-output {
        min-height: 34px;
        border: 1px solid var(--line);
        background: #f5f7fa;
        border-radius: 8px;
        padding: 7px 9px;
        display: flex;
        align-items: center;
        justify-content: flex-start;
        text-align: left;
        font-weight: 700;
        color: var(--ink);
        font-size: 12px;
        line-height: 1.25;
    }

    .lookup-wrap {
        position: relative;
    }

    .lookup-results {
        position: absolute;
        top: calc(100% + 6px);
        left: 0;
        right: 0;
        z-index: 2800;
        border: 1px solid var(--line);
        border-radius: 10px;
        background: #ffffff;
        box-shadow: 0 14px 30px rgba(21, 24, 29, 0.08);
        max-height: 240px;
        overflow: auto;
        display: none;
    }

    .lookup-results.open {
        display: grid;
        justify-items: stretch;
        align-items: start;
    }

    .lookup-option {
        padding: 8px 10px;
        border: none;
        background: transparent;
        display: grid;
        justify-items: start;
        align-items: start;
        text-align: left;
        color: var(--ink);
        cursor: pointer;
        border-bottom: 1px solid #eef1f4;
        white-space: normal;
        overflow-wrap: anywhere;
        line-height: 1.35;
        width: 100%;
    }

    .lookup-option:last-child {
        border-bottom: none;
    }

    .lookup-option strong,
    .lookup-option span {
        display: block;
        width: 100%;
        white-space: normal;
        overflow-wrap: anywhere;
        text-align: left;
    }

    .lookup-option strong {
        font-size: 11px;
        font-weight: 600;
        line-height: 1.35;
    }

    .lookup-option span {
        color: var(--muted);
        font-size: 10px;
        margin-top: 2px;
        line-height: 1.35;
    }

    #supportDeptCodeResults,
    #supportDeptNameResults {
        text-align: left;
    }

    #supportDeptCodeResults .lookup-option,
    #supportDeptNameResults .lookup-option {
        display: block;
        width: 100%;
        justify-self: stretch;
        text-align: left !important;
    }

    #supportDeptCodeResults .lookup-option strong,
    #supportDeptCodeResults .lookup-option span,
    #supportDeptNameResults .lookup-option strong,
    #supportDeptNameResults .lookup-option span {
        width: 100%;
        margin: 0;
        text-align: left !important;
    }

    .row-table-wrap {
        overflow: visible;
        border: 1px solid var(--line);
        border-radius: 10px;
        background: #ffffff;
        position: relative;
        z-index: 1;
    }

    .row-table {
        width: 100%;
        min-width: 0;
        border-collapse: collapse;
        table-layout: fixed;
    }

    .row-table th,
    .row-table td {
        padding: 6px 7px;
        border-bottom: 1px solid #eef1f4;
        font-size: 12px;
        vertical-align: top;
    }

    .row-table th {
        background: #f7f8fa;
        color: #525a66;
        font-size: 11px;
        font-weight: 700;
    }

    .row-table td .inline-input {
        width: 100%;
    }

    .row-table td .lookup-wrap {
        width: 100%;
    }

    .row-table .lookup-results {
        max-height: 150px;
    }

    .modal-col-dept,
    .modal-col-work,
    .modal-col-amount,
    .modal-col-remove {
        width: auto;
    }

    .modal-col-dept {
        width: 34%;
    }

    .modal-col-work {
        width: 28%;
    }

    .modal-col-amount {
        width: 28%;
    }

    .modal-col-remove {
        width: 10%;
    }

    .section-columns {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 8px;
        align-items: start;
    }

    .actual-input-layout {
        display: grid;
        gap: 8px;
    }

    .actual-input-top,
    .actual-input-bottom {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 8px;
        align-items: start;
    }

    .mini-sector {
        border: 1px solid #e6eaf0;
        border-radius: 10px;
        background: rgba(255, 255, 255, 0.96);
        padding: 10px;
        display: grid;
        gap: 8px;
        align-content: start;
        overflow: visible;
    }

    .mini-sector h4 {
        font-size: 12px;
        line-height: 1.35;
    }

    .row-remove-button {
        min-width: 16px;
        width: 16px;
        height: 16px;
        padding: 0;
        border-radius: 5px;
        box-shadow: none;
    }

    .row-remove-button svg {
        width: 10px;
        height: 10px;
        stroke: currentColor;
        fill: none;
        stroke-width: 2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    .row-table.compact {
        min-width: 100%;
    }

    .actual-labor-table col:nth-child(1) {
        width: 21%;
    }

    .actual-labor-table col:nth-child(2) {
        width: 33%;
    }

    .actual-labor-table col:nth-child(3) {
        width: 30%;
    }

    .actual-labor-table col:nth-child(4) {
        width: 8%;
    }

    .actual-labor-table td:last-child {
        text-align: right;
        vertical-align: middle;
    }

    .actual-labor-table .row-remove-button {
        margin-left: auto;
    }

    .actual-labor-table td:nth-child(2),
    .actual-labor-table td:nth-child(3) {
        min-width: 0;
    }

    .actual-labor-table .actual-labor-grade {
        width: 100%;
        max-width: 102px;
    }

    .actual-labor-table .actual-labor-amount {
        width: 100%;
        max-width: 118px;
        margin-left: 0;
    }

    .row-table.compact th,
    .row-table.compact td {
        padding: 6px;
    }

    .collection-footer {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: 8px;
    }

    .summary-chip {
        border: 1px solid #e6eaf0;
        border-radius: 10px;
        background: #ffffff;
        padding: 8px 10px;
        display: grid;
        gap: 3px;
        text-align: left;
        align-content: start;
        justify-items: start;
        box-sizing: border-box;
    }

    .summary-chip.is-clickable {
        cursor: pointer;
        transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
    }

    .summary-chip.is-clickable:hover {
        border-color: #c8d7e5;
        box-shadow: 0 12px 22px rgba(22, 33, 50, 0.08);
        transform: translateY(-1px);
    }

    .summary-chip span {
        color: var(--muted);
        font-size: 11px;
        text-align: left;
    }

    .summary-chip strong {
        font-size: 13px;
        text-align: left;
        line-height: 1.25;
        word-break: break-word;
    }

    .collection-contract-mini {
        font-size: 11px;
        font-weight: 800;
        letter-spacing: -0.02em;
        line-height: 1.2;
        white-space: normal;
        word-break: break-word;
    }

    .amount-input {
        text-align: right;
        font-variant-numeric: tabular-nums;
    }

    .collection-entry-list {
        display: grid;
        gap: 8px;
    }

    .collection-entry-card {
        border: 1px solid #e6eaf0;
        border-radius: 12px;
        background: linear-gradient(180deg, #ffffff, #f9fafb);
        padding: 10px;
        display: grid;
        gap: 8px;
    }

    .collection-entry-grid {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: 8px;
    }

    .collection-field {
        display: grid;
        gap: 4px;
        min-width: 0;
    }

    .collection-field label {
        font-size: 10px;
        color: var(--muted);
        font-weight: 700;
        line-height: 1.2;
    }

    .collection-field.contract-field,
    .collection-field.balance-field,
    .collection-field.amount-field {
        min-width: 0;
    }

    .section-chip {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 6px 10px;
        border-radius: 999px;
        border: 1px solid #dbe2ea;
        background: #ffffff;
        color: var(--ink);
        font-size: 12px;
        font-weight: 700;
        white-space: nowrap;
    }

    .section-chip strong {
        font-variant-numeric: tabular-nums;
    }

    .collection-field .readonly-output,
    .collection-field .inline-input {
        min-width: 0;
        width: 100%;
    }

    .table-tools {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 8px;
        flex-wrap: wrap;
    }

    .table-tools-actions {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        flex-wrap: nowrap;
        justify-content: flex-end;
    }

    .table-tools-amount {
        width: 132px;
        max-width: 132px;
        min-width: 132px;
    }

    .table-total {
        color: var(--muted);
        font-size: 11px;
        font-weight: 700;
    }

    .mini-button {
        padding: 5px 8px;
        font-size: 11px;
        box-shadow: none;
    }

    .mini-button.button-icon {
        width: 28px;
        min-width: 28px;
        height: 28px;
        padding: 0;
        border-radius: 9px;
    }

    .mini-button.button-icon svg {
        width: 14px;
        height: 14px;
        stroke: currentColor;
        fill: none;
        stroke-width: 2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    .ghost-button {
        background: #ffffff;
        color: #1b1d21;
        border-color: var(--line);
    }

    .danger-lite {
        background: #fff5f5;
        color: #a93a3a;
        border-color: #f0c5c5;
    }

    .stacked-note {
        color: var(--muted);
        font-size: 12px;
        line-height: 1.5;
    }

    .metric-grid .stat-card {
        min-height: 78px;
        padding: 11px 12px 10px;
    }

    .metric-grid .stat-card .value {
        font-size: clamp(14px, 1.2vw, 22px);
        line-height: 1.15;
    }

    .metric-grid .stat-card .label {
        font-size: 11px;
        margin-bottom: 4px;
    }

    .metric-grid .stat-card .meta {
        font-size: 11px;
        display: none;
    }

    .modal-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 12px;
        position: sticky;
        top: -12px;
        z-index: 4;
        margin: -12px -12px 8px;
        padding: 9px 12px;
        background: rgba(255, 255, 255, 0.98);
        border-bottom: 1px solid #e4e9ef;
        backdrop-filter: blur(10px);
    }

    .modal-title {
        font-size: 16px;
        line-height: 1.2;
        letter-spacing: -0.02em;
    }

    .modal-header-actions {
        display: inline-flex;
        align-items: center;
        gap: 4px;
    }

    .close-button {
        background: #e7f0f5;
        color: #204257;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 40px;
        min-width: 40px;
        height: 40px;
        padding: 0;
        border-radius: 12px;
    }

    .close-button svg {
        width: 16px;
        height: 16px;
        stroke: currentColor;
        fill: none;
        stroke-width: 2.2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    .actual-time-grid {
        display: inline-grid;
        grid-template-columns: 52px 22px 34px 14px;
        gap: 4px;
        align-items: center;
        justify-content: start;
        width: auto;
        max-width: 100%;
    }

    .actual-time-grid .inline-input {
        min-width: 0;
        text-align: right;
        font-variant-numeric: tabular-nums;
        padding-left: 6px;
        padding-right: 6px;
    }

    .actual-time-unit {
        font-size: 11px;
        color: var(--muted);
        font-weight: 600;
        white-space: nowrap;
        text-align: left;
    }

    .muted {
        color: var(--muted);
        font-size: 12px;
    }

    @media (max-width: 1180px) {
        .overview-row,
        .analysis-toolbar,
        .project-search-controls,
        .analysis-kpis,
        .analysis-note-grid,
        .comparison-detail-grid,
        .metric-grid,
        .metric-note-grid,
        .compare-grid,
        .search-bar,
        .detail-grid,
        .info-metrics,
        .section-grid,
        .lookup-grid,
        .section-columns,
        .collection-footer,
        .collection-entry-grid {
            grid-template-columns: 1fr;
        }

        .analysis-hero-header,
        .analysis-stat-grid,
        .uncontracted-summary-grid,
        .uncontracted-dashboard-grid,
        .uncontracted-toolbar {
            grid-template-columns: 1fr;
        }

    }
</style>
{% endblock %}

{% block content %}
<section class="panel analysis-root-panel">
    <div class="analysis-shell">
        <div class="analysis-main">
            <section class="analysis-toolbar">
                <div class="toolbar-field">
                    <div class="toolbar-head">
                        <div class="toolbar-title-group">
                            <h3>프로젝트 검색</h3>
                            <div class="project-quick-link-bar" id="projectQuickLinkBar"></div>
                        </div>
                        <div class="toolbar-head-actions">
                            <button type="button" id="saveProjectPageState" class="button-icon button-secondary" title="페이지 입력사항 저장" aria-label="페이지 입력사항 저장">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M6 4h10l4 4v12H6z"></path>
                                    <path d="M9 4v6h6V4"></path>
                                    <path d="M9 20v-6h6v6"></path>
                                </svg>
                                <span class="sr-only">페이지 입력사항 저장</span>
                            </button>
                            <button type="button" id="openRelatedProjectToolbar" class="button-icon button-secondary" title="연관 프로젝트 검색" aria-label="연관 프로젝트 검색">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M10.8 13.2l2.4-2.4"></path>
                                    <path d="M7.5 14.7l-1.6 1.6a3 3 0 1 1-4.2-4.2l3-3a3 3 0 0 1 4.2 0"></path>
                                    <path d="M16.5 9.3l1.6-1.6a3 3 0 1 1 4.2 4.2l-3 3a3 3 0 0 1-4.2 0"></path>
                                </svg>
                                <span class="sr-only">연관 프로젝트 검색</span>
                            </button>
                            <button type="button" id="toggleProjectAnalysis" class="button-icon button-secondary toggle-analysis-button" title="프로젝트 상세 열기/숨기기" aria-label="프로젝트 상세 열기/숨기기">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M6 9l6 6 6-6"></path>
                                </svg>
                                <span class="sr-only">프로젝트 상세 열기/숨기기</span>
                            </button>
                            <button type="button" id="openProjectModal" class="button-icon" title="사업현황 추가/수정" aria-label="사업현황 추가/수정">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">사업현황 추가/수정</span>
                            </button>
                        </div>
                    </div>
                    <div class="project-search-controls">
                        <div class="project-search-field">
                            <input id="projectCostSearch" type="text" placeholder="지원부서코드 또는 사업명을 입력하세요." autocomplete="off">
                            <div class="project-search-dropdown" id="projectSearchDropdown">
                                <div class="explorer-list" id="projectExplorerList"></div>
                                <div class="project-search-empty" id="projectExplorerEmpty"></div>
                            </div>
                        </div>
                        <div class="field">
                            <select id="projectCostYearFilter" aria-label="연도 필터">
                                <option value="">전체 연도</option>
                                {% for year in project_year_options %}
                                    <option value="{{ year }}">{{ year }}</option>
                                {% endfor %}
                            </select>
                        </div>
                        <div class="field">
                            <select id="projectContractFilter" aria-label="계약 상태 필터">
                                <option value="">전체 상태</option>
                                <option value="contracted">계약 진행</option>
                                <option value="uncontracted">미계약</option>
                                <option value="uncontracted_cost">미계약 비용 발생</option>
                            </select>
                        </div>
                    </div>
                </div>
            </section>
            <div class="analysis-content is-hidden" id="projectAnalysisContent">
                <section class="analysis-panel analysis-related-bar" id="projectRelatedBar"></section>
                <section class="analysis-panel" id="projectAnalysisHero"></section>
                <section class="analysis-panel" id="projectAnalysisMetrics"></section>
                <section class="analysis-panel" id="projectAnalysisComparison"></section>
                <section class="analysis-panel" id="projectAnalysisNotes"></section>
            </div>
        </div>
    </div>
</section>

<section class="panel project-uncontracted-panel" id="projectUncontractedDashboard"></section>

<div class="modal-backdrop {% if project_edit.support_dept_code %}open{% endif %}" id="projectModal">
    <div class="modal-card">
        <form action="javascript:void(0)" method="post" class="modal-form" id="projectStatusForm" data-collab-form onsubmit="return false;">
            <div class="modal-header">
                <h2 class="modal-title">사업현황 추가/수정</h2>
                <div class="modal-header-actions">
                    <button type="button" id="saveProjectModal" class="button-icon" title="사업현황 저장" aria-label="사업현황 저장">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M5 12l5 5 9-10"></path>
                        </svg>
                        <span class="sr-only">사업현황 저장</span>
                    </button>
                    <button type="button" class="close-button button-secondary button-icon" id="closeProjectModal" title="닫기" aria-label="닫기">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M7 7l10 10"></path>
                            <path d="M17 7L7 17"></path>
                        </svg>
                        <span class="sr-only">닫기</span>
                    </button>
                </div>
            </div>
            <input type="hidden" name="selected_year" value="{{ selected_year or '' }}">
            <input type="hidden" name="edit_revision" value="{{ project_edit.updated_at }}">
            <input type="hidden" id="support_dept_code" name="support_dept_code" value="{{ project_edit.support_dept_code }}">
            <input type="hidden" id="exec_labor_rates_json" name="exec_labor_rates_json" value='{{ project_edit.exec_labor_rates | tojson | e }}'>

            <section class="form-section-block">
                <div class="form-section-head">
                    <h3>기본 정보</h3>
                    <div class="section-help">진행율은 계약금액 대비 수금액 합계로 자동 계산됩니다.</div>
                </div>
                <div id="projectImportReviewBanner" class="import-review-banner{% if project_edit.review_tag %} is-visible{% endif %}">
                    <strong id="projectImportReviewTag">{{ project_edit.review_tag }}</strong>
                    <span id="projectImportReviewNote">{{ project_edit.review_note }}</span>
                </div>
                <div class="section-grid">
                    <div class="field field-full">
                        <div class="lookup-grid">
                            <div class="field">
                                <label for="supportDeptCodeLookup">지원부서코드</label>
                                <div class="lookup-wrap">
                                    <input id="supportDeptCodeLookup" type="text" value="{{ project_edit.support_dept_code }}" placeholder="코드를 검색하거나 입력하세요." autocomplete="off" required>
                                    <div id="supportDeptCodeResults" class="lookup-results"></div>
                                </div>
                            </div>
                            <div class="field">
                                <label for="support_dept_name">지원부서명</label>
                                <div class="lookup-wrap">
                                    <input id="support_dept_name" type="text" name="support_dept_name" value="{{ project_edit.support_dept_name }}" placeholder="부서명을 검색하거나 입력하세요." autocomplete="off" required>
                                    <div id="supportDeptNameResults" class="lookup-results"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="field">
                        <label for="project_type">사업 종류</label>
                        <input id="project_type" type="text" name="project_type" value="{{ project_edit.project_type }}" placeholder="향후 A/S비·판관비 자동 연동 대비">
                    </div>
                    <div class="field">
                        <label for="contract_amount">계약금액</label>
                        <input id="contract_amount" type="number" step="0.01" name="contract_amount" value="{{ project_edit.contract_amount }}" placeholder="계약금액 입력">
                    </div>
                    <div class="field">
                        <label for="progress_rate_display">진행율(%)</label>
                        <div id="progress_rate_display" class="readonly-output">{{ "{:.1f}".format(project_edit.progress_rate or 0) }}%</div>
                    </div>
                    <div class="field">
                        <label for="completion_status">종료여부</label>
                        <input id="completion_status" type="text" name="completion_status" value="{{ project_edit.completion_status }}" placeholder="진행중 / 종료 / 완료">
                    </div>
                    <div class="field">
                        <label for="change_round">차수변경</label>
                        <input id="change_round" type="text" name="change_round" value="{{ project_edit.change_round }}">
                    </div>
                    <div class="field">
                        <label for="project_start_date">시작일</label>
                        <input id="project_start_date" class="date-text-input" type="text" inputmode="numeric" maxlength="10" name="project_start_date" value="{{ project_edit.project_start_date }}" placeholder="YYYY-MM-DD">
                    </div>
                    <div class="field">
                        <label for="project_end_date">종료일</label>
                        <input id="project_end_date" class="date-text-input" type="text" inputmode="numeric" maxlength="10" name="project_end_date" value="{{ project_edit.project_end_date }}" placeholder="YYYY-MM-DD">
                    </div>
                </div>
            </section>

            <section class="form-section-block">
                <div class="form-section-head">
                    <h3>수금 정보</h3>
                    <div class="table-tools">
                        <div class="actions">
                            <div class="section-chip">계약금액 <strong id="collectionContractInlineDisplay">{{ "{:,.0f}".format(project_edit.contract_amount or 0) }}원</strong></div>
                        </div>
                        <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="collection" title="수금 행 추가" aria-label="수금 행 추가">
                            <svg viewBox="0 0 24 24" aria-hidden="true">
                                <path d="M12 5v14"></path>
                                <path d="M5 12h14"></path>
                            </svg>
                            <span class="sr-only">수금 행 추가</span>
                        </button>
                    </div>
                </div>
                <div class="collection-entry-list" id="collectionRows">
                    {% for row in project_edit.collection_entries %}
                    <div class="collection-entry-card" data-collection-row>
                        <div class="collection-entry-grid">
                            <div class="collection-field">
                                <label>기성구분</label>
                                <select class="inline-input" name="collection_progress_type[]">
                                    {% for option in collection_progress_type_options %}
                                    <option value="{{ option }}" {% if row.progress_type == option %}selected{% endif %}>{{ option }}</option>
                                    {% endfor %}
                                </select>
                            </div>
                            <div class="collection-field">
                                <label>기성차수</label>
                                <input class="inline-input round-input" type="number" min="1" step="1" inputmode="numeric" name="collection_billing_round[]" value="{{ row.billing_round or loop.index }}">
                            </div>
                            <div class="collection-field">
                                <label>청구구분</label>
                                <select class="inline-input" name="collection_billing_type[]">
                                    {% for option in collection_billing_type_options %}
                                    <option value="{{ option }}" {% if row.billing_type == option %}selected{% endif %}>{{ option }}</option>
                                    {% endfor %}
                                </select>
                            </div>
                            <div class="collection-field">
                                <label>청구일자</label>
                                <input class="inline-input date-text-input" type="text" inputmode="numeric" maxlength="10" name="collection_billing_date[]" value="{{ row.billing_date }}" placeholder="YYYY-MM-DD">
                            </div>
                            <div class="collection-field amount-field">
                                <label>청구금액</label>
                                <input class="inline-input collection-billed-amount amount-input formatted-amount-input" type="text" inputmode="numeric" name="collection_billed_amount[]" value="{{ "{:,.0f}".format(row.billed_amount or 0) if row.billed_amount else '' }}">
                            </div>
                            <div class="collection-field">
                                <label>수금차수</label>
                                <input class="inline-input round-input" type="number" min="1" step="1" inputmode="numeric" name="collection_round[]" value="{{ row.round or loop.index }}">
                            </div>
                            <div class="collection-field">
                                <label>수금일자</label>
                                <input class="inline-input date-text-input" type="text" inputmode="numeric" maxlength="10" name="collection_date[]" value="{{ row.date }}" placeholder="YYYY-MM-DD">
                            </div>
                            <div class="collection-field amount-field">
                                <label>수금금액</label>
                                <input class="inline-input collection-amount amount-input formatted-amount-input" type="text" inputmode="numeric" name="collection_amount_row[]" value="{{ "{:,.0f}".format(row.amount or 0) if row.amount else '' }}">
                            </div>
                            <div class="collection-field balance-field">
                                <label>수금잔액</label>
                                <div class="readonly-output collection-balance-cell"></div>
                            </div>
                        </div>
                        <div class="actions" style="justify-content: flex-end;">
                            <button type="button" class="button-link danger-lite mini-button button-icon row-remove-button" data-remove-row title="행 삭제" aria-label="행 삭제">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M6 12h12"></path>
                                </svg>
                                <span class="sr-only">행 삭제</span>
                            </button>
                        </div>
                    </div>
                    {% endfor %}
                </div>
                <div class="collection-footer">
                    <div class="summary-chip">
                        <span>계약금액</span>
                        <strong id="collectionContractDisplay">{{ "{:,.0f}".format(project_edit.contract_amount or 0) }}원</strong>
                    </div>
                    <div class="summary-chip">
                        <span>청구금액 합계</span>
                        <strong id="collectionBilledTotalDisplay">0원</strong>
                    </div>
                    <div class="summary-chip">
                        <span>수금금액 합계</span>
                        <strong id="collectionTotalDisplay">{{ "{:,.0f}".format(project_edit.collection_amount or 0) }}원</strong>
                    </div>
                    <div class="summary-chip">
                        <span>수금잔액</span>
                        <strong id="collectionBalanceTotalDisplay">0원</strong>
                    </div>
                </div>
            </section>

            <section class="form-section-block">
                <div class="form-section-head">
                    <h3>과업수행계획</h3>
                </div>
                <div class="table-total">계획 합계: <span id="taskPlanTotalDisplay">0</span>원</div>
                <div class="section-columns">
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>부서별배분</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="task-plan-department" title="부서별배분 행 추가" aria-label="부서별배분 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">부서별배분 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table compact">
                                <colgroup>
                                    <col class="modal-col-dept">
                                    <col class="modal-col-work">
                                    <col class="modal-col-amount">
                                    <col class="modal-col-remove">
                                </colgroup>
                                <thead>
                                    <tr>
                                        <th>부서명</th>
                                        <th>공종명</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="taskPlanDepartmentRows"></tbody>
                            </table>
                        </div>
                    </section>
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>외주비</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="task-plan-outsource" title="외주비 행 추가" aria-label="외주비 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">외주비 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table compact">
                                <colgroup>
                                    <col class="modal-col-dept">
                                    <col class="modal-col-work">
                                    <col class="modal-col-amount">
                                    <col class="modal-col-remove">
                                </colgroup>
                                <thead>
                                    <tr>
                                        <th>부서명</th>
                                        <th>공종명</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="taskPlanOutsourceRows"></tbody>
                            </table>
                        </div>
                    </section>
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>합사운영비</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="task-plan-joint" title="합사운영비 행 추가" aria-label="합사운영비 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">합사운영비 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table compact">
                                <colgroup>
                                    <col class="modal-col-dept">
                                    <col class="modal-col-work">
                                    <col class="modal-col-amount">
                                    <col class="modal-col-remove">
                                </colgroup>
                                <thead>
                                    <tr>
                                        <th>부서명</th>
                                        <th>공종명</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="taskPlanJointRows"></tbody>
                            </table>
                        </div>
                    </section>
                </div>
            </section>

            <section class="form-section-block">
                <div class="form-section-head">
                    <h3>실행예산계획</h3>
                </div>
                <div class="table-total">실행예산 합계: <span id="execBudgetTotalDisplay">0</span>원</div>
                <div class="section-columns">
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>인건비</h4>
                            <div style="display:inline-flex; gap:6px; align-items:center;">
                                <button type="button" class="button-link button-secondary mini-button" id="openLaborRateModal">기준인건비</button>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="exec-labor" title="실행예산 인건비 행 추가" aria-label="실행예산 인건비 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">실행예산 인건비 행 추가</span>
                            </button>
                            </div>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table compact">
                                <colgroup>
                                    <col class="modal-col-dept">
                                    <col class="modal-col-work">
                                    <col class="modal-col-amount">
                                    <col class="modal-col-remove">
                                </colgroup>
                                <thead>
                                    <tr>
                                        <th>직급</th>
                                        <th>투입시간</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="execLaborRows"></tbody>
                            </table>
                        </div>
                    </section>
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>외주비</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="exec-outsource" title="실행예산 외주비 행 추가" aria-label="실행예산 외주비 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">실행예산 외주비 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table compact">
                                <colgroup>
                                    <col class="modal-col-dept">
                                    <col class="modal-col-work">
                                    <col class="modal-col-amount">
                                    <col class="modal-col-remove">
                                </colgroup>
                                <thead>
                                    <tr>
                                        <th>부서명</th>
                                        <th>공종명</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="execOutsourceRows"></tbody>
                            </table>
                        </div>
                    </section>
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>계정별비용계획</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="exec-cost-plan" title="계정별비용계획 행 추가" aria-label="계정별비용계획 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">계정별비용계획 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table compact">
                                <colgroup>
                                    <col class="modal-col-dept">
                                    <col class="modal-col-work">
                                    <col class="modal-col-amount">
                                    <col class="modal-col-remove">
                                </colgroup>
                                <thead>
                                    <tr>
                                        <th>계정코드</th>
                                        <th>계정과목명</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="execCostPlanRows"></tbody>
                            </table>
                        </div>
                    </section>
                </div>
            </section>

            <section class="form-section-block">
                <div class="form-section-head">
                    <h3>실투입 관리</h3>
                </div>
                <div class="table-total">실투입 합계: <span id="actualInputTotalDisplay">0</span>원</div>
                <div class="actual-input-layout">
                    <div class="actual-input-top">
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>인건비</h4>
                            <div class="table-tools-actions">
                                <input id="actualLaborAdjustmentTotal" class="inline-input amount-input formatted-amount-input table-tools-amount" data-amount-input type="text" inputmode="numeric" name="actual_labor_adjustment_total" placeholder="조정금액">
                                <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="actual-labor" title="실투입 인건비 행 추가" aria-label="실투입 인건비 행 추가">
                                    <svg viewBox="0 0 24 24" aria-hidden="true">
                                        <path d="M12 5v14"></path>
                                        <path d="M5 12h14"></path>
                                    </svg>
                                    <span class="sr-only">실투입 인건비 행 추가</span>
                                </button>
                            </div>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table actual-labor-table">
                                <colgroup>
                                    <col>
                                    <col>
                                    <col>
                                    <col>
                                </colgroup>
                                <thead>
                                    <tr>
                                        <th>직급</th>
                                        <th>투입시간</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="actualLaborRows"></tbody>
                            </table>
                        </div>
                    </section>

                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>인건비(합사)</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="actual-labor-joint" title="실투입 인건비(합사) 행 추가" aria-label="실투입 인건비(합사) 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">실투입 인건비(합사) 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table">
                                <thead>
                                    <tr>
                                        <th>항목</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="actualLaborJointRows"></tbody>
                            </table>
                        </div>
                    </section>
                    </div>

                    <div class="actual-input-bottom">
                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>A/S비</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="actual-as" title="실투입 A/S비 행 추가" aria-label="실투입 A/S비 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">실투입 A/S비 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table">
                                <thead>
                                    <tr>
                                        <th>항목</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="actualAsRows"></tbody>
                            </table>
                        </div>
                    </section>

                    <section class="mini-sector">
                        <div class="table-tools">
                            <h4>판관비</h4>
                            <button type="button" class="button-link button-secondary mini-button button-icon" data-add-row="actual-sga" title="실투입 판관비 행 추가" aria-label="실투입 판관비 행 추가">
                                <svg viewBox="0 0 24 24" aria-hidden="true">
                                    <path d="M12 5v14"></path>
                                    <path d="M5 12h14"></path>
                                </svg>
                                <span class="sr-only">실투입 판관비 행 추가</span>
                            </button>
                        </div>
                        <div class="row-table-wrap">
                            <table class="row-table">
                                <thead>
                                    <tr>
                                        <th>항목</th>
                                        <th>금액(원)</th>
                                        <th></th>
                                    </tr>
                                </thead>
                                <tbody id="actualSgaRows"></tbody>
                            </table>
                        </div>
                    </section>
                    </div>
                </div>
            </section>

            <section class="form-section-block">
                <div class="form-section-head">
                    <h3>예상 배분 설정</h3>
                </div>
                <div class="section-grid">
                    <div class="field">
                        <label for="expected_as_rate">예상 A/S비 비율</label>
                        <select id="expected_as_rate" name="expected_as_rate">
                            {% for option in expected_as_rate_options %}
                            <option value="{{ option.value }}" {% if project_edit.expected_as_rate|string == option.value|string %}selected{% endif %}>{{ option.label }}</option>
                            {% endfor %}
                        </select>
                    </div>
                    <div class="field">
                        <label for="expected_as_cost_display">예상 A/S비 금액</label>
                        <input id="expected_as_cost_display" class="inline-input amount-input formatted-amount-input" type="text" inputmode="numeric" name="expected_as_cost" value="{{ "{:,.0f}".format(project_edit.expected_as_cost or 0) if project_edit.expected_as_cost else '' }}" placeholder="">
                    </div>
                    <div class="field">
                        <label for="expected_sga_rate">예상 판관비 비율</label>
                        <select id="expected_sga_rate" name="expected_sga_rate">
                            {% for option in expected_sga_rate_options %}
                            <option value="{{ option.value }}" {% if project_edit.expected_sga_rate|string == option.value|string %}selected{% endif %}>{{ option.label }}</option>
                            {% endfor %}
                        </select>
                    </div>
                    <div class="field">
                        <label for="expected_sga_budget_display">예상 판관비 금액</label>
                        <input id="expected_sga_budget_display" class="inline-input amount-input formatted-amount-input" type="text" inputmode="numeric" name="expected_sga_budget" value="{{ "{:,.0f}".format(project_edit.expected_sga_budget or 0) if project_edit.expected_sga_budget else '' }}" placeholder="">
                    </div>
                </div>
            </section>

            <section class="form-section-block">
                <div class="form-section-head">
                    <h3>비고</h3>
                </div>
                <div class="field field-full">
                    <label for="notes">메모</label>
                    <textarea id="notes" name="notes">{{ project_edit.notes }}</textarea>
                </div>
            </section>
        </form>
    </div>
</div>

<div class="modal-backdrop" id="laborRateModal">
    <div class="related-modal-card" style="max-width:720px;">
        <div class="related-modal-head">
            <h3 class="related-modal-title">기준인건비</h3>
            <button type="button" class="button-secondary button-icon" id="closeLaborRateModal" title="닫기" aria-label="닫기">
                <svg viewBox="0 0 24 24" aria-hidden="true">
                    <path d="M6 6l12 12"></path>
                    <path d="M18 6L6 18"></path>
                </svg>
                <span class="sr-only">닫기</span>
            </button>
        </div>
        <div class="related-modal-body">
            <div class="row-table-wrap">
                <table class="row-table compact">
                    <colgroup>
                        <col style="width:40%">
                        <col style="width:60%">
                    </colgroup>
                    <thead>
                        <tr>
                            <th>직급</th>
                            <th>기준인건비</th>
                        </tr>
                    </thead>
                    <tbody id="laborRateRows"></tbody>
                </table>
            </div>
        </div>
    </div>
</div>

<div class="modal-backdrop" id="relatedProjectModal">
    <div class="related-modal-card">
        <div class="related-modal-head">
            <h3 class="related-modal-title">연관 프로젝트 선택</h3>
            <button type="button" class="button-secondary button-icon" id="closeRelatedProjectModal" title="닫기" aria-label="닫기">
                <svg viewBox="0 0 24 24" aria-hidden="true">
                    <path d="M6 6l12 12"></path>
                    <path d="M18 6L6 18"></path>
                </svg>
                <span class="sr-only">닫기</span>
            </button>
        </div>
        <div class="related-modal-body">
            <input id="relatedProjectSearchInput" type="text" placeholder="지원부서코드 또는 사업명을 검색하세요." autocomplete="off">
            <div class="related-search-results" id="relatedProjectSearchResults"></div>
            <div class="project-search-empty" id="relatedProjectSearchEmpty"></div>
        </div>
    </div>
</div>

<div class="modal-backdrop" id="detailYearSettingModal">
    <div class="related-modal-card" style="max-width:460px;">
        <div class="related-modal-head">
            <h3 class="related-modal-title">세부내역 반영 연도 설정</h3>
            <button type="button" class="button-secondary button-icon" id="closeDetailYearSettingModal" title="닫기" aria-label="닫기">
                <svg viewBox="0 0 24 24" aria-hidden="true">
                    <path d="M6 6l12 12"></path>
                    <path d="M18 6L6 18"></path>
                </svg>
                <span class="sr-only">닫기</span>
            </button>
        </div>
        <div class="related-modal-body">
            <div class="field">
                <label for="detailVisibleMinYearInput">세부내역 반영 시작 연도</label>
                <input id="detailVisibleMinYearInput" type="number" min="2000" max="2099" step="1">
            </div>
            <p class="stacked-note">이 연도보다 이전에 생성된 프로젝트 코드는 세부 내역과 합계에서 제외됩니다.</p>
            <div class="actions" style="justify-content:flex-end;">
                <button type="button" class="button-icon" id="saveDetailYearSetting" title="설정 저장" aria-label="설정 저장">
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M6 4h9l3 3v13H6z"></path>
                        <path d="M9 4v6h6V4"></path>
                        <path d="M9 17h6"></path>
                    </svg>
                    <span class="sr-only">설정 저장</span>
                </button>
            </div>
        </div>
    </div>
</div>

<div class="modal-backdrop" id="excludedDetailItemsModal">
    <div class="related-modal-card" style="max-width:1080px; width:min(1080px, calc(100vw - 48px));">
        <div class="related-modal-head">
            <h3 class="related-modal-title">세부 내역 제외 항목</h3>
            <button type="button" class="button-secondary button-icon" id="closeExcludedDetailItemsModal" title="닫기" aria-label="닫기">
                <svg viewBox="0 0 24 24" aria-hidden="true">
                    <path d="M6 6l12 12"></path>
                    <path d="M18 6L6 18"></path>
                </svg>
                <span class="sr-only">닫기</span>
            </button>
        </div>
        <div class="related-modal-body">
            <p class="stacked-note" id="excludedDetailItemsSummary"></p>
            <div class="row-table-wrap">
                <table class="compact-table">
                    <thead>
                        <tr>
                            <th>프로젝트</th>
                            <th>생성연도</th>
                            <th>발생 비용</th>
                            <th>제외 사유</th>
                        </tr>
                    </thead>
                    <tbody id="excludedDetailItemsRows">
                        <tr><td colspan="4">표시할 제외 항목이 없습니다.</td></tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block script %}
<script>
    const selectedYear = {{ selected_year or "null" }};
    const yearFilter = document.getElementById("year");
    const supportDepartmentOptions = {{ support_department_options | tojson }};
    const costDepartmentOptions = {{ cost_department_options | tojson }};
    const costAccountOptions = {{ cost_account_options | tojson }};
    const laborGradeOptions = {{ labor_grade_options | tojson }};
    const uncontractedCategoryOptions = {{ uncontracted_category_options | tojson }};
    const specialXClassificationRules = {{ special_x_classification_rules | tojson }};
    const projectRuntimeSettings = {{ project_runtime_settings | tojson }};
    let projectEdit = {{ project_edit | tojson }};
    const serverFocusCode = {{ project_focus_code | tojson }};
    const projectEditCache = new Map();
    let modalOriginalEdit = null;
    const revenueMix = {{ project_revenue_mix | tojson }};
    const revenueMixMode = {{ ("monthly" if selected_year else "yearly") | tojson }};
    const projectCostRows = {{ project_cost_by_year | tojson }};
    const projectMonthlyCostRows = {{ project_monthly_cost_rows | tojson }};
    const projectStatusRows = {{ project_status_rows | tojson }};
    const projectComparisonNotes = {{ project_comparison_notes | tojson }};
    const persistedProjectPageState = {{ project_page_state | tojson }};
    const persistedProjectRelatedLinks = {{ project_related_links | tojson }};
    const persistedUncontractedCategoryOverrides = {{ project_uncontracted_classifications | tojson }};
    const projectPageSessionId = window.clientSessionId || "";
    const projectStatusMap = Object.fromEntries(projectStatusRows.map((item) => [item.support_dept_code, item]));
    const projectAccountBreakdowns = {{ project_account_breakdowns | tojson }};
    const projectCostMap = Object.fromEntries(projectCostRows.map((item) => [item.support_dept_code, item]));
    const currencyFormatter = new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 0 });
    window.__projectRelatedSelections = new Map();
    const persistedUncontractedFilterState = {
        start_year: String(persistedProjectPageState?.uncontracted_year_start || ""),
        end_year: String(persistedProjectPageState?.uncontracted_year_end || ""),
    };
    let currentSelectedProjectCode = projectEdit.support_dept_code || "";
    const projectStatusForm = document.getElementById("projectStatusForm");
    const modal = document.getElementById("projectModal");
    const openModalButton = document.getElementById("openProjectModal");
    const closeModalButton = document.getElementById("closeProjectModal");
    const saveProjectModalButton = document.getElementById("saveProjectModal");
    const supportDeptCodeLookup = document.getElementById("supportDeptCodeLookup");
    const supportDeptCodeInput = document.getElementById("support_dept_code");
    const supportDeptNameInput = document.getElementById("support_dept_name");
    const projectTypeInput = document.getElementById("project_type");
    const completionStatusInput = document.getElementById("completion_status");
    const changeRoundInput = document.getElementById("change_round");
    const projectStartDateInput = document.getElementById("project_start_date");
    const projectEndDateInput = document.getElementById("project_end_date");
    const notesInput = document.getElementById("notes");
    const projectImportReviewBanner = document.getElementById("projectImportReviewBanner");
    const projectImportReviewTag = document.getElementById("projectImportReviewTag");
    const projectImportReviewNote = document.getElementById("projectImportReviewNote");
    const supportDeptCodeResults = document.getElementById("supportDeptCodeResults");
    const supportDeptNameResults = document.getElementById("supportDeptNameResults");
    const contractAmountInput = document.getElementById("contract_amount");
    const expectedAsRateInput = document.getElementById("expected_as_rate");
    const expectedSgaRateInput = document.getElementById("expected_sga_rate");
    const editRevisionInput = projectStatusForm?.querySelector('input[name="edit_revision"]');
    const collectionRows = document.getElementById("collectionRows");
    const taskPlanDepartmentRows = document.getElementById("taskPlanDepartmentRows");
    const taskPlanOutsourceRows = document.getElementById("taskPlanOutsourceRows");
    const taskPlanJointRows = document.getElementById("taskPlanJointRows");
    const execLaborRows = document.getElementById("execLaborRows");
    const execOutsourceRows = document.getElementById("execOutsourceRows");
    const execCostPlanRows = document.getElementById("execCostPlanRows");
    const actualLaborRows = document.getElementById("actualLaborRows");
    const actualLaborJointRows = document.getElementById("actualLaborJointRows");
    const actualLaborAdjustmentInput = document.getElementById("actualLaborAdjustmentTotal");
    const actualAsRows = document.getElementById("actualAsRows");
    const actualSgaRows = document.getElementById("actualSgaRows");
    const execLaborRatesInput = document.getElementById("exec_labor_rates_json");
    const laborRateModal = document.getElementById("laborRateModal");
    const laborRateRows = document.getElementById("laborRateRows");
    const openLaborRateModalButton = document.getElementById("openLaborRateModal");
    const closeLaborRateModalButton = document.getElementById("closeLaborRateModal");
    const detailYearSettingModal = document.getElementById("detailYearSettingModal");
    const detailVisibleMinYearInput = document.getElementById("detailVisibleMinYearInput");
    const closeDetailYearSettingModalButton = document.getElementById("closeDetailYearSettingModal");
    const saveDetailYearSettingButton = document.getElementById("saveDetailYearSetting");
    const excludedDetailItemsModal = document.getElementById("excludedDetailItemsModal");
    const closeExcludedDetailItemsModalButton = document.getElementById("closeExcludedDetailItemsModal");
    const excludedDetailItemsSummary = document.getElementById("excludedDetailItemsSummary");
    const excludedDetailItemsRows = document.getElementById("excludedDetailItemsRows");
    let currentLaborRates = {};
    let latestExcludedDetailRows = [];

    yearFilter?.addEventListener("change", (event) => {
        const nextYear = event.target.value;
        const nextUrl = nextYear ? `/projects?year=${encodeURIComponent(nextYear)}` : "/projects";
        window.location.href = nextUrl;
    });

    function escapeHtml(value) {
        return String(value ?? "")
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;");
    }

    function formatNumber(value) {
        return currencyFormatter.format(value || 0);
    }

    function formatDisplayAmount(value) {
        const numeric = Number(value || 0);
        if (!numeric) return "0";
        if (Math.abs(numeric) >= 100000000) {
            return `${(numeric / 100000000).toFixed(1).replace(/\.0$/, "")}억`;
        }
        if (Math.abs(numeric) >= 1000000) {
            return `${(numeric / 1000000).toFixed(1).replace(/\.0$/, "")}백만`;
        }
        if (Math.abs(numeric) >= 1000) {
            return `${(numeric / 1000).toFixed(1).replace(/\.0$/, "")}천`;
        }
        return formatNumber(numeric);
    }

    function formatCompactCurrency(value) {
        const numeric = Number(value || 0);
        if (!numeric) return "0원";
        if (Math.abs(numeric) >= 100000000) {
            return `${(numeric / 100000000).toFixed(1).replace(/\.0$/, "")}억원`;
        }
        if (Math.abs(numeric) >= 1000000) {
            return `${(numeric / 1000000).toFixed(1).replace(/\.0$/, "")}백만원`;
        }
        return `${formatNumber(numeric)}원`;
    }

    function formatAxisLabel(value) {
        const numeric = Number(value || 0);
        if (!numeric) return "0";
        if (numeric >= 100000000) {
            return `${numeric / 100000000}억`;
        }
        if (numeric >= 1000000) {
            return `${numeric / 1000000}백만`;
        }
        if (numeric >= 1000) {
            return `${numeric / 1000}천`;
        }
        return formatNumber(numeric);
    }

    function pickTickStep(maxValue) {
        const baseUnit = maxValue < 1000000 ? 1000 : 1000000;
        const units = [1, 2, 5];
        const raw = Math.max(maxValue / baseUnit, 1);
        let power = 1;
        while (power * 10 <= raw) {
            power *= 10;
        }
        for (const unit of units) {
            const candidate = unit * power;
            if (candidate >= raw) {
                return candidate * baseUnit;
            }
        }
        return power * 10 * baseUnit;
    }

    function buildPositiveAxis(maxValue, tickCount = 4) {
        const safeMax = Math.max(Number(maxValue || 0), 1);
        const paddedMax = safeMax * (safeMax < 1000 ? 1.12 : 1.08);
        const tickStep = pickTickStep(paddedMax / tickCount);
        const tickMax = Math.max(tickStep, Math.ceil(paddedMax / tickStep) * tickStep);
        return { tickStep, tickMax };
    }

    function parseAmount(value) {
        const numeric = Number(String(value ?? "").replace(/,/g, ""));
        return Number.isFinite(numeric) ? numeric : 0;
    }

    function digitsOnly(value) {
        return String(value ?? "").replace(/\D/g, "");
    }

    function formatAmountInputValue(value) {
        const numeric = parseAmount(value);
        if (!numeric) return "";
        return formatNumber(Math.round(numeric));
    }

    function parseHours(value) {
        const normalized = String(value ?? "").replace(/,/g, ".").replace(/[^\d.]/g, "");
        const numeric = Number(normalized);
        return Number.isFinite(numeric) ? numeric : 0;
    }

    function formatHourInputValue(value) {
        const numeric = parseHours(value);
        if (!numeric) return "";
        return String(Number(numeric.toFixed(2)));
    }

    const collectionProgressTypeOptions = {{ collection_progress_type_options | tojson }};
    const collectionBillingTypeOptions = {{ collection_billing_type_options | tojson }};

    function renderSelectOptions(options, selectedValue) {
        const currentValue = String(selectedValue || "");
        return options.map((option) => (
            `<option value="${escapeHtml(option)}" ${currentValue === String(option) ? "selected" : ""}>${escapeHtml(option)}</option>`
        )).join("");
    }

    function parseDurationMinutes(value) {
        const raw = String(value ?? "").trim().toLowerCase();
        if (!raw) return 0;
        if (/^\d+$/.test(raw)) {
            return Number(raw) * 60;
        }
        if (/^\d+(?:\.\d+)?$/.test(raw)) {
            return Math.round(Number(raw) * 60);
        }
        const colonMatch = raw.match(/^(\d+)\s*:\s*(\d{1,2})$/);
        if (colonMatch) {
            return Number(colonMatch[1]) * 60 + Number(colonMatch[2]);
        }
        let total = 0;
        const hourMatch = raw.match(/(\d+(?:\.\d+)?)\s*(?:h|시간)/);
        const minuteMatch = raw.match(/(\d+)\s*(?:m|분)/);
        if (hourMatch) {
            total += Math.round(Number(hourMatch[1]) * 60);
        }
        if (minuteMatch) {
            total += Number(minuteMatch[1]);
        }
        if (total) return total;
        const digits = raw.replace(/[^\d]/g, "");
        return digits ? Number(digits) : 0;
    }

    function formatDurationInputValue(value) {
        const minutes = Number(value || 0);
        if (!minutes) return "";
        const hours = Math.floor(minutes / 60);
        const remainder = minutes % 60;
        if (!remainder) return `${hours}h`;
        if (!hours) return `${remainder}m`;
        return `${hours}h ${remainder}m`;
    }

    function splitDurationMinutes(value) {
        const totalMinutes = Number(value || 0);
        if (!totalMinutes) {
            return { hours: "", minutes: "" };
        }
        return {
            hours: String(Math.floor(totalMinutes / 60)),
            minutes: String(totalMinutes % 60),
        };
    }

    function readLaborRates() {
        try {
            return JSON.parse(execLaborRatesInput?.value || "{}") || {};
        } catch (error) {
            return {};
        }
    }

    function writeLaborRates(nextRates) {
        currentLaborRates = { ...nextRates };
        if (execLaborRatesInput) {
            execLaborRatesInput.value = JSON.stringify(currentLaborRates);
        }
    }

    function getLaborRate(grade) {
        const direct = Number(currentLaborRates?.[grade] || 0);
        if (direct) return direct;
        const gradeIndex = laborGradeOptions.indexOf(grade);
        if (gradeIndex === -1) return 0;
        for (let index = gradeIndex + 1; index < laborGradeOptions.length; index += 1) {
            const fallback = Number(currentLaborRates?.[laborGradeOptions[index]] || 0);
            if (fallback) return fallback;
        }
        return 0;
    }

    function calculateLaborAmount(grade, hours) {
        return getLaborRate(grade) * parseHours(hours);
    }

    function formatDateInputValue(value) {
        const digits = digitsOnly(value).slice(0, 8);
        if (digits.length <= 4) return digits;
        if (digits.length <= 6) return `${digits.slice(0, 4)}-${digits.slice(4)}`;
        return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6)}`;
    }

    function getNextRoundValue(fieldName) {
        const values = [...collectionRows.querySelectorAll(`input[name="${fieldName}"]`)]
            .map((input) => Number(input.value || 0))
            .filter((value) => Number.isFinite(value) && value > 0);
        return String((values.length ? Math.max(...values) : 0) + 1);
    }

    function getAllTaskPlanAmountInputs() {
        return [
            ...taskPlanDepartmentRows.querySelectorAll("[data-amount-input]"),
            ...taskPlanOutsourceRows.querySelectorAll("[data-amount-input]"),
            ...taskPlanJointRows.querySelectorAll("[data-amount-input]"),
        ];
    }

    function getAllExecBudgetAmountInputs() {
        return [
            ...execLaborRows.querySelectorAll("[data-amount-input]"),
            ...execOutsourceRows.querySelectorAll("[data-amount-input]"),
            ...execCostPlanRows.querySelectorAll("[data-amount-input]"),
        ];
    }

    function getAllActualAmountInputs() {
        return [
            ...actualLaborRows.querySelectorAll("[data-amount-input]"),
            ...actualLaborJointRows.querySelectorAll("[data-amount-input]"),
            ...(actualLaborAdjustmentInput ? [actualLaborAdjustmentInput] : []),
            ...actualAsRows.querySelectorAll("[data-amount-input]"),
            ...actualSgaRows.querySelectorAll("[data-amount-input]"),
        ];
    }

    function updateCollectionContractCells(contractAmount, collectionTotal) {
        const contractDisplay = document.getElementById("collectionContractDisplay");
        const contractInlineDisplay = document.getElementById("collectionContractInlineDisplay");
        if (contractDisplay) {
            contractDisplay.textContent = `${formatNumber(contractAmount)}원`;
        }
        if (contractInlineDisplay) {
            contractInlineDisplay.textContent = `${formatNumber(contractAmount)}원`;
        }
        let runningCollected = 0;
        collectionRows.querySelectorAll("[data-collection-row]").forEach((row) => {
            const amountInput = row.querySelector(".collection-amount");
            runningCollected += parseAmount(amountInput?.value);
            const balanceCell = row.querySelector(".collection-balance-cell");
            if (balanceCell) {
                balanceCell.textContent = `${formatNumber(Math.max(contractAmount - runningCollected, 0))}원`;
            }
        });
        const balance = Math.max(contractAmount - collectionTotal, 0);
        document.getElementById("collectionBalanceTotalDisplay").textContent = `${formatNumber(balance)}원`;
    }

    function updateComputedFields() {
        const contractAmount = parseAmount(contractAmountInput?.value);
        const billedTotal = [...document.querySelectorAll(".collection-billed-amount")]
            .reduce((sum, input) => sum + parseAmount(input.value), 0);
        const collectionTotal = [...document.querySelectorAll(".collection-amount")]
            .reduce((sum, input) => sum + parseAmount(input.value), 0);
        const progressRate = contractAmount ? (collectionTotal / contractAmount) * 100 : 0;
        document.getElementById("collectionBilledTotalDisplay").textContent = `${formatNumber(billedTotal)}원`;
        document.getElementById("collectionTotalDisplay").textContent = `${formatNumber(collectionTotal)}원`;
        updateCollectionContractCells(contractAmount, collectionTotal);
        document.getElementById("progress_rate_display").textContent = `${progressRate.toFixed(1)}%`;

        const expectedAsCost = contractAmount * parseAmount(expectedAsRateInput?.value) / 100;
        const expectedSgaBudget = contractAmount * parseAmount(expectedSgaRateInput?.value) / 100;
        const expectedAsCostField = document.getElementById("expected_as_cost_display");
        const expectedSgaBudgetField = document.getElementById("expected_sga_budget_display");
        if (expectedAsCostField && document.activeElement !== expectedAsCostField) {
            const keepManual = parseAmount(expectedAsCostField.value);
            expectedAsCostField.value = keepManual ? formatAmountInputValue(keepManual) : (expectedAsCost ? formatAmountInputValue(expectedAsCost) : "");
        }
        if (expectedSgaBudgetField && document.activeElement !== expectedSgaBudgetField) {
            const keepManual = parseAmount(expectedSgaBudgetField.value);
            expectedSgaBudgetField.value = keepManual ? formatAmountInputValue(keepManual) : (expectedSgaBudget ? formatAmountInputValue(expectedSgaBudget) : "");
        }
        syncExpectedAllocationsToActualInputs();

        const taskPlanTotal = getAllTaskPlanAmountInputs()
            .reduce((sum, input) => sum + parseAmount(input.value), 0);
        const execBudgetTotal = getAllExecBudgetAmountInputs()
            .reduce((sum, input) => sum + parseAmount(input.value), 0);
        const actualInputTotal = getAllActualAmountInputs()
            .reduce((sum, input) => sum + parseAmount(input.value), 0);
        document.getElementById("taskPlanTotalDisplay").textContent = formatNumber(taskPlanTotal);
        document.getElementById("execBudgetTotalDisplay").textContent = formatNumber(execBudgetTotal);
        document.getElementById("actualInputTotalDisplay").textContent = formatNumber(actualInputTotal);
    }

    function syncExpectedAllocationsToActualInputs() {
        const expectedAsAmount = parseAmount(document.getElementById("expected_as_cost_display")?.value);
        const expectedSgaAmount = parseAmount(document.getElementById("expected_sga_budget_display")?.value);
        const ensureAutoSyncTarget = (container, labelText, expectedAmount) => {
            let syncInputs = [...container.querySelectorAll('[data-auto-sync="true"]')];
            if (syncInputs.length) return syncInputs;
            const fallbackRow = [...container.querySelectorAll("tr")].find((row) => {
                const labelInput = row.querySelector('input[type="text"][name$="_label[]"], input[type="text"]');
                const amountInput = row.querySelector('[data-amount-input]');
                const normalizedLabel = String(labelInput?.value || "").trim();
                const amountValue = parseAmount(amountInput?.value);
                return normalizedLabel === labelText && (!amountValue || amountValue === expectedAmount);
            });
            const fallbackInput = fallbackRow?.querySelector('[data-amount-input]');
            if (fallbackInput) {
                fallbackInput.dataset.autoSync = "true";
                syncInputs = [fallbackInput];
            }
            return syncInputs;
        };
        const asSyncInputs = ensureAutoSyncTarget(actualAsRows, "A/S비", expectedAsAmount);
        const sgaSyncInputs = ensureAutoSyncTarget(actualSgaRows, "판관비", expectedSgaAmount);
        asSyncInputs.forEach((input) => {
            input.value = expectedAsAmount ? formatAmountInputValue(expectedAsAmount) : "";
        });
        sgaSyncInputs.forEach((input) => {
            input.value = expectedSgaAmount ? formatAmountInputValue(expectedSgaAmount) : "";
        });
    }

    function upsertProjectStatusRow(projectRow) {
        if (!projectRow?.support_dept_code) return;
        const code = String(projectRow.support_dept_code);
        projectStatusMap[code] = projectRow;
        const index = projectStatusRows.findIndex((item) => item.support_dept_code === code);
        if (index >= 0) {
            projectStatusRows[index] = projectRow;
        } else {
            projectStatusRows.push(projectRow);
        }
    }

    function createCollectionRow(row = {}) {
        const billingRound = row.billing_round || getNextRoundValue("collection_billing_round[]");
        const collectionRound = row.round || getNextRoundValue("collection_round[]");
        return `
            <div class="collection-entry-card" data-collection-row>
                <div class="collection-entry-grid">
                    <div class="collection-field">
                        <label>기성구분</label>
                        <select class="inline-input" name="collection_progress_type[]">
                            ${renderSelectOptions(collectionProgressTypeOptions, row.progress_type || "")}
                        </select>
                    </div>
                    <div class="collection-field">
                        <label>기성차수</label>
                        <input class="inline-input round-input" type="number" min="1" step="1" inputmode="numeric" name="collection_billing_round[]" value="${escapeHtml(billingRound)}">
                    </div>
                    <div class="collection-field">
                        <label>청구구분</label>
                        <select class="inline-input" name="collection_billing_type[]">
                            ${renderSelectOptions(collectionBillingTypeOptions, row.billing_type || "")}
                        </select>
                    </div>
                    <div class="collection-field">
                        <label>청구일자</label>
                        <input class="inline-input date-text-input" type="text" inputmode="numeric" maxlength="10" name="collection_billing_date[]" value="${escapeHtml(row.billing_date || "")}" placeholder="YYYY-MM-DD">
                    </div>
                    <div class="collection-field amount-field">
                        <label>청구금액</label>
                        <input class="inline-input collection-billed-amount amount-input formatted-amount-input" data-billed-amount-input type="text" inputmode="numeric" name="collection_billed_amount[]" value="${escapeHtml(formatAmountInputValue(row.billed_amount || ""))}">
                    </div>
                    <div class="collection-field">
                        <label>수금차수</label>
                        <input class="inline-input round-input" type="number" min="1" step="1" inputmode="numeric" name="collection_round[]" value="${escapeHtml(collectionRound)}">
                    </div>
                    <div class="collection-field">
                        <label>수금일자</label>
                        <input class="inline-input date-text-input" type="text" inputmode="numeric" maxlength="10" name="collection_date[]" value="${escapeHtml(row.date || "")}" placeholder="YYYY-MM-DD">
                    </div>
                    <div class="collection-field amount-field">
                        <label>수금금액</label>
                        <input class="inline-input collection-amount amount-input formatted-amount-input" data-amount-input type="text" inputmode="numeric" name="collection_amount_row[]" value="${escapeHtml(formatAmountInputValue(row.amount || ""))}">
                    </div>
                    <div class="collection-field balance-field">
                        <label>수금잔액</label>
                        <div class="readonly-output collection-balance-cell"></div>
                    </div>
                </div>
                <div class="actions" style="justify-content: flex-end;">
                    <button type="button" class="button-link danger-lite mini-button button-icon row-remove-button" data-remove-row title="행 삭제" aria-label="행 삭제">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M6 12h12"></path>
                        </svg>
                        <span class="sr-only">행 삭제</span>
                    </button>
                </div>
            </div>
        `;
    }

    function createTaskPlanRow(group, row = {}) {
        const fieldMap = {
            department: ["task_plan_department_dept[]", "task_plan_department_work[]", "task_plan_department_amount[]"],
            outsource: ["task_plan_outsource_dept[]", "task_plan_outsource_work[]", "task_plan_outsource_amount[]"],
            joint: ["task_plan_joint_dept[]", "task_plan_joint_work[]", "task_plan_joint_amount[]"],
        };
        const [deptName, workName, amountName] = fieldMap[group];
        const deptValue = row.dept_name || row.label || "";
        const workValue = row.work_name || row.note || "";
        const deptField = `
            <div class="lookup-wrap">
                <input class="inline-input cost-dept-lookup" type="text" name="${deptName}" value="${escapeHtml(deptValue)}" autocomplete="off">
                <div class="lookup-results cost-dept-results"></div>
            </div>
        `;
        const workField = `<input class="inline-input" type="text" name="${workName}" value="${escapeHtml(workValue)}">`;
        return `
            <tr data-group="${group}">
                <td>${deptField}</td>
                <td>${workField}</td>
                <td><input class="inline-input amount-input formatted-amount-input" data-amount-input type="text" inputmode="numeric" name="${amountName}" value="${escapeHtml(formatAmountInputValue(row.amount || ""))}"></td>
                <td><button type="button" class="button-link danger-lite mini-button button-icon row-remove-button" data-remove-row title="행 삭제" aria-label="행 삭제">
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M6 12h12"></path>
                    </svg>
                    <span class="sr-only">행 삭제</span>
                </button></td>
            </tr>
        `;
    }

    function createExecBudgetRow(group, row = {}) {
        if (group === "labor") {
            const selectedGrade = row.grade || row.dept_name || "";
            const inputHours = row.hours || row.work_name || "";
            const amountValue = row.amount || calculateLaborAmount(selectedGrade, inputHours);
            return `
                <tr data-group="${group}">
                    <td>
                        <select class="inline-input exec-labor-grade" name="exec_labor_grade[]">
                            <option value="">직급 선택</option>
                            ${laborGradeOptions.map((grade) => `<option value="${escapeHtml(grade)}" ${selectedGrade === grade ? "selected" : ""}>${escapeHtml(grade)}</option>`).join("")}
                        </select>
                    </td>
                    <td><input class="inline-input exec-labor-hours" type="text" inputmode="decimal" name="exec_labor_hours[]" value="${escapeHtml(formatHourInputValue(inputHours))}"></td>
                    <td><input class="inline-input amount-input formatted-amount-input exec-labor-amount" data-amount-input type="text" inputmode="numeric" name="exec_labor_amount[]" value="${escapeHtml(formatAmountInputValue(amountValue))}" readonly></td>
                    <td><button type="button" class="button-link danger-lite mini-button button-icon row-remove-button" data-remove-row title="행 삭제" aria-label="행 삭제">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M6 12h12"></path>
                        </svg>
                        <span class="sr-only">행 삭제</span>
                    </button></td>
                </tr>
            `;
        }
        const fieldMap = {
            outsource: ["exec_outsource_dept[]", "exec_outsource_work[]", "exec_outsource_amount[]"],
            cost_plan: ["exec_cost_plan_code[]", "exec_cost_plan_name[]", "exec_cost_plan_amount[]"],
        };
        const [firstName, secondName, amountName] = fieldMap[group];
        const firstValue = row.dept_name || row.account_code || row.label || "";
        const secondValue = row.work_name || row.account_name || row.note || "";
        return `
            <tr data-group="${group}">
                <td>
                    ${group === "cost_plan" ? `
                        <div class="lookup-wrap">
                            <input class="inline-input cost-account-code-lookup" type="text" name="${firstName}" value="${escapeHtml(firstValue)}" autocomplete="off">
                            <div class="lookup-results cost-account-results"></div>
                        </div>
                    ` : `
                        <div class="lookup-wrap">
                            <input class="inline-input cost-dept-lookup" type="text" name="${firstName}" value="${escapeHtml(firstValue)}" autocomplete="off">
                            <div class="lookup-results cost-dept-results"></div>
                        </div>
                    `}
                </td>
                <td>
                    ${group === "cost_plan" ? `
                        <div class="lookup-wrap">
                            <input class="inline-input cost-account-name-lookup" type="text" name="${secondName}" value="${escapeHtml(secondValue)}" autocomplete="off">
                            <div class="lookup-results cost-account-results"></div>
                        </div>
                    ` : `<input class="inline-input" type="text" name="${secondName}" value="${escapeHtml(secondValue)}">`}
                </td>
                <td><input class="inline-input amount-input formatted-amount-input" data-amount-input type="text" inputmode="numeric" name="${amountName}" value="${escapeHtml(formatAmountInputValue(row.amount || ""))}"></td>
                <td><button type="button" class="button-link danger-lite mini-button button-icon row-remove-button" data-remove-row title="행 삭제" aria-label="행 삭제">
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M6 12h12"></path>
                    </svg>
                    <span class="sr-only">행 삭제</span>
                </button></td>
            </tr>
        `;
    }

    function createActualLaborRow(row = {}) {
        const selectedGrade = row.grade || "";
        const inputMinutes = Number(row.minutes || row.work_name || 0);
        const splitDuration = splitDurationMinutes(inputMinutes);
        const amountValue = row.amount || calculateLaborAmount(selectedGrade, inputMinutes / 60);
        return `
            <tr data-group="labor">
                <td>
                    <select class="inline-input actual-labor-grade" name="actual_labor_grade[]">
                        <option value="">직급 선택</option>
                        ${laborGradeOptions.map((grade) => `<option value="${escapeHtml(grade)}" ${selectedGrade === grade ? "selected" : ""}>${escapeHtml(grade)}</option>`).join("")}
                    </select>
                </td>
                <td>
                    <div class="actual-time-grid">
                        <input class="inline-input actual-labor-hours" type="text" inputmode="numeric" value="${escapeHtml(splitDuration.hours)}" placeholder="0">
                        <span class="actual-time-unit">시간</span>
                        <input class="inline-input actual-labor-minute-part" type="text" inputmode="numeric" value="${escapeHtml(splitDuration.minutes)}" placeholder="0">
                        <span class="actual-time-unit">분</span>
                    </div>
                    <input class="actual-labor-minutes" type="hidden" name="actual_labor_minutes[]" value="${escapeHtml(String(inputMinutes || ""))}">
                </td>
                <td><input class="inline-input amount-input formatted-amount-input actual-labor-amount" data-amount-input type="text" inputmode="numeric" name="actual_labor_amount[]" value="${escapeHtml(formatAmountInputValue(amountValue))}" readonly></td>
                <td><button type="button" class="button-link danger-lite mini-button button-icon row-remove-button" data-remove-row title="행 삭제" aria-label="행 삭제">
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M6 12h12"></path>
                    </svg>
                    <span class="sr-only">행 삭제</span>
                </button></td>
            </tr>
        `;
    }

    function createActualCostRow(group, row = {}) {
        const fieldPrefix = group === "as" ? "actual_as" : (group === "sga" ? "actual_sga" : "actual_labor_joint");
        const labelValue = row.label || row.note || "";
        const autoSync = row.auto_sync === true;
        return `
            <tr data-group="${group}">
                <td><input class="inline-input" type="text" name="${fieldPrefix}_label[]" value="${escapeHtml(labelValue)}"></td>
                <td><input class="inline-input amount-input formatted-amount-input" data-amount-input ${autoSync ? 'data-auto-sync="true"' : ""} type="text" inputmode="numeric" name="${fieldPrefix}_amount[]" value="${escapeHtml(formatAmountInputValue(row.amount || ""))}"></td>
                <td><button type="button" class="button-link danger-lite mini-button button-icon row-remove-button" data-remove-row title="행 삭제" aria-label="행 삭제">
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M6 12h12"></path>
                    </svg>
                    <span class="sr-only">행 삭제</span>
                </button></td>
            </tr>
        `;
    }

    function recalculateActualLaborAmounts() {
        actualLaborRows.querySelectorAll("tr").forEach((row) => {
            const grade = row.querySelector(".actual-labor-grade")?.value || "";
            const hoursInput = row.querySelector(".actual-labor-hours");
            const minuteInput = row.querySelector(".actual-labor-minute-part");
            const hours = Number(String(hoursInput?.value || "").replace(/[^\d]/g, "")) || 0;
            const minutesPartRaw = Number(String(minuteInput?.value || "").replace(/[^\d]/g, "")) || 0;
            const normalizedHours = hours + Math.floor(minutesPartRaw / 60);
            const normalizedMinutePart = minutesPartRaw % 60;
            const minutes = normalizedHours * 60 + normalizedMinutePart;
            const hiddenMinutesInput = row.querySelector(".actual-labor-minutes");
            const amountInput = row.querySelector(".actual-labor-amount");
            if (hoursInput) {
                hoursInput.value = normalizedHours ? String(normalizedHours) : "";
            }
            if (minuteInput) {
                minuteInput.value = normalizedMinutePart ? String(normalizedMinutePart) : "";
            }
            if (hiddenMinutesInput) {
                hiddenMinutesInput.value = String(minutes || "");
            }
            if (amountInput) {
                amountInput.value = formatAmountInputValue(calculateLaborAmount(grade, minutes / 60));
            }
        });
    }

    function bindCollectionFieldBehaviors(container) {
        container.querySelectorAll(".formatted-amount-input").forEach((input) => {
            input.value = formatAmountInputValue(input.value);
            input.addEventListener("input", () => {
                input.value = formatAmountInputValue(input.value);
            });
            input.addEventListener("blur", () => {
                input.value = formatAmountInputValue(input.value);
            });
        });

        container.querySelectorAll(".date-text-input").forEach((input) => {
            input.value = formatDateInputValue(input.value);
            input.addEventListener("input", () => {
                input.value = formatDateInputValue(input.value);
            });
            input.addEventListener("blur", () => {
                input.value = formatDateInputValue(input.value);
            });
        });

        container.querySelectorAll(".round-input").forEach((input) => {
            input.addEventListener("input", () => {
                input.value = digitsOnly(input.value).slice(0, 3);
            });
            input.addEventListener("blur", () => {
                if (!input.value) {
                    input.value = "1";
                }
            });
        });
    }

    function bindDateTextInputs(scope = document) {
        scope.querySelectorAll(".date-text-input").forEach((input) => {
            if (input.dataset.dateBound === "true") return;
            input.dataset.dateBound = "true";
            input.value = formatDateInputValue(input.value);
            input.addEventListener("input", () => {
                input.value = formatDateInputValue(input.value);
            });
            input.addEventListener("blur", () => {
                input.value = formatDateInputValue(input.value);
            });
        });
    }

    function getSupportDeptMatch(keyword) {
        const normalized = (keyword || "").trim().toLowerCase();
        if (!normalized) return null;
        const exact = supportDepartmentOptions.find((item) =>
            item.support_dept_code.toLowerCase() === normalized || item.support_dept_name.toLowerCase() === normalized
        );
        if (exact) return exact;
        const matches = supportDepartmentOptions.filter((item) =>
            `${item.support_dept_code} ${item.support_dept_name}`.toLowerCase().includes(normalized)
        );
        return matches.length === 1 ? matches[0] : null;
    }

    function applySupportDepartment(item) {
        if (!item) return;
        supportDeptCodeInput.value = item.support_dept_code || "";
        supportDeptCodeLookup.value = item.support_dept_code || "";
        supportDeptNameInput.value = item.support_dept_name || "";
    }

    function attachTableEvents(container) {
        bindCollectionFieldBehaviors(container);
        container.querySelectorAll("[data-remove-row]").forEach((button) => {
            button.onclick = () => {
                button.closest("[data-collection-row], tr")?.remove();
                if (container === actualLaborRows) {
                    recalculateActualLaborAmounts();
                }
                updateComputedFields();
            };
        });
        container.querySelectorAll("input, select").forEach((input) => {
            input.oninput = () => {
                if ((container === actualAsRows || container === actualSgaRows) && input.matches("[data-amount-input]")) {
                    input.dataset.autoSync = "false";
                }
                if (container === execLaborRows) {
                    recalculateExecLaborAmounts();
                }
                if (container === actualLaborRows) {
                    recalculateActualLaborAmounts();
                }
                updateComputedFields();
            };
            input.onchange = input.oninput;
        });
        if (container === actualLaborRows) {
            container.querySelectorAll(".actual-labor-hours, .actual-labor-minute-part").forEach((input) => {
                input.oninput = () => {
                    input.value = input.value.replace(/[^\d]/g, "").slice(0, 4);
                    recalculateActualLaborAmounts();
                    updateComputedFields();
                };
                input.onblur = () => {
                    recalculateActualLaborAmounts();
                    updateComputedFields();
                };
            });
        }
    }

    function addRow(target, row = {}) {
        if (target === "collection") {
            collectionRows.insertAdjacentHTML("beforeend", createCollectionRow(row));
            attachTableEvents(collectionRows);
        }
        if (target === "task-plan-department") {
            taskPlanDepartmentRows.insertAdjacentHTML("beforeend", createTaskPlanRow("department", row));
            attachTableEvents(taskPlanDepartmentRows);
        }
        if (target === "task-plan-outsource") {
            taskPlanOutsourceRows.insertAdjacentHTML("beforeend", createTaskPlanRow("outsource", row));
            attachTableEvents(taskPlanOutsourceRows);
        }
        if (target === "task-plan-joint") {
            taskPlanJointRows.insertAdjacentHTML("beforeend", createTaskPlanRow("joint", row));
            attachTableEvents(taskPlanJointRows);
        }
        if (target === "exec-labor") {
            execLaborRows.insertAdjacentHTML("beforeend", createExecBudgetRow("labor", row));
            attachTableEvents(execLaborRows);
        }
        if (target === "exec-outsource") {
            execOutsourceRows.insertAdjacentHTML("beforeend", createExecBudgetRow("outsource", row));
            attachTableEvents(execOutsourceRows);
        }
        if (target === "exec-cost-plan") {
            execCostPlanRows.insertAdjacentHTML("beforeend", createExecBudgetRow("cost_plan", row));
            attachTableEvents(execCostPlanRows);
        }
        if (target === "actual-labor") {
            actualLaborRows.insertAdjacentHTML("beforeend", createActualLaborRow(row));
            attachTableEvents(actualLaborRows);
            recalculateActualLaborAmounts();
        }
        if (target === "actual-labor-joint") {
            actualLaborJointRows.insertAdjacentHTML("beforeend", createActualCostRow("labor_joint", row));
            attachTableEvents(actualLaborJointRows);
        }
        if (target === "actual-as") {
            actualAsRows.insertAdjacentHTML("beforeend", createActualCostRow("as", row));
            attachTableEvents(actualAsRows);
        }
        if (target === "actual-sga") {
            actualSgaRows.insertAdjacentHTML("beforeend", createActualCostRow("sga", row));
            attachTableEvents(actualSgaRows);
        }
        updateComputedFields();
    }

    function renderSupportDeptResults(keyword, target) {
        const normalized = keyword.trim().toLowerCase();
        if (!normalized) {
            target.classList.remove("open");
            target.innerHTML = "";
            return;
        }
        const matches = supportDepartmentOptions
            .filter((item) => `${item.support_dept_code} ${item.support_dept_name}`.toLowerCase().includes(normalized))
            .slice(0, 10);
        if (!matches.length) {
            target.classList.remove("open");
            target.innerHTML = "";
            return;
        }
        target.innerHTML = matches.map((item) => `
            <button type="button" class="lookup-option" data-code="${escapeHtml(item.support_dept_code)}" data-name="${escapeHtml(item.support_dept_name)}">
                <strong>${escapeHtml(item.support_dept_code)}</strong>
                <span>${escapeHtml(item.support_dept_name)}</span>
            </button>
        `).join("");
        target.classList.add("open");
        target.querySelectorAll(".lookup-option").forEach((button) => {
            button.addEventListener("click", () => {
                applySupportDepartment({
                    support_dept_code: button.dataset.code || "",
                    support_dept_name: button.dataset.name || "",
                });
                supportDeptCodeResults.classList.remove("open");
                supportDeptNameResults.classList.remove("open");
            });
        });
    }

    function renderCostDeptResults(keyword, target, input) {
        const normalized = keyword.trim().toLowerCase();
        if (!normalized || !target) {
            target?.classList.remove("open");
            if (target) target.innerHTML = "";
            return;
        }
        const matches = costDepartmentOptions
            .filter((item) => String(item.cost_dept_name || "").toLowerCase().includes(normalized))
            .slice(0, 10);
        if (!matches.length) {
            target.classList.remove("open");
            target.innerHTML = "";
            return;
        }
        target.innerHTML = matches.map((item) => `
            <button type="button" class="lookup-option" data-cost-dept-name="${escapeHtml(item.cost_dept_name)}">
                <strong>${escapeHtml(item.cost_dept_name)}</strong>
            </button>
        `).join("");
        target.classList.add("open");
        target.querySelectorAll("[data-cost-dept-name]").forEach((button) => {
            button.addEventListener("click", () => {
                input.value = button.dataset.costDeptName || "";
                target.classList.remove("open");
                target.innerHTML = "";
                input.focus();
            });
        });
    }

    function renderCostAccountResults(keyword, target, row, mode) {
        const normalized = keyword.trim().toLowerCase();
        if (!normalized || !target) {
            target?.classList.remove("open");
            if (target) target.innerHTML = "";
            return;
        }
        const matches = costAccountOptions
            .filter((item) => `${item.account_code} ${item.account_name}`.toLowerCase().includes(normalized))
            .slice(0, 10);
        if (!matches.length) {
            target.classList.remove("open");
            target.innerHTML = "";
            return;
        }
        target.innerHTML = matches.map((item) => `
            <button type="button" class="lookup-option" data-account-code="${escapeHtml(item.account_code)}" data-account-name="${escapeHtml(item.account_name)}">
                <strong>${escapeHtml(item.account_code)} · ${escapeHtml(item.account_name)}</strong>
            </button>
        `).join("");
        target.classList.add("open");
        target.querySelectorAll("[data-account-code]").forEach((button) => {
            button.addEventListener("click", () => {
                applyCostAccountSelection(row, {
                    account_code: button.dataset.accountCode || "",
                    account_name: button.dataset.accountName || "",
                });
                target.classList.remove("open");
                target.innerHTML = "";
                const nextField = mode === "code"
                    ? row.querySelector(".cost-account-name-lookup")
                    : row.querySelector(".cost-account-code-lookup");
                nextField?.focus();
            });
        });
    }

    function applyCostAccountSelection(row, item) {
        row.querySelector(".cost-account-code-lookup").value = item.account_code || "";
        row.querySelector(".cost-account-name-lookup").value = item.account_name || "";
    }

    function renderLaborRateRows() {
        if (!laborRateRows) return;
        laborRateRows.innerHTML = laborGradeOptions.map((grade) => `
            <tr>
                <td>${escapeHtml(grade)}</td>
                <td><input class="inline-input amount-input formatted-amount-input labor-rate-input" type="text" inputmode="numeric" data-grade="${escapeHtml(grade)}" value="${escapeHtml(formatAmountInputValue(currentLaborRates?.[grade] || ""))}"></td>
            </tr>
        `).join("");
        bindCollectionFieldBehaviors(laborRateRows);
        laborRateRows.querySelectorAll(".labor-rate-input").forEach((input) => {
            input.addEventListener("input", () => {
                const grade = input.dataset.grade || "";
                currentLaborRates[grade] = parseAmount(input.value);
                writeLaborRates(currentLaborRates);
                recalculateExecLaborAmounts();
            });
        });
    }

    function openLaborRateModal() {
        renderLaborRateRows();
        laborRateModal?.classList.add("open");
    }

    function closeLaborRateModal() {
        laborRateModal?.classList.remove("open");
    }

    function recalculateExecLaborAmounts() {
        execLaborRows.querySelectorAll("tr").forEach((row) => {
            const grade = row.querySelector(".exec-labor-grade")?.value || "";
            const hours = row.querySelector(".exec-labor-hours")?.value || "";
            const amountInput = row.querySelector(".exec-labor-amount");
            if (amountInput) {
                amountInput.value = formatAmountInputValue(calculateLaborAmount(grade, hours));
            }
        });
        updateComputedFields();
    }

    function bindSupportLookup(input, target) {
        input?.addEventListener("input", (event) => {
            renderSupportDeptResults(event.target.value, target);
            const match = getSupportDeptMatch(event.target.value);
            if (match) applySupportDepartment(match);
        });
        input?.addEventListener("blur", () => {
            setTimeout(() => target.classList.remove("open"), 120);
            const match = getSupportDeptMatch(input.value);
            if (match) applySupportDepartment(match);
        });
    }

    function resolveSupportDepartmentFromLookup() {
        const match = getSupportDeptMatch(supportDeptCodeLookup?.value || "")
            || getSupportDeptMatch(supportDeptNameInput?.value || "");
        if (match) {
            applySupportDepartment(match);
        } else {
            supportDeptCodeInput.value = supportDeptCodeLookup?.value?.trim() || "";
            supportDeptNameInput.value = supportDeptNameInput?.value?.trim() || "";
        }
    }

    function createEmptyProjectEdit(code = "", name = "") {
        return {
            support_dept_code: code,
            support_dept_name: name,
            progress_rate: "",
            contract_amount: "",
            collection_amount: "",
            collection_entries: [],
            change_round: "",
            item_investment: "",
            task_plan_department_budget: "",
            task_plan_outsource_budget: "",
            task_plan_outsource_detail: "",
            task_plan_joint_operating_cost: "",
            task_plan_entries: [],
            exec_budget_labor_by_grade: "",
            exec_labor_rates: {},
            exec_budget_outsource: "",
            exec_budget_cost_plan: "",
            exec_budget_entries: [],
            actual_input_entries: [],
            actual_labor_adjustment_total: "",
            project_type: "",
            expected_as_rate: "",
            expected_sga_rate: "",
            expected_as_cost: "",
            expected_sga_budget: "",
            project_start_date: "",
            project_end_date: "",
            completion_status: "",
            notes: "",
            client_name: "",
            order_method: "",
            joint_contract: "",
            pm_name: "",
            contract_status: "",
            progress_status: "",
            work_category: "",
            review_tag: "",
            review_note: "",
            total_contract_amount: 0,
            hanmac_contract_amount: 0,
            billing_contract_amount: 0,
            billed_amount: 0,
            collection_balance_amount: 0,
            latest_billing_date: "",
            updated_at: "",
        };
    }

    function cloneProjectEditData(data) {
        return JSON.parse(JSON.stringify(data || createEmptyProjectEdit()));
    }

    function clearDynamicRows() {
        collectionRows.innerHTML = "";
        taskPlanDepartmentRows.innerHTML = "";
        taskPlanOutsourceRows.innerHTML = "";
        taskPlanJointRows.innerHTML = "";
        execLaborRows.innerHTML = "";
        execOutsourceRows.innerHTML = "";
        execCostPlanRows.innerHTML = "";
        actualLaborRows.innerHTML = "";
        actualLaborJointRows.innerHTML = "";
        actualAsRows.innerHTML = "";
        actualSgaRows.innerHTML = "";
    }

    function applyProjectEditData(editData) {
        const actualEntries = Array.isArray(editData?.actual_input_entries) ? editData.actual_input_entries : [];
        const actualLaborAdjustmentTotal = actualEntries.reduce((sum, entry) => {
            const entryGroup = entry?.group || "labor";
            if (entryGroup === "labor_adjustment") {
                return sum + Number(entry?.amount || 0);
            }
            return sum + Number(entry?.adjustment_amount || 0);
        }, 0);
        const normalized = {
            ...createEmptyProjectEdit(),
            ...(editData || {}),
            collection_entries: Array.isArray(editData?.collection_entries) ? editData.collection_entries : [],
            task_plan_entries: Array.isArray(editData?.task_plan_entries) ? editData.task_plan_entries : [],
            exec_budget_entries: Array.isArray(editData?.exec_budget_entries) ? editData.exec_budget_entries : [],
            actual_input_entries: actualEntries,
            actual_labor_adjustment_total: actualLaborAdjustmentTotal,
        };
        projectEdit = normalized;
        currentLaborRates = { ...(normalized.exec_labor_rates || {}) };
        writeLaborRates(currentLaborRates);
        if (normalized.support_dept_code) {
            projectEditCache.set(normalized.support_dept_code, normalized);
        }
        currentSelectedProjectCode = normalized.support_dept_code || currentSelectedProjectCode || "";
        supportDeptCodeInput.value = normalized.support_dept_code || "";
        supportDeptCodeLookup.value = normalized.support_dept_code || "";
        supportDeptNameInput.value = normalized.support_dept_name || "";
        projectTypeInput.value = normalized.project_type || "";
        contractAmountInput.value = normalized.contract_amount || "";
        completionStatusInput.value = normalized.completion_status || "";
        changeRoundInput.value = normalized.change_round || "";
        projectStartDateInput.value = normalized.project_start_date || "";
        projectEndDateInput.value = normalized.project_end_date || "";
        notesInput.value = normalized.notes || "";
        if (projectImportReviewBanner && projectImportReviewTag && projectImportReviewNote) {
            const hasReview = Boolean(normalized.review_tag || normalized.review_note);
            projectImportReviewBanner.classList.toggle("is-visible", hasReview);
            projectImportReviewTag.textContent = normalized.review_tag || "";
            projectImportReviewNote.textContent = normalized.review_note || "";
        }
        expectedAsRateInput.value = normalized.expected_as_rate === null || normalized.expected_as_rate === undefined || normalized.expected_as_rate === "" ? "" : String(normalized.expected_as_rate);
        expectedSgaRateInput.value = normalized.expected_sga_rate === null || normalized.expected_sga_rate === undefined || normalized.expected_sga_rate === "" ? "" : String(normalized.expected_sga_rate);
        if (editRevisionInput) {
            editRevisionInput.value = normalized.updated_at || "";
        }
        if (actualLaborAdjustmentInput) {
            actualLaborAdjustmentInput.value = formatAmountInputValue(normalized.actual_labor_adjustment_total || "");
        }

        clearDynamicRows();

        (normalized.collection_entries || []).forEach((row) => addRow("collection", row));
        if (!normalized.collection_entries?.length) {
            addRow("collection");
        }

        (normalized.task_plan_entries || []).forEach((row) => {
            if (row.group === "outsource") addRow("task-plan-outsource", row);
            else if (row.group === "joint") addRow("task-plan-joint", row);
            else addRow("task-plan-department", row);
        });
        if (!normalized.task_plan_entries?.length) {
            addRow("task-plan-department");
        }

        const execEntries = [...(normalized.exec_budget_entries || [])];
        if (!execEntries.some((row) => row.group === "outsource")) {
            execEntries.push(
                ...(normalized.task_plan_entries || [])
                    .filter((row) => row.group === "outsource")
                    .map((row) => ({ ...row, group: "outsource" }))
            );
        }

        execEntries.forEach((row) => {
            if (row.group === "outsource") addRow("exec-outsource", row);
            else if (row.group === "cost_plan") addRow("exec-cost-plan", row);
            else addRow("exec-labor", row);
        });
        if (!execEntries.length) {
            addRow("exec-labor");
        }

        (normalized.actual_input_entries || []).forEach((row) => {
            if (row.group === "labor_joint") addRow("actual-labor-joint", row);
            else if (row.group === "as") {
                const autoSyncRow = {
                    ...row,
                    auto_sync: row.auto_sync === true || (
                        String(row.label || row.note || "").trim() === "A/S비" &&
                        (!parseAmount(row.amount || 0) || parseAmount(row.amount || 0) === parseAmount(normalized.expected_as_cost || 0))
                    ),
                };
                addRow("actual-as", autoSyncRow);
            }
            else if (row.group === "sga") {
                const autoSyncRow = {
                    ...row,
                    auto_sync: row.auto_sync === true || (
                        String(row.label || row.note || "").trim() === "판관비" &&
                        (!parseAmount(row.amount || 0) || parseAmount(row.amount || 0) === parseAmount(normalized.expected_sga_budget || 0))
                    ),
                };
                addRow("actual-sga", autoSyncRow);
            }
            else if (row.group === "labor_adjustment") return;
            else addRow("actual-labor", row);
        });
        if (!normalized.actual_input_entries?.length) {
            addRow("actual-labor");
        }
        if (!actualLaborJointRows.children.length) addRow("actual-labor-joint");
        if (!actualAsRows.children.length) addRow("actual-as", { label: "A/S비", amount: normalized.expected_as_cost || "", auto_sync: true });
        if (!actualSgaRows.children.length) addRow("actual-sga", { label: "판관비", amount: normalized.expected_sga_budget || "", auto_sync: true });

        bindDateTextInputs(document);
        recalculateExecLaborAmounts();
        recalculateActualLaborAmounts();
        updateComputedFields();
    }

    async function openProjectModalForCode(code = "") {
        const normalizedCode = String(code || "").trim();
        if (!normalizedCode) {
            openProjectModalForFreshEntry(currentSelectedProjectCode || "");
            return;
        }
        try {
            const response = await fetch(`/projects/edit-data?code=${encodeURIComponent(normalizedCode)}`, {
                credentials: "same-origin",
                cache: "no-store",
                headers: { "Accept": "application/json" },
            });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            const payload = await response.json();
            if (payload?.error) {
                throw new Error(payload.error);
            }
            projectEditCache.set(normalizedCode, cloneProjectEditData(payload));
            modalOriginalEdit = cloneProjectEditData(payload);
            applyProjectEditData(payload);
            window.__suspendAutoRefresh = true;
            modal?.classList.add("open");
        } catch (error) {
            console.error(error);
            if (projectEditCache.has(normalizedCode)) {
                const cachedData = cloneProjectEditData(projectEditCache.get(normalizedCode));
                modalOriginalEdit = cloneProjectEditData(cachedData);
                applyProjectEditData(cachedData);
                window.__suspendAutoRefresh = true;
                modal?.classList.add("open");
                return;
            }
            window.alert("사업현황 데이터를 불러오지 못했습니다.");
        }
    }

    function openProjectModalForFreshEntry(code = "") {
        const normalizedCode = String(code || "").trim();
        const selectedItem = supportDepartmentOptions.find((item) => item.support_dept_code === normalizedCode);
        const emptyData = createEmptyProjectEdit(normalizedCode, selectedItem?.support_dept_name || "");
        modalOriginalEdit = cloneProjectEditData(emptyData);
        applyProjectEditData(emptyData);
        window.__suspendAutoRefresh = true;
        modal?.classList.add("open");
    }

    function closeProjectModal() {
        if (modalOriginalEdit) {
            applyProjectEditData(cloneProjectEditData(modalOriginalEdit));
        }
        modal?.classList.remove("open");
        window.__suspendAutoRefresh = false;
        supportDeptCodeResults.classList.remove("open");
        supportDeptNameResults.classList.remove("open");
        document.activeElement?.blur?.();
    }

    bindSupportLookup(supportDeptCodeLookup, supportDeptCodeResults);
    bindSupportLookup(supportDeptNameInput, supportDeptNameResults);
    bindDateTextInputs(document);

    projectStatusForm?.addEventListener("input", (event) => {
        const lookupInput = event.target.closest(".cost-dept-lookup");
        if (lookupInput) {
            const resultBox = lookupInput.parentElement?.querySelector(".cost-dept-results");
            renderCostDeptResults(lookupInput.value || "", resultBox, lookupInput);
            return;
        }
        const accountCodeInput = event.target.closest(".cost-account-code-lookup");
        if (accountCodeInput) {
            const row = accountCodeInput.closest("tr");
            const resultBox = accountCodeInput.parentElement?.querySelector(".cost-account-results");
            renderCostAccountResults(accountCodeInput.value || "", resultBox, row, "code");
            return;
        }
        const accountNameInput = event.target.closest(".cost-account-name-lookup");
        if (accountNameInput) {
            const row = accountNameInput.closest("tr");
            const resultBox = accountNameInput.parentElement?.querySelector(".cost-account-results");
            renderCostAccountResults(accountNameInput.value || "", resultBox, row, "name");
        }
    });

    projectStatusForm?.addEventListener("focusout", (event) => {
        const lookupInput = event.target.closest(".cost-dept-lookup");
        if (lookupInput) {
            const resultBox = lookupInput.parentElement?.querySelector(".cost-dept-results");
            setTimeout(() => resultBox?.classList.remove("open"), 120);
            return;
        }
        const accountLookup = event.target.closest(".cost-account-code-lookup, .cost-account-name-lookup");
        if (accountLookup) {
            const resultBox = accountLookup.parentElement?.querySelector(".cost-account-results");
            setTimeout(() => resultBox?.classList.remove("open"), 120);
        }
    });

    openLaborRateModalButton?.addEventListener("click", openLaborRateModal);
    closeLaborRateModalButton?.addEventListener("click", closeLaborRateModal);
    laborRateModal?.addEventListener("click", (event) => {
        if (event.target === laborRateModal) {
            closeLaborRateModal();
        }
    });

    contractAmountInput?.addEventListener("input", updateComputedFields);
    expectedAsRateInput?.addEventListener("change", updateComputedFields);
    expectedSgaRateInput?.addEventListener("change", updateComputedFields);
    document.getElementById("expected_as_cost_display")?.addEventListener("input", (event) => {
        event.target.value = formatAmountInputValue(event.target.value);
        syncExpectedAllocationsToActualInputs();
        updateComputedFields();
    });
    document.getElementById("expected_as_cost_display")?.addEventListener("blur", (event) => {
        event.target.value = formatAmountInputValue(event.target.value);
        syncExpectedAllocationsToActualInputs();
        updateComputedFields();
    });
    document.getElementById("expected_sga_budget_display")?.addEventListener("input", (event) => {
        event.target.value = formatAmountInputValue(event.target.value);
        syncExpectedAllocationsToActualInputs();
        updateComputedFields();
    });
    document.getElementById("expected_sga_budget_display")?.addEventListener("blur", (event) => {
        event.target.value = formatAmountInputValue(event.target.value);
        syncExpectedAllocationsToActualInputs();
        updateComputedFields();
    });
    if (actualLaborAdjustmentInput) {
        actualLaborAdjustmentInput.value = formatAmountInputValue(actualLaborAdjustmentInput.value);
        actualLaborAdjustmentInput.addEventListener("input", () => {
            actualLaborAdjustmentInput.value = formatAmountInputValue(actualLaborAdjustmentInput.value);
            updateComputedFields();
        });
        actualLaborAdjustmentInput.addEventListener("blur", () => {
            actualLaborAdjustmentInput.value = formatAmountInputValue(actualLaborAdjustmentInput.value);
            updateComputedFields();
        });
    }

    document.querySelectorAll("[data-add-row]").forEach((button) => {
        button.addEventListener("click", () => {
            addRow(button.dataset.addRow);
        });
    });

    if (projectEdit?.support_dept_code) {
        projectEditCache.set(projectEdit.support_dept_code, projectEdit);
    }
    applyProjectEditData(projectEdit);

    async function handleProjectStatusSave(event) {
        event?.preventDefault?.();
        event?.stopPropagation?.();
        event?.stopImmediatePropagation?.();
        window.__suspendAutoRefresh = true;
        resolveSupportDepartmentFromLookup();
        const form = projectStatusForm;
        if (!(form instanceof HTMLFormElement)) return;
        const saveButtons = [...form.querySelectorAll('button[type="submit"]')];
        if (saveProjectModalButton) {
            saveButtons.push(saveProjectModalButton);
        }
        saveButtons.forEach((button) => {
            button.disabled = true;
        });
        try {
            const formData = new FormData(form);
            if (selectedYear) {
                formData.set("selected_year", String(selectedYear));
            }
            const body = new URLSearchParams();
            formData.forEach((value, key) => {
                body.append(key, String(value));
            });
            const response = await fetch("/projects/save-json", {
                method: "POST",
                credentials: "same-origin",
                headers: {
                    "Accept": "application/json",
                    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
                },
                body,
            });
            const payload = await response.json().catch(() => ({}));
            if (!response.ok || payload?.ok === false) {
                throw new Error(payload?.error || `HTTP ${response.status}`);
            }
            const updatedEdit = cloneProjectEditData(payload?.project_edit || {});
            const updatedRow = payload?.project_row || null;
            if (payload?.data_version && typeof window.__setPageDataVersion === "function") {
                window.__setPageDataVersion(payload.data_version);
            }
            if (updatedEdit?.support_dept_code) {
                projectEdit = updatedEdit;
                modalOriginalEdit = cloneProjectEditData(updatedEdit);
                projectEditCache.set(updatedEdit.support_dept_code, updatedEdit);
                applyProjectEditData(updatedEdit);
            }
            if (updatedRow?.support_dept_code) {
                const projectPageHelpers = window.__projectPageHelpers || null;
                if (projectPageHelpers?.upsertProjectStatusRow) {
                    projectPageHelpers.upsertProjectStatusRow(updatedRow);
                }
                if (projectPageHelpers?.refreshAnalysisForCode) {
                    projectPageHelpers.refreshAnalysisForCode(updatedRow.support_dept_code);
                }
            }
        } catch (error) {
            console.error(error);
            window.alert(error?.message || "사업현황 저장 중 오류가 발생했습니다.");
        } finally {
            saveButtons.forEach((button) => {
                button.disabled = false;
            });
        }
    }

    projectStatusForm?.addEventListener("submit", handleProjectStatusSave);
    saveProjectModalButton?.addEventListener("click", handleProjectStatusSave);

    function renderRevenueLegend(items) {
        const legend = document.getElementById("revenueLegend");
        if (!legend) return;
        legend.innerHTML = items.map((item) => `
            <span class="legend-item">
                <span class="legend-swatch" style="background:${item.color}"></span>
                ${item.label}
            </span>
        `).join("");
    }

    function renderRevenueChart() {
        const svg = document.getElementById("revenueMixChart");
        if (!svg) return;

        const width = 1200;
        const height = 390;
        const margin = { top: 70, right: 10, bottom: 64, left: 64 };
        const isMonthlyView = revenueMixMode === "monthly";
        const items = [
            { key: "design_revenue", label: "설계", color: "#4f7cff" },
            { key: "design_other_revenue", label: "설계 외", color: "#67c7c9" },
            { key: "supervision_revenue", label: "감리", color: "#233a5a" },
            { key: "inspection_revenue", label: "점검", color: "#ffb54a" },
        ];

        renderRevenueLegend(items);

        const rows = revenueMix || [];
        const values = rows.flatMap((row) => items.map((item) => Number(row[item.key] || 0)));
        const maxValue = Math.max(...values, 1);
        const { tickStep, tickMax } = buildPositiveAxis(maxValue, 4);
        const plotWidth = width - margin.left - margin.right;
        const plotHeight = height - margin.top - margin.bottom;
        const groupWidth = plotWidth / Math.max(rows.length, 1);
        const barWidth = Math.min(groupWidth / Math.max(items.length + (isMonthlyView ? 0.9 : 0.7), 1), isMonthlyView ? 24 : 34);
        const axisBaseY = margin.top + plotHeight;

        let markup = `
            <defs>
                <linearGradient id="chartPanelBg" x1="0" x2="0" y1="0" y2="1">
                    <stop offset="0%" stop-color="#ffffff" stop-opacity="0.88" />
                    <stop offset="100%" stop-color="#f2f7fa" stop-opacity="0.96" />
                </linearGradient>
                <filter id="barShadow" x="-20%" y="-20%" width="140%" height="160%">
                    <feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="rgba(44, 68, 89, 0.14)" />
                </filter>
            </defs>
            <rect x="0" y="0" width="${width}" height="${height}" rx="8" fill="url(#chartPanelBg)"></rect>
            <rect x="${margin.left}" y="${margin.top}" width="${plotWidth}" height="${plotHeight}" rx="4" fill="rgba(255,255,255,0.66)"></rect>
        `;

        for (let value = 0; value <= tickMax; value += tickStep) {
            const y = margin.top + plotHeight - (value / tickMax) * plotHeight;
            markup += `<line x1="${margin.left}" y1="${y}" x2="${width - margin.right}" y2="${y}" stroke="#d6e2e8" stroke-dasharray="4 7"></line>`;
            markup += `<text x="${margin.left - 10}" y="${y + 5}" text-anchor="end" fill="#62717f" font-size="12">${formatAxisLabel(value)}</text>`;
        }

        markup += `<line x1="${margin.left}" y1="${axisBaseY}" x2="${width - margin.right}" y2="${axisBaseY}" stroke="#8ba0ae" stroke-width="1.2"></line>`;

        rows.forEach((row, rowIndex) => {
            const baseX = margin.left + rowIndex * groupWidth;
            items.forEach((item, itemIndex) => {
                const value = Number(row[item.key] || 0);
                const drawWidth = Math.max(barWidth - 5, 12);
                const x = baseX + 8 + itemIndex * barWidth;
                const barHeight = tickMax ? (value / tickMax) * plotHeight : 0;
                const y = margin.top + plotHeight - barHeight;
                markup += `<rect x="${x}" y="${y}" width="${drawWidth}" height="${barHeight}" rx="3" fill="${item.color}" filter="url(#barShadow)"></rect>`;
                if (!isMonthlyView || barHeight > 26) {
                    markup += `<text x="${x + drawWidth / 2}" y="${Math.max(y - 8, 18)}" text-anchor="middle" fill="#344b5e" font-size="${isMonthlyView ? 9.5 : 10.5}" font-weight="800">${formatDisplayAmount(value)}</text>`;
                }
            });
            markup += `<text x="${baseX + groupWidth / 2}" y="${height - 26}" text-anchor="middle" fill="#425464" font-size="${isMonthlyView ? 12 : 13}" font-weight="700">${row.label || row.year}</text>`;
        });

        svg.innerHTML = markup;
    }

    function buildSearchResults(keyword, dataset, renderItem) {
        if (!keyword) return [];
        const normalized = keyword.trim().toLowerCase();
        if (!normalized) return [];
        return dataset
            .filter((item) => `${item.support_dept_code} ${item.support_dept_name}`.toLowerCase().includes(normalized))
            .slice(0, 8)
            .map(renderItem)
            .join("");
    }

    function buildProjectCostDataset(keyword, yearFilterValue) {
        const normalized = (keyword || "").trim().toLowerCase();
        const baseDataset = getAggregatedProjectCostDataset(yearFilterValue);
        if (!normalized) {
            return baseDataset;
        }
        return baseDataset.filter((item) => String(item._searchText || "").includes(normalized));
    }

    function filterProjectCostDatasetByContractState(dataset, contractFilterValue) {
        if (!contractFilterValue) return dataset;
        return dataset.filter((item) => getProjectContractState(getAnalysisItem(item.support_dept_code) || item) === contractFilterValue);
    }

    function getAnalysisItem(code) {
        const costItem = aggregatedAllProjectCostMap.get(code) || null;
        const statusItem = projectStatusMap[code] || null;
        if (!costItem && !statusItem) return null;
        const supportDeptName = statusItem?.support_dept_name || costItem?.support_dept_name || code;
        const supportDeptCode = code;
        const totalRevenue = Number(statusItem?.total_revenue ?? costItem?.revenue_amount ?? 0);
        const totalExpense = Number((statusItem?.total_cost || 0) + (statusItem?.total_sga || 0) || costItem?.expense_amount || 0);
        const plannedTaskTotal = Number(statusItem?.task_plan_department_budget || 0) + Number(statusItem?.task_plan_outsource_budget || 0) + Number(statusItem?.task_plan_joint_operating_cost || 0);
        const execBudgetTotal = Number(statusItem?.exec_budget_labor_by_grade || 0) + Number(statusItem?.exec_budget_outsource || 0) + Number(statusItem?.exec_budget_cost_plan || 0);
        return {
            ...costItem,
            ...statusItem,
            support_dept_name: supportDeptName,
            support_dept_code: supportDeptCode,
            revenue_amount: Number(costItem?.revenue_amount || 0),
            expense_amount: Number(costItem?.expense_amount || 0),
            total_revenue: totalRevenue,
            total_expense: totalExpense,
            planned_task_total: plannedTaskTotal,
            exec_budget_total: execBudgetTotal,
            operating_balance: totalRevenue - totalExpense,
            year_range: formatYearRange(costItem || statusItem || {}),
        };
    }

    function getProjectContractState(item) {
        const code = item?.support_dept_code || "";
        const contractAmount = Number(item?.contract_amount || projectStatusMap[code]?.contract_amount || 0);
        const expenseAmount = Number(item?.expense_amount || item?.total_expense || 0);
        const collectionAmount = Number(
            item?.collection_amount
            || item?.total_revenue
            || projectStatusMap[code]?.collection_amount
            || 0,
        );
        const hasRelatedContract = getCurrentRelatedCodes(code).some((relatedCode) => {
            const relatedItem = getAnalysisItem(relatedCode) || projectStatusMap[relatedCode] || {};
            const relatedContractAmount = Number(relatedItem?.contract_amount || 0);
            const relatedCollectionAmount = Number(
                relatedItem?.collection_amount
                || relatedItem?.total_revenue
                || 0,
            );
            return relatedCollectionAmount > 0 || relatedContractAmount > 0;
        });
        if (collectionAmount > 0) {
            return "contracted";
        }
        if (contractAmount > 0) {
            return "contracted";
        }
        if (hasRelatedContract) {
            return "contracted";
        }
        if (expenseAmount > 0) {
            return "uncontracted_cost";
        }
        return "uncontracted";
    }

    function classifyUncontractedSpecialType(item) {
        const code = String(item?.support_dept_code || "");
        const override = String(persistedUncontractedCategoryOverrides?.[code] || "");
        if (override) return override;
        const name = String(item?.support_dept_name || "");
        if (!code.startsWith("X")) return "general";
        const normalizedName = name.replace(/\s+/g, "");
        for (const [categoryKey, keywords] of Object.entries(specialXClassificationRules || {})) {
            if ((keywords || []).some((keyword) => normalizedName.includes(String(keyword || "").replace(/\s+/g, "")))) {
                return categoryKey;
            }
        }
        return "precontract";
    }

    function getProjectContractStateLabel(state) {
        if (state === "contracted") return "계약 진행";
        if (state === "uncontracted_cost") return "미계약 비용 발생";
        return "미계약";
    }

    function getProjectContractStateBadge(item) {
        const state = getProjectContractState(item);
        return `<span class="contract-state-badge is-${state}">${getProjectContractStateLabel(state)}</span>`;
    }

    function formatSignedAmount(value) {
        const numeric = Number(value || 0);
        const prefix = numeric > 0 ? "+" : "";
        return `${prefix}${formatNumber(numeric)}`;
    }

    function differenceClass(value) {
        const numeric = Number(value || 0);
        if (numeric > 0) return "delta-positive";
        if (numeric < 0) return "delta-negative";
        return "";
    }

    function getBreakdownList(code, kind) {
        const item = projectAccountBreakdowns?.[code] || {};
        return Array.isArray(item?.[kind]) ? item[kind] : [];
    }

    function normalizeDetailEntries(entries, fallbackLabel) {
        if (!Array.isArray(entries) || !entries.length) return [];
        return entries.map((entry, index) => ({
            ...entry,
            label:
                entry.label
                || [entry.grade, entry.minutes ? formatDurationInputValue(entry.minutes) : ""].filter(Boolean).join(" / ")
                || [entry.grade, entry.hours ? `예상 ${formatHourInputValue(entry.hours)}시간` : ""].filter(Boolean).join(" / ")
                || [entry.account_code, entry.account_name].filter(Boolean).join(" / ")
                || [entry.dept_name, entry.work_name].filter(Boolean).join(" / ")
                || entry.reference
                || entry.note
                || `${fallbackLabel} ${index + 1}`,
            amount: Number(entry.amount || 0),
            adjustment_amount: Number(entry.adjustment_amount || 0),
        })).filter((entry) => entry.label || entry.amount || entry.adjustment_amount);
    }

    function renderComparisonDetailList(entries, emptyLabel) {
        if (!entries.length) {
            return `<div class="comparison-detail-item"><span>${emptyLabel}</span><strong>-</strong></div>`;
        }
        return entries.map((entry) => `
            <div class="comparison-detail-item">
                <span>${escapeHtml(entry.label || "-")}</span>
                <strong>${formatNumber(entry.amount || 0)}</strong>
            </div>
        `).join("");
    }

    function stripLeadingAccountCode(label) {
        return String(label || "").replace(/^\d{6,8}\s*(?:[·ㆍ]|\/)\s*/u, "").trim();
    }

    function splitProjectEntryLabel(label) {
        const normalized = String(label || "").trim();
        const match = normalized.match(/^([A-Z]\d{5})\s*[·ㆍ]\s*(.+)$/u);
        if (!match) {
            return {
                projectCode: "",
                detailLabel: stripLeadingAccountCode(normalized) || "-",
            };
        }
        return {
            projectCode: match[1],
            detailLabel: stripLeadingAccountCode(match[2]) || "-",
        };
    }

    function buildGroupedComparisonEntries(entries) {
        const groups = [];
        const groupMap = new Map();
        (entries || []).forEach((entry) => {
            const { projectCode, detailLabel } = splitProjectEntryLabel(entry.label);
            const groupCode = projectCode || "기타";
            if (!groupMap.has(groupCode)) {
                const group = {
                    code: groupCode,
                    items: [],
                    subtotal: 0,
                };
                groupMap.set(groupCode, group);
                groups.push(group);
            }
            const group = groupMap.get(groupCode);
            const amount = Number(entry.amount || 0);
            group.items.push({
                ...entry,
                label: detailLabel,
                amount,
            });
            group.subtotal += amount;
        });
        return {
            groups,
            total: groups.reduce((sum, group) => sum + Number(group.subtotal || 0), 0),
        };
    }

    function getProjectPairOrder(plannedEntries, actualEntries) {
        const order = [];
        const seen = new Set();
        [...plannedEntries, ...actualEntries].forEach((entry) => {
            const projectCode = splitProjectEntryLabel(entry.label).projectCode || "기타";
            if (seen.has(projectCode)) return;
            seen.add(projectCode);
            order.push(projectCode);
        });
        return order;
    }

    function buildComparisonSideRows(group, emptyLabel, placeholderCount = 0, warningConfig = null) {
        const rows = [];
        const items = group?.items || [];
        const groupCode = group?.code || "기타";
        if (items.length) {
            items.forEach((entry) => {
                rows.push({
                    type: "item",
                    label: `${groupCode} · ${entry.label || "-"}`,
                    amount: Number(entry.amount || 0),
                });
            });
        } else {
            if (warningConfig) {
                const warningRowCount = Math.max(1, Number(warningConfig.rowSpan || 1));
                const warningRowIndex = Math.floor((warningRowCount - 1) / 2);
                for (let index = 0; index < warningRowCount; index += 1) {
                    if (index === warningRowIndex) {
                        rows.push({
                            type: "warning",
                            title: warningConfig.title || "",
                            subtitle: warningConfig.subtitle || "",
                        });
                    } else {
                        rows.push({
                            type: "placeholder",
                            label: "",
                            amount: null,
                        });
                    }
                }
            } else {
                rows.push({
                    type: "empty",
                    label: emptyLabel ? `${groupCode} · ${emptyLabel}` : "",
                    amount: null,
                });
            }
        }
        if (!warningConfig) {
            for (let index = 0; index < placeholderCount; index += 1) {
                rows.push({
                    type: "placeholder",
                    label: "",
                    amount: null,
                });
            }
        }
        rows.push({
            type: "subtotal",
            label: `${groupCode} 소계`,
            amount: Number(group?.subtotal || 0),
        });
        return rows;
    }

    function renderComparisonSideRows(rows) {
        return rows.map((row) => {
            if (row.type === "warning") {
                return `
                    <div class="comparison-detail-item is-warning">
                        <div class="comparison-detail-warning-box">
                            <span>${escapeHtml(row.title || "")}</span>
                            <strong>${escapeHtml(row.subtitle || "")}</strong>
                        </div>
                    </div>
                `;
            }
            if (row.type === "placeholder") {
                return `
                    <div class="comparison-detail-item is-placeholder">
                        <span>-</span>
                        <strong>-</strong>
                    </div>
                `;
            }
            if (row.type === "empty") {
                return `
                    <div class="comparison-detail-item">
                        <span>${escapeHtml(row.label || "")}</span>
                        <strong></strong>
                    </div>
                `;
            }
            const extraClass = row.type === "subtotal" ? " is-subtotal" : "";
            return `
                <div class="comparison-detail-item${extraClass}">
                    <span>${escapeHtml(row.label || "-")}</span>
                    <strong>${row.amount === null || row.amount === undefined ? "" : formatNumber(row.amount)}</strong>
                </div>
            `;
        }).join("");
    }

    function renderGroupedComparisonSection(entries, emptyLabel, title, totalLabel = "총계") {
        const grouped = buildGroupedComparisonEntries(entries);
        const rows = [];
        grouped.groups.forEach((group) => {
            rows.push(...buildComparisonSideRows(group, emptyLabel, 0));
        });
        return `
            <section class="comparison-detail-group">
                ${title ? `<h5>${escapeHtml(title)}</h5>` : ""}
                <div class="comparison-detail-list">
                    ${renderComparisonSideRows(rows)}
                    <div class="comparison-detail-item is-total">
                        <span>${escapeHtml(totalLabel)}</span>
                        <strong>${formatNumber(grouped.total)}</strong>
                    </div>
                </div>
            </section>
        `;
    }

    function renderAlignedGroupedComparisonSplit(leftEntries, rightEntries, leftTitle, rightTitle, emptyLeftLabel, emptyRightLabel) {
        const leftGrouped = buildGroupedComparisonEntries(leftEntries);
        const rightGrouped = buildGroupedComparisonEntries(rightEntries);
        const leftMap = new Map(leftGrouped.groups.map((group) => [group.code, group]));
        const rightMap = new Map(rightGrouped.groups.map((group) => [group.code, group]));
        const order = getProjectPairOrder(leftEntries, rightEntries);
        const leftRows = [];
        const rightRows = [];
        order.forEach((projectCode) => {
            const leftGroup = leftMap.get(projectCode) || { code: projectCode, items: [], subtotal: 0 };
            const rightGroup = rightMap.get(projectCode) || { code: projectCode, items: [], subtotal: 0 };
            const leftItemsCount = leftGroup.items?.length ? leftGroup.items.length : 1;
            const rightItemsCount = rightGroup.items?.length ? rightGroup.items.length : 1;
            const maxItemsCount = Math.max(leftItemsCount, rightItemsCount);
            leftRows.push(...buildComparisonSideRows(leftGroup, emptyLeftLabel, maxItemsCount - leftItemsCount));
            rightRows.push(...buildComparisonSideRows(rightGroup, emptyRightLabel, maxItemsCount - rightItemsCount));
        });
        return `
            <div class="comparison-detail-split">
                <section class="comparison-detail-group">
                    <h5>${escapeHtml(leftTitle)}</h5>
                    <div class="comparison-detail-list">
                        ${renderComparisonSideRows(leftRows)}
                        <div class="comparison-detail-item is-total">
                            <span>${escapeHtml(`${leftTitle} 총계`)}</span>
                            <strong>${formatNumber(leftGrouped.total)}</strong>
                        </div>
                    </div>
                </section>
                <section class="comparison-detail-group">
                    <h5>${escapeHtml(rightTitle)}</h5>
                    <div class="comparison-detail-list">
                        ${renderComparisonSideRows(rightRows)}
                        <div class="comparison-detail-item is-total">
                            <span>${escapeHtml(`${rightTitle} 총계`)}</span>
                            <strong>${formatNumber(rightGrouped.total)}</strong>
                        </div>
                    </div>
                </section>
            </div>
        `;
    }

    function renderComparisonSideBox(rows, total, totalLabel, title = "") {
        return `
            <section class="comparison-detail-group">
                ${title ? `<h5>${escapeHtml(title)}</h5>` : ""}
                <div class="comparison-detail-list">
                    ${renderComparisonSideRows(rows)}
                    <div class="comparison-detail-item is-total">
                        <span>${escapeHtml(totalLabel)}</span>
                        <strong>${formatNumber(total)}</strong>
                    </div>
                </div>
            </section>
        `;
    }

    function renderLaborComparisonDetails(plannedEntries, erpEntries, jointEntries) {
        const plannedGrouped = buildGroupedComparisonEntries(plannedEntries);
        const erpGrouped = buildGroupedComparisonEntries(erpEntries);
        const jointGrouped = buildGroupedComparisonEntries(jointEntries);
        const plannedMap = new Map(plannedGrouped.groups.map((group) => [group.code, group]));
        const erpMap = new Map(erpGrouped.groups.map((group) => [group.code, group]));
        const jointMap = new Map(jointGrouped.groups.map((group) => [group.code, group]));
        const order = getProjectPairOrder([...plannedEntries, ...erpEntries], jointEntries);
        const detailRowHeight = 38;

        function renderProjectBlock(group, rowCount, mode = "default") {
            const items = group.items || [];
            const detailRows = [];
            if (mode === "joint-warning" && !items.length) {
                for (let index = 0; index < rowCount; index += 1) {
                    detailRows.push('<div class="comparison-detail-item is-placeholder"><span></span><strong></strong></div>');
                }
                const detailHeight = Math.max(rowCount, 1) * detailRowHeight;
                return `
                    <div class="comparison-project-block">
                        <div class="comparison-project-detail-area is-empty" style="min-height:${detailHeight}px;">
                            ${detailRows.join("")}
                            <div class="comparison-project-warning-anchor">
                                <div class="comparison-detail-warning-box">
                                    <span>합사인건비 반영 필요</span>
                                    <strong>(사업관리팀 자료 회신 대기 중)</strong>
                                </div>
                            </div>
                        </div>
                        <div class="comparison-detail-item is-subtotal">
                            <span>${group.code} 소계</span>
                            <strong>${formatNumber(group.subtotal || 0)}</strong>
                        </div>
                    </div>
                `;
            }

            for (let index = 0; index < rowCount; index += 1) {
                const item = items[index];
                if (item) {
                    detailRows.push(`
                        <div class="comparison-detail-item">
                            <span>${item.label}</span>
                            <strong>${formatNumber(item.amount)}</strong>
                        </div>
                    `);
                } else {
                    detailRows.push('<div class="comparison-detail-item is-placeholder"><span></span><strong></strong></div>');
                }
            }

            return `
                <div class="comparison-project-block">
                    <div class="comparison-project-detail-area">
                        ${detailRows.join("")}
                    </div>
                    <div class="comparison-detail-item is-subtotal">
                        <span>${group.code} 소계</span>
                        <strong>${formatNumber(group.subtotal || 0)}</strong>
                    </div>
                </div>
            `;
        }

        const plannedBlocks = [];
        const erpBlocks = [];
        const jointBlocks = [];

        order.forEach((projectCode) => {
            const plannedGroup = plannedMap.get(projectCode) || { code: projectCode, items: [], subtotal: 0 };
            const erpGroup = erpMap.get(projectCode) || { code: projectCode, items: [], subtotal: 0 };
            const jointGroup = jointMap.get(projectCode) || { code: projectCode, items: [], subtotal: 0 };
            const maxCount = Math.max(
                (plannedGroup.items || []).length || 1,
                (erpGroup.items || []).length || 1,
                (jointGroup.items || []).length || 1,
            );
            plannedBlocks.push(renderProjectBlock(plannedGroup, maxCount));
            erpBlocks.push(renderProjectBlock(erpGroup, maxCount));
            jointBlocks.push(renderProjectBlock(jointGroup, maxCount, !(jointGroup.items || []).length ? "joint-warning" : "default"));
        });

        return `
            <div class="comparison-detail-grid">
                <section class="comparison-detail-group">
                    <h5>계획/입력 세부</h5>
                    <div class="comparison-detail-list">
                        ${plannedBlocks.join("")}
                        <div class="comparison-detail-item is-total">
                            <span>총계</span>
                            <strong>${formatNumber(plannedGrouped.total)}</strong>
                        </div>
                    </div>
                </section>
                <section class="comparison-detail-group">
                    <h5>실제 집행 세부</h5>
                    <div class="comparison-detail-split">
                        <section class="comparison-detail-group">
                            <div class="comparison-detail-list">
                                ${erpBlocks.join("")}
                                <div class="comparison-detail-item is-total">
                                    <span>인건비(ERP) 총계</span>
                                    <strong>${formatNumber(erpGrouped.total)}</strong>
                                </div>
                            </div>
                        </section>
                        <section class="comparison-detail-group">
                            <div class="comparison-detail-list">
                                ${jointBlocks.join("")}
                                <div class="comparison-detail-item is-total">
                                    <span>인건비(합사) 총계</span>
                                    <strong>${formatNumber(jointGrouped.total)}</strong>
                                </div>
                            </div>
                        </section>
                    </div>
                </section>
            </div>
        `;
    }

    function renderPairedComparisonDetails(plannedEntries, actualEntries, emptyPlannedLabel, emptyActualLabel) {
        const planned = buildGroupedComparisonEntries(plannedEntries);
        const actual = buildGroupedComparisonEntries(actualEntries);
        const plannedMap = new Map(planned.groups.map((group) => [group.code, group]));
        const actualMap = new Map(actual.groups.map((group) => [group.code, group]));
        const order = getProjectPairOrder(plannedEntries, actualEntries);
        const plannedRows = [];
        const actualRows = [];
        order.forEach((projectCode) => {
            const plannedGroup = plannedMap.get(projectCode) || { code: projectCode, items: [], subtotal: 0 };
            const actualGroup = actualMap.get(projectCode) || { code: projectCode, items: [], subtotal: 0 };
            const plannedItemsCount = plannedGroup.items?.length ? plannedGroup.items.length : 1;
            const actualItemsCount = actualGroup.items?.length ? actualGroup.items.length : 1;
            const maxItemsCount = Math.max(plannedItemsCount, actualItemsCount);
            plannedRows.push(...buildComparisonSideRows(plannedGroup, emptyPlannedLabel, maxItemsCount - plannedItemsCount));
            actualRows.push(...buildComparisonSideRows(actualGroup, emptyActualLabel, maxItemsCount - actualItemsCount));
        });
        return `
            <div class="comparison-detail-grid">
                <section class="comparison-detail-group">
                    <h5>계획/입력 세부</h5>
                    <div class="comparison-detail-list">
                        ${renderComparisonSideRows(plannedRows)}
                        <div class="comparison-detail-item is-total">
                            <span>총계</span>
                            <strong>${formatNumber(planned.total)}</strong>
                        </div>
                    </div>
                </section>
                <section class="comparison-detail-group">
                    <h5>실제 집행 세부</h5>
                    <div class="comparison-detail-list">
                        ${renderComparisonSideRows(actualRows)}
                        <div class="comparison-detail-item is-total">
                            <span>총계</span>
                            <strong>${formatNumber(actual.total)}</strong>
                        </div>
                    </div>
                </section>
            </div>
        `;
    }

    function sumEntryAmounts(entries) {
        return (entries || []).reduce((sum, entry) => sum + Number(entry?.amount || 0), 0);
    }

    function getExecBudgetGroupEntries(item, group, fallbackLabel) {
        const entries = Array.isArray(item?.exec_budget_entries)
            ? item.exec_budget_entries.filter((entry) => (entry?.group || "labor") === group)
            : [];
        const normalized = normalizeDetailEntries(entries, fallbackLabel);
        if (normalized.length) return normalized;
        const fallbackAmountMap = {
            labor: Number(item?.exec_budget_labor_by_grade || 0),
            outsource: Number(item?.exec_budget_outsource || 0),
            cost_plan: Number(item?.exec_budget_cost_plan || 0),
        };
        const amount = fallbackAmountMap[group] || 0;
        return amount ? [{ label: fallbackLabel, amount }] : [];
    }

    function getActualGroupEntries(item, group, fallbackLabel) {
        const entries = Array.isArray(item?.actual_input_entries)
            ? item.actual_input_entries.filter((entry) => {
                const entryGroup = entry?.group || "labor";
                if (group === "labor") {
                    return entryGroup === "labor" || entryGroup === "labor_adjustment";
                }
                if (group === "labor_joint") {
                    return entryGroup === "labor_joint";
                }
                return entryGroup === group;
            })
            : [];
        const normalized = normalizeDetailEntries(entries, fallbackLabel);
        if (group === "labor") {
            const expanded = [];
            normalized.forEach((entry) => {
                const entryGroup = entry.group || "labor";
                if (entryGroup === "labor_adjustment") {
                    expanded.push({
                        label: entry.label === "인건비 조정비용" ? "인건비 조정" : (entry.label || "인건비 조정"),
                        amount: Number(entry.amount || 0),
                    });
                    return;
                }
                if (Number(entry.amount || 0)) {
                    expanded.push({
                        ...entry,
                        amount: Number(entry.amount || 0),
                    });
                }
                if (Number(entry.adjustment_amount || 0)) {
                    expanded.push({
                        label: `${entry.label} / 조정`,
                        amount: Number(entry.adjustment_amount || 0),
                    });
                }
            });
            return expanded;
        }
        return normalized;
    }

    function getCostBreakdownEntries(code) {
        return Array.isArray(projectAccountBreakdowns?.[code]?.cost) ? projectAccountBreakdowns[code].cost : [];
    }

    function sumNumericFields(target, source, keys) {
        keys.forEach((key) => {
            target[key] = Number(target[key] || 0) + Number(source?.[key] || 0);
        });
    }

    function mergeLabeledAmountEntries(entries) {
        const merged = new Map();
        (entries || []).forEach((entry) => {
            const label = String(entry?.label || entry?.reference || entry?.note || "").trim() || "미분류";
            const current = merged.get(label) || { label, amount: 0 };
            current.amount += Number(entry?.amount || 0);
            merged.set(label, current);
        });
        return [...merged.values()].sort((a, b) => Number(b.amount || 0) - Number(a.amount || 0));
    }

    function prefixProjectEntries(item, entries, fallbackLabel) {
        return normalizeDetailEntries(entries, fallbackLabel).map((entry) => ({
            ...entry,
            label: `${item.support_dept_code} · ${entry.label}`,
            amount: Number(entry.amount || 0),
        }));
    }

    function getCombinedBreakdownList(codes, kind) {
        const merged = [];
        (codes || []).forEach((code) => {
            const entries = getBreakdownList(code, kind);
            entries.forEach((entry) => {
                merged.push({
                    label: `${code} · ${entry.label || "-"}`,
                    amount: Number(entry.amount || 0),
                });
            });
        });
        return mergeLabeledAmountEntries(merged);
    }

    function aggregateAnalysisItem(baseItem, relatedItems = []) {
        if (!baseItem) return null;
        const items = [baseItem, ...relatedItems].filter(Boolean);
        const aggregated = {
            ...baseItem,
            _aggregateCodes: items.map((item) => item.support_dept_code),
            _relatedItems: relatedItems,
            min_year: Math.min(...items.map((item) => Number(item.min_year || item.year || 0)).filter(Boolean)),
            max_year: Math.max(...items.map((item) => Number(item.max_year || item.year || 0)).filter(Boolean)),
            latest_year: baseItem.latest_year,
            latest_month: baseItem.latest_month,
            revenue_amount: 0,
            expense_amount: 0,
            total_revenue: 0,
            total_expense: 0,
            total_cost: 0,
            total_sga: 0,
            contract_amount: 0,
            collection_amount: 0,
            planned_task_total: 0,
            exec_budget_total: 0,
            operating_balance: 0,
            expected_as_cost: 0,
            expected_sga_budget: 0,
            task_plan_department_budget: 0,
            task_plan_outsource_budget: 0,
            task_plan_joint_operating_cost: 0,
            exec_budget_labor_by_grade: 0,
            exec_budget_outsource: 0,
            exec_budget_cost_plan: 0,
            actual_labor: 0,
            actual_outsource: 0,
            item_investment: 0,
            collection_entries: [],
            task_plan_entries: [],
            exec_budget_entries: [],
            actual_input_entries: [],
        };
        const sumKeys = [
            "revenue_amount",
            "expense_amount",
            "total_revenue",
            "total_cost",
            "total_sga",
            "contract_amount",
            "collection_amount",
            "planned_task_total",
            "exec_budget_total",
            "expected_as_cost",
            "expected_sga_budget",
            "task_plan_department_budget",
            "task_plan_outsource_budget",
            "task_plan_joint_operating_cost",
            "exec_budget_labor_by_grade",
            "exec_budget_outsource",
            "exec_budget_cost_plan",
            "actual_labor",
            "actual_outsource",
            "item_investment",
        ];
        items.forEach((item) => {
            sumNumericFields(aggregated, item, sumKeys);
            aggregated.collection_entries.push(...prefixProjectEntries(item, item.collection_entries, "수금 입력"));
            aggregated.task_plan_entries.push(...prefixProjectEntries(item, item.task_plan_entries, "과업수행계획"));
            aggregated.exec_budget_entries.push(...prefixProjectEntries(item, item.exec_budget_entries, "실행예산"));
            aggregated.actual_input_entries.push(...prefixProjectEntries(item, item.actual_input_entries, "실투입"));
            const itemLatest = Number(item.latest_year || 0) * 100 + Number(item.latest_month || 0);
            const aggregatedLatest = Number(aggregated.latest_year || 0) * 100 + Number(aggregated.latest_month || 0);
            if (itemLatest > aggregatedLatest) {
                aggregated.latest_year = item.latest_year;
                aggregated.latest_month = item.latest_month;
            }
        });
        aggregated.total_expense = Number(aggregated.total_cost || 0) + Number(aggregated.total_sga || 0);
        aggregated.operating_balance = Number(aggregated.total_revenue || 0) - Number(aggregated.total_expense || 0);
        return aggregated;
    }

    function isDesignOutsourceEntry(entry) {
        const label = String(entry?.label || "");
        return label.includes("기술협력비");
    }

    function formatComparisonCell(value) {
        return value === null || value === undefined ? "" : formatNumber(value);
    }

    function formatComparisonDiff(planned, actual) {
        const diff = Number(planned || 0) - Number(actual || 0);
        return `<span class="${differenceClass(diff)}">${formatSignedAmount(diff)}</span>`;
    }

    function buildComparisonDetails(item) {
        const aggregateCodes = Array.isArray(item?._aggregateCodes) && item._aggregateCodes.length
            ? item._aggregateCodes
            : [item.support_dept_code];
        const revenuePlanned = normalizeDetailEntries(item.collection_entries, "수금 입력");
        const revenueActual = getCombinedBreakdownList(aggregateCodes, "revenue");
        const laborPlanned = getExecBudgetGroupEntries(item, "labor", "직급별 인건비");
        const outsourcePlanned = getExecBudgetGroupEntries(item, "outsource", "외주비");
        const overheadPlanned = getExecBudgetGroupEntries(item, "cost_plan", "계정별비용계획");
        const laborActual = getActualGroupEntries(item, "labor", "실투입 인건비");
        const laborJointActual = getActualGroupEntries(item, "labor_joint", "실투입 인건비(합사)");
        const asActual = getActualGroupEntries(item, "as", "실투입 A/S비");
        const sgaInputActual = getActualGroupEntries(item, "sga", "실투입 판관비");
        const costEntries = aggregateCodes.flatMap((code) => getCostBreakdownEntries(code).map((entry) => ({
            label: `${code} · ${entry.label || "-"}`,
            amount: Number(entry.amount || 0),
        })));
        const outsourceActual = costEntries.filter(isDesignOutsourceEntry);
        const overheadActual = costEntries.filter((entry) => !isDesignOutsourceEntry(entry));
        return {
            collection: {
                planned: revenuePlanned,
                actual: revenueActual,
            },
            labor: {
                planned: laborPlanned,
                actual: laborActual,
                actual_joint: laborJointActual,
            },
            outsource: {
                planned: outsourcePlanned,
                actual: outsourceActual,
            },
            overhead: {
                planned: overheadPlanned,
                actual: overheadActual,
            },
            as: {
                planned: item.expected_as_cost ? [{ label: "예상 A/S비", amount: Number(item.expected_as_cost || 0) }] : [],
                actual: asActual,
            },
            sga: {
                planned: item.expected_sga_budget ? [{ label: "예상 판관비", amount: Number(item.expected_sga_budget || 0) }] : [],
                actual: sgaInputActual,
            },
        };
    }

    function renderAnalysisHero(item) {
        const editUrl = getProjectEditUrl(item.support_dept_code);
        return `
            <div class="analysis-hero">
                <div class="analysis-hero-header">
                    <div class="analysis-title">
                        <h3>${item.support_dept_name}</h3>
                        <p>${item.support_dept_code} · ${item.year_range} · 최신 반영 ${detailValue(item.latest_year, "-")}년 ${detailValue(item.latest_month, "-")}월</p>
                    </div>
                    <div class="actions">
                        <button type="button" class="button-link button-secondary button-icon" data-set-quick-link="${item.support_dept_code}" title="바로가기 설정" aria-label="바로가기 설정">
                            <svg viewBox="0 0 24 24" aria-hidden="true">
                                <path d="M7 4h10a1 1 0 0 1 1 1v15l-6-3-6 3V5a1 1 0 0 1 1-1z"></path>
                            </svg>
                            <span class="sr-only">바로가기 설정</span>
                        </button>
                        <a class="button-link button-secondary button-icon" href="${editUrl}" data-edit-code="${item.support_dept_code}" title="수정" aria-label="수정">
                            <svg viewBox="0 0 24 24" aria-hidden="true">
                                <path d="M4 20h4l10-10-4-4L4 16v4z"></path>
                                <path d="M13 7l4 4"></path>
                            </svg>
                            <span class="sr-only">수정</span>
                        </a>
                    </div>
                </div>
                <div class="analysis-stat-grid">
                    <div class="analysis-stat">
                        <span>사업종류</span>
                        <strong>${detailValue(item.project_type)}</strong>
                    </div>
                    <div class="analysis-stat">
                        <span>진행율</span>
                        <strong>${Number(item.progress_rate || 0).toFixed(1)}%</strong>
                    </div>
                    <div class="analysis-stat">
                        <span>종료여부</span>
                        <strong>${detailValue(item.completion_status)}</strong>
                    </div>
                    <div class="analysis-stat">
                        <span>시작일</span>
                        <strong>${detailValue(item.project_start_date)}</strong>
                    </div>
                    <div class="analysis-stat">
                        <span>종료일</span>
                        <strong>${detailValue(item.project_end_date)}</strong>
                    </div>
                </div>
            </div>
        `;
    }

    function formatMonthLabel(month) {
        return `${Number(month || 0)}월`;
    }

    function getUncontractedCategoryLabel(category) {
        const matched = (uncontractedCategoryOptions || []).find((item) => item.value === category);
        return matched?.label || "일반 미계약";
    }

    function getProjectCodeYear(code) {
        const match = String(code || "").match(/^[A-Z](\d{2})/i);
        return match ? Number(match[1]) : 0;
    }

    function getProjectCodeFullYear(code) {
        const shortYear = getProjectCodeYear(code);
        if (!shortYear) return 0;
        return 2000 + shortYear;
    }

    function hasProjectVariantMarker(name) {
        return /(\d+\s*차|[nN]\s*차|변경|보완|지연보상금|가칭|연차|년분)/.test(String(name || ""));
    }

    function getCurrentRelatedCodes(baseCode) {
        const selectionMap = window.__projectRelatedSelections;
        if (!(selectionMap instanceof Map)) return [];
        return [...(selectionMap.get(baseCode) || new Set())];
    }

    let uncontractedDetailSelection = { type: "", year: null, month: null };
    let uncontractedDetailVisible = false;

    function getUncontractedYearRange() {
        const currentYear = new Date().getFullYear();
        const dataYears = [...new Set(projectCostRows.map((item) => Number(item.year || 0)).filter(Boolean))].sort((a, b) => a - b);
        const minYear = 1994;
        const maxYear = Math.max(currentYear, dataYears[dataYears.length - 1] || currentYear);
        const years = Array.from({ length: Math.max(0, maxYear - minYear + 1) }, (_, index) => minYear + index);
        const fallbackStart = Number(persistedUncontractedFilterState.start_year || years[0] || minYear);
        const fallbackEnd = Number(persistedUncontractedFilterState.end_year || years[years.length - 1] || maxYear);
        return {
            allYears: years,
            startYear: Number(
                document.getElementById("uncontractedYearStart")?.value
                || persistedUncontractedFilterState.start_year
                || fallbackStart
                || 0
            ),
            endYear: Number(
                document.getElementById("uncontractedYearEnd")?.value
                || persistedUncontractedFilterState.end_year
                || fallbackEnd
                || 0
            ),
        };
    }

    function buildUncontractedDashboardData() {
        const { startYear, endYear } = getUncontractedYearRange();
        const filteredRows = projectCostRows.filter((row) => {
            const year = Number(row.year || 0);
            return year >= startYear && year <= endYear;
        });
        const filteredAggregate = aggregateProjectCostRows(filteredRows).filter((item) => {
            const merged = getAnalysisItem(item.support_dept_code) || item;
            return getProjectContractState(merged) !== "contracted";
        }).filter((item) => Number(item.expense_amount || 0) > 0);
        const filteredAggregateMap = new Map(filteredAggregate.map((item) => [item.support_dept_code, item]));
        const filteredExpenseRows = filteredRows.filter((row) => {
            if (!filteredAggregateMap.has(row.support_dept_code)) return false;
            return Number(row.expense_amount || 0) > 0;
        });
        const summary = {
            uncontracted_projects: filteredAggregate.length,
            cost_incurred_projects: filteredAggregate.length,
            expense_amount: filteredAggregate.reduce((sum, item) => sum + Number(item.expense_amount || 0), 0),
            revenue_amount: filteredAggregate.reduce((sum, item) => sum + Number(item.revenue_amount || 0), 0),
            review_needed_projects: filteredAggregate.filter((item) => Boolean(projectStatusMap[item.support_dept_code]?.review_tag)).length,
            precontract_projects: 0,
            precontract_expense_amount: 0,
            corporate_rnd_projects: 0,
            corporate_rnd_expense_amount: 0,
            external_research_projects: 0,
            external_research_expense_amount: 0,
        };
        filteredAggregate.forEach((item) => {
            const type = classifyUncontractedSpecialType(item);
            if (type === "corporate_rnd") {
                summary.corporate_rnd_projects += 1;
                summary.corporate_rnd_expense_amount += Number(item.expense_amount || 0);
            } else if (type === "external_research") {
                summary.external_research_projects += 1;
                summary.external_research_expense_amount += Number(item.expense_amount || 0);
            } else if (type === "precontract") {
                summary.precontract_projects += 1;
                summary.precontract_expense_amount += Number(item.expense_amount || 0);
            }
        });

        const yearlyMap = new Map();
        filteredExpenseRows.forEach((row) => {
            const merged = getAnalysisItem(row.support_dept_code) || row;
            if (getProjectContractState(merged) === "contracted") return;
            const year = Number(row.year || 0);
            const current = yearlyMap.get(year) || {
                year,
                uncontracted_projects: new Set(),
                cost_incurred_projects: new Set(),
                expense_amount: 0,
            };
            current.uncontracted_projects.add(row.support_dept_code);
            current.cost_incurred_projects.add(row.support_dept_code);
            current.expense_amount += Number(row.expense_amount || 0);
            yearlyMap.set(year, current);
        });
        const yearlyRows = [];
        for (let year = startYear; year <= endYear; year += 1) {
            const row = yearlyMap.get(year);
            yearlyRows.push({
                year,
                uncontracted_projects: row?.uncontracted_projects.size || 0,
                cost_incurred_projects: row?.cost_incurred_projects.size || 0,
                expense_amount: row?.expense_amount || 0,
            });
        }

        const focusYear = Number(uncontractedDetailSelection.year || yearlyRows[yearlyRows.length - 1]?.year || endYear || 0);
        const monthlyMap = new Map();
        (projectMonthlyCostRows || []).forEach((row) => {
            const year = Number(row.year || 0);
            if (year !== focusYear) return;
            const merged = getAnalysisItem(row.support_dept_code) || row;
            if (getProjectContractState(merged) === "contracted") return;
            const month = Number(row.month || 0);
            const current = monthlyMap.get(month) || {
                month,
                cost_incurred_projects: new Set(),
                expense_amount: 0,
            };
            const expenseAmount = Number(row.cost_sum || 0) + Number(row.sga_sum || 0);
            if (expenseAmount <= 0) return;
            if (expenseAmount > 0) current.cost_incurred_projects.add(row.support_dept_code);
            current.expense_amount += expenseAmount;
            monthlyMap.set(month, current);
        });
        const monthlyRows = [...monthlyMap.values()].sort((a, b) => a.month - b.month).map((row) => ({
            month: row.month,
            cost_incurred_projects: row.cost_incurred_projects.size,
            expense_amount: row.expense_amount,
        }));

        let detailRows = [];
        let excludedDetailRows = [];
        const detailTitle = "세부 내역";
        if (uncontractedDetailSelection.type === "month" && uncontractedDetailSelection.year && uncontractedDetailSelection.month) {
            const map = new Map();
            (projectMonthlyCostRows || []).forEach((row) => {
                if (Number(row.year || 0) !== Number(uncontractedDetailSelection.year)) return;
                if (Number(row.month || 0) !== Number(uncontractedDetailSelection.month)) return;
                const merged = getAnalysisItem(row.support_dept_code) || row;
                if (getProjectContractState(merged) === "contracted") return;
                const current = map.get(row.support_dept_code) || {
                    support_dept_code: row.support_dept_code,
                    support_dept_name: row.support_dept_name,
                    expense_amount: 0,
                };
                current.expense_amount += Number(row.cost_sum || 0) + Number(row.sga_sum || 0);
                map.set(row.support_dept_code, current);
            });
            detailRows = [...map.values()].sort((a, b) => Number(b.expense_amount || 0) - Number(a.expense_amount || 0));
        } else if (uncontractedDetailSelection.type === "year" && uncontractedDetailSelection.year) {
            const map = new Map();
            const excludedMap = new Map();
            filteredRows.forEach((row) => {
                if (Number(row.year || 0) !== Number(uncontractedDetailSelection.year)) return;
                const merged = getAnalysisItem(row.support_dept_code) || row;
                if (getProjectContractState(merged) === "contracted") return;
                if (Number(row.expense_amount || 0) <= 0) return;
                const projectCreatedYear = getProjectCodeFullYear(row.support_dept_code || "");
                if (!projectCreatedYear || projectCreatedYear < startYear || projectCreatedYear > endYear) {
                    const excludedCurrent = excludedMap.get(row.support_dept_code) || {
                        support_dept_code: row.support_dept_code,
                        support_dept_name: row.support_dept_name,
                        expense_amount: 0,
                        created_year: projectCreatedYear || null,
                        reason: "현재 연도 필터 범위 밖에서 생성된 프로젝트",
                    };
                    excludedCurrent.expense_amount += Number(row.expense_amount || 0);
                    excludedMap.set(row.support_dept_code, excludedCurrent);
                    return;
                }
                const current = map.get(row.support_dept_code) || {
                    support_dept_code: row.support_dept_code,
                    support_dept_name: row.support_dept_name,
                    expense_amount: 0,
                    revenue_amount: 0,
                };
                current.expense_amount += Number(row.expense_amount || 0);
                current.revenue_amount += Number(row.revenue_amount || 0);
                map.set(row.support_dept_code, current);
            });
            detailRows = [...map.values()].sort((a, b) => Number(b.expense_amount || 0) - Number(a.expense_amount || 0));
            excludedDetailRows = [...excludedMap.values()].sort((a, b) => Number(b.expense_amount || 0) - Number(a.expense_amount || 0));
        } else if (uncontractedDetailSelection.type === "category" && uncontractedDetailSelection.category) {
            const map = new Map();
            filteredAggregate.forEach((item) => {
                if (classifyUncontractedSpecialType(item) !== uncontractedDetailSelection.category) return;
                map.set(item.support_dept_code, {
                    support_dept_code: item.support_dept_code,
                    support_dept_name: item.support_dept_name,
                    expense_amount: Number(item.expense_amount || 0),
                    revenue_amount: Number(item.revenue_amount || 0),
                    category: classifyUncontractedSpecialType(item),
                });
            });
            detailRows = [...map.values()].sort((a, b) => Number(b.expense_amount || 0) - Number(a.expense_amount || 0));
        }

        const selectedYearlyRow = uncontractedDetailSelection.type === "year"
            ? yearlyRows.find((row) => Number(row.year) === Number(uncontractedDetailSelection.year))
            : null;
        const detailExpenseTotal = detailRows.reduce((sum, row) => sum + Number(row.expense_amount || 0), 0);
        const detailExpenseGap = Math.max(0, Number(selectedYearlyRow?.expense_amount || 0) - detailExpenseTotal);

        return {
            startYear,
            endYear,
            summary,
            yearlyRows,
            monthlyRows,
            focusYear,
            detailRows,
            excludedDetailRows,
            detailExpenseGap,
            detailTitle,
        };
    }

    function renderUncontractedDashboard() {
        const data = buildUncontractedDashboardData();
        const allYears = getUncontractedYearRange().allYears;
        const startOptions = allYears.map((year) => `<option value="${year}"${Number(year) === Number(data.startYear) ? " selected" : ""}>${year}</option>`).join("");
        const endOptions = allYears.map((year) => `<option value="${year}"${Number(year) === Number(data.endYear) ? " selected" : ""}>${year}</option>`).join("");
        const detailExpenseTotal = (data.detailRows || []).reduce((sum, row) => sum + Number(row.expense_amount || 0), 0);
        const detailRevenueTotal = (data.detailRows || []).reduce((sum, row) => sum + Number(row.revenue_amount || 0), 0);
        latestExcludedDetailRows = data.excludedDetailRows || [];
        return `
            <div class="uncontracted-dashboard">
                <div class="toolbar-head">
                    <h3>미계약 비용 발생 현황</h3>
                    <div class="uncontracted-toolbar">
                        <div class="field">
                            <select id="uncontractedYearStart" aria-label="미계약 시작 연도">${startOptions}</select>
                        </div>
                        <div class="field">
                            <select id="uncontractedYearEnd" aria-label="미계약 종료 연도">${endOptions}</select>
                        </div>
                        <button type="button" class="button-icon button-secondary" id="saveUncontractedFilterState" title="미계약 현황 필터 저장" aria-label="미계약 현황 필터 저장">
                            <svg viewBox="0 0 24 24" aria-hidden="true">
                                <path d="M6 4h9l3 3v13H6z"></path>
                                <path d="M9 4v6h6V4"></path>
                                <path d="M9 17h6"></path>
                            </svg>
                            <span class="sr-only">미계약 현황 필터 저장</span>
                        </button>
                    </div>
                </div>
                <div class="uncontracted-summary-grid">
                    <div class="uncontracted-stat"><span>미계약 건수</span><strong>${formatNumber(data.summary.uncontracted_projects || 0)}</strong></div>
                    <div class="uncontracted-stat"><span>비용 발생 건수</span><strong>${formatNumber(data.summary.cost_incurred_projects || 0)}</strong></div>
                    <div class="uncontracted-stat"><span>발생 비용</span><strong>${formatDisplayAmount(data.summary.expense_amount || 0)}</strong></div>
                    <div class="uncontracted-stat"><span>관련 수금</span><strong>${formatDisplayAmount(data.summary.revenue_amount || 0)}</strong></div>
                    <div class="uncontracted-stat"><span>검토 필요</span><strong>${formatNumber(data.summary.review_needed_projects || 0)}</strong></div>
                    <div class="uncontracted-stat uncontracted-stat-copy is-clickable ${uncontractedDetailSelection.type === "category" && uncontractedDetailSelection.category === "precontract" ? "is-active" : ""}" data-uncontracted-category="precontract" role="button" tabindex="0" aria-label="사전 사업 코드 세부 내역 보기">
                        <span>사전 사업 코드</span>
                        <strong>${formatNumber(data.summary.precontract_projects || 0)}</strong>
                        <em>${formatDisplayAmount(data.summary.precontract_expense_amount || 0)}</em>
                    </div>
                    <div class="uncontracted-stat uncontracted-stat-copy is-clickable ${uncontractedDetailSelection.type === "category" && uncontractedDetailSelection.category === "corporate_rnd" ? "is-active" : ""}" data-uncontracted-category="corporate_rnd" role="button" tabindex="0" aria-label="기업 연구개발 세부 내역 보기">
                        <span>기업 연구개발</span>
                        <strong>${formatNumber(data.summary.corporate_rnd_projects || 0)}</strong>
                        <em>${formatDisplayAmount(data.summary.corporate_rnd_expense_amount || 0)}</em>
                    </div>
                    <div class="uncontracted-stat uncontracted-stat-copy is-clickable ${uncontractedDetailSelection.type === "category" && uncontractedDetailSelection.category === "external_research" ? "is-active" : ""}" data-uncontracted-category="external_research" role="button" tabindex="0" aria-label="외부 연구과제 세부 내역 보기">
                        <span>외부 연구과제</span>
                        <strong>${formatNumber(data.summary.external_research_projects || 0)}</strong>
                        <em>${formatDisplayAmount(data.summary.external_research_expense_amount || 0)}</em>
                    </div>
                </div>
                <div class="uncontracted-dashboard-grid">
                    <div class="detail-block">
                        <h4>연도별 현황</h4>
                        <table class="compact-table">
                            <thead><tr><th>연도</th><th>미계약</th><th>비용 발생</th><th>발생 비용</th></tr></thead>
                            <tbody>
                                ${(data.yearlyRows || []).length ? data.yearlyRows.map((row) => `
                                    <tr class="interactive-row ${uncontractedDetailSelection.type === "year" && Number(uncontractedDetailSelection.year) === Number(row.year) ? "is-active" : ""}" data-uncontracted-year="${row.year}">
                                        <td>${detailValue(row.year, "-")}</td>
                                        <td>${formatNumber(row.uncontracted_projects || 0)}</td>
                                        <td>${formatNumber(row.cost_incurred_projects || 0)}</td>
                                        <td>${formatNumber(row.expense_amount || 0)}</td>
                                    </tr>
                                `).join("") : `<tr><td colspan="4">표시할 연도별 데이터가 없습니다.</td></tr>`}
                            </tbody>
                        </table>
                    </div>
                    <div class="detail-block">
                        <h4>${data.focusYear ? `${data.focusYear}년 월별 비용` : "월별 비용"}</h4>
                        <table class="compact-table">
                            <thead><tr><th>월</th><th>비용 발생</th><th>발생 비용</th></tr></thead>
                            <tbody>
                                ${(data.monthlyRows || []).length ? data.monthlyRows.map((row) => `
                                    <tr class="interactive-row ${uncontractedDetailSelection.type === "month" && Number(uncontractedDetailSelection.year) === Number(data.focusYear) && Number(uncontractedDetailSelection.month) === Number(row.month) ? "is-active" : ""}" data-uncontracted-month="${row.month}" data-uncontracted-month-year="${data.focusYear}">
                                        <td>${formatMonthLabel(row.month)}</td>
                                        <td>${formatNumber(row.cost_incurred_projects || 0)}</td>
                                        <td>${formatNumber(row.expense_amount || 0)}</td>
                                    </tr>
                                `).join("") : `<tr><td colspan="3">표시할 월별 데이터가 없습니다.</td></tr>`}
                            </tbody>
                        </table>
                    </div>
                </div>
                <div class="toolbar-head">
                    <h4>세부 내역</h4>
                    <button type="button" class="button-icon button-secondary" id="toggleUncontractedDetail" title="세부 내역 열기/숨기기" aria-label="세부 내역 열기/숨기기">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="${uncontractedDetailVisible ? "M7 7l10 10 M17 7L7 17" : "M6 9l6 6 6-6"}"></path>
                        </svg>
                        <span class="sr-only">세부 내역 열기/숨기기</span>
                    </button>
                </div>
                ${uncontractedDetailVisible && uncontractedDetailSelection.type ? `
                    <div class="detail-block">
                        <div class="uncontracted-detail-summary">
                            <div class="summary-chip">
                                <span>세부 내역 합계</span>
                                <strong>${formatNumber(detailExpenseTotal)}</strong>
                            </div>
                            ${uncontractedDetailSelection.type === "year" ? `
                                <div class="summary-chip is-clickable" id="openExcludedDetailItems" role="button" tabindex="0" title="제외된 항목 보기" aria-label="제외된 항목 보기">
                                    <span>연도별 발생비용 차액</span>
                                    <strong>${formatNumber(data.detailExpenseGap || 0)}</strong>
                                </div>
                            ` : `<div class="summary-chip"><span>연도별 발생비용 차액</span><strong>0</strong></div>`}
                            <div class="summary-chip">
                                <span>관련수금 합계</span>
                                <strong>${formatNumber(detailRevenueTotal)}</strong>
                            </div>
                        </div>
                        <table class="compact-table">
                            <thead>
                                <tr>
                                    <th>프로젝트</th>
                                    <th>발생 비용</th>
                                    ${uncontractedDetailSelection.type === "month" ? "" : "<th>관련 수금</th>"}
                                    <th class="classification-cell">섹터 수정</th>
                                </tr>
                            </thead>
                            <tbody>
                                ${(data.detailRows || []).length ? data.detailRows.map((row) => `
                                    <tr class="interactive-row" data-open-project-code="${escapeHtml(row.support_dept_code)}">
                                        <td>${escapeHtml(row.support_dept_code)} · ${escapeHtml(row.support_dept_name)}</td>
                                        <td>${formatNumber(row.expense_amount || 0)}</td>
                                        ${uncontractedDetailSelection.type === "month" ? "" : `<td>${formatNumber(row.revenue_amount || 0)}</td>`}
                                        <td class="classification-cell">
                                            <select class="detail-classification-select" data-uncontracted-classification="${escapeHtml(row.support_dept_code)}">
                                                ${(uncontractedCategoryOptions || []).map((option) => `
                                                    <option value="${escapeHtml(option.value)}" ${classifyUncontractedSpecialType(row) === option.value ? "selected" : ""}>${escapeHtml(option.label)}</option>
                                                `).join("")}
                                            </select>
                                        </td>
                                    </tr>
                                `).join("") : `<tr><td colspan="${uncontractedDetailSelection.type === "month" ? 3 : 4}">표시할 세부 내역이 없습니다.</td></tr>`}
                            </tbody>
                        </table>
                    </div>
                ` : ""}
            </div>
        `;
    }

    function renderRelatedProjectBar(baseItem, relatedItems) {
        if (!baseItem) return "";
        const tags = (relatedItems || []).map((item) => `
            <span class="related-project-tag">
                ${escapeHtml(item.support_dept_code)} · ${escapeHtml(item.support_dept_name)}
                <button type="button" data-remove-related="${escapeHtml(item.support_dept_code)}" title="연관 프로젝트 제거" aria-label="연관 프로젝트 제거">×</button>
            </span>
        `).join("");
        if (!tags) return "";
        return `
            <div class="related-tag-row">
                <span class="related-tag-label">연관 프로젝트</span>
                ${tags}
            </div>
        `;
    }

    function renderAnalysisMetrics(item) {
        const details = buildComparisonDetails(item);
        const actualExecutionCost =
            Number(item.total_expense || 0)
            || (
                sumEntryAmounts(details.labor.actual)
                + sumEntryAmounts(details.labor.actual_joint)
                + sumEntryAmounts(details.outsource.actual)
                + sumEntryAmounts(details.overhead.actual)
                + sumEntryAmounts(details.as.actual)
                + sumEntryAmounts(details.sga.actual)
            );
        return `
            <h4>핵심 비교 지표</h4>
            <div class="analysis-kpis">
                <div class="analysis-kpi">
                    <span>계약금액</span>
                    <strong>${formatDisplayAmount(item.contract_amount)}</strong>
                </div>
                <div class="analysis-kpi">
                    <span>수금액</span>
                    <strong>${formatDisplayAmount(item.collection_amount)}</strong>
                </div>
                <div class="analysis-kpi">
                    <span>집행비용</span>
                    <strong>${formatDisplayAmount(actualExecutionCost)}</strong>
                </div>
                <div class="analysis-kpi">
                    <span>영업수지</span>
                    <strong>${formatDisplayAmount(item.operating_balance)}</strong>
                </div>
                <div class="analysis-kpi">
                    <span>과업수행계획비용</span>
                    <strong>${formatDisplayAmount(item.planned_task_total)}</strong>
                </div>
                <div class="analysis-kpi">
                    <span>실행예산</span>
                    <strong>${formatDisplayAmount(item.exec_budget_total)}</strong>
                </div>
            </div>
        `;
    }

    function renderAnalysisComparison(item) {
        const details = buildComparisonDetails(item);
        const comparisonNotes = projectComparisonNotes?.[item.support_dept_code] || {};
        const rows = [
            ["수금", item.collection_amount || 0, item.total_revenue || 0, "collection"],
            ["인건비", sumEntryAmounts(details.labor.planned), sumEntryAmounts(details.labor.actual) + sumEntryAmounts(details.labor.actual_joint), "labor"],
            ["외주비", sumEntryAmounts(details.outsource.planned), sumEntryAmounts(details.outsource.actual), "outsource"],
            ["제경비", sumEntryAmounts(details.overhead.planned), sumEntryAmounts(details.overhead.actual), "overhead"],
            ["A/S비", item.expected_as_cost || 0, sumEntryAmounts(details.as.actual), "as"],
            ["판관비", item.expected_sga_budget || 0, sumEntryAmounts(details.sga.actual), "sga"],
        ];
        return `
            <h4>계획 대비 실제 비교</h4>
            <table class="analysis-table">
                <thead>
                    <tr>
                        <th>항목</th>
                        <th>계획/입력값</th>
                        <th>실제 집행값</th>
                        <th>차이</th>
                        <th>비고</th>
                    </tr>
                </thead>
                <tbody>
                    ${rows.map(([label, planned, actual, key]) => {
                        const detail = key ? details[key] : null;
                        const hasDetail = Boolean(detail);
                        const plannedEntries = detail?.planned || [];
                        const actualEntries = detail?.actual || [];
                        return `
                            <tr class="comparison-main-row${hasDetail ? "" : " no-detail"}" ${hasDetail ? `data-detail-target="${key}"` : ""}>
                                <td>
                                    ${hasDetail ? `
                                        <span class="comparison-toggle">
                                            <svg class="comparison-chevron" viewBox="0 0 24 24" aria-hidden="true">
                                                <path d="M9 6l6 6-6 6"></path>
                                            </svg>
                                            ${label}
                                        </span>
                                    ` : label}
                                </td>
                                <td>${formatComparisonCell(planned)}</td>
                                <td>${formatComparisonCell(actual)}</td>
                                <td>${formatComparisonDiff(planned, actual)}</td>
                                <td>
                                    <textarea
                                        class="comparison-note-input"
                                        data-comparison-note-code="${escapeHtml(item.support_dept_code)}"
                                        data-comparison-note-key="${escapeHtml(key)}"
                                    >${escapeHtml(comparisonNotes[key] || "")}</textarea>
                                </td>
                            </tr>
                            ${hasDetail ? `
                            <tr class="comparison-detail-row" data-detail-row="${key}">
                                <td colspan="5" class="comparison-detail-cell">
                                    <div class="comparison-detail-box">
                                        ${key === "labor"
                                            ? `
                                                ${renderLaborComparisonDetails(
                                                    plannedEntries,
                                                    detail?.actual || [],
                                                    detail?.actual_joint || [],
                                                )}
                                            `
                                            : renderPairedComparisonDetails(
                                                plannedEntries,
                                                actualEntries,
                                                "",
                                                "",
                                            )}
                                    </div>
                                </td>
                            </tr>` : ""}
                        `;
                    }).join("")}
                </tbody>
            </table>
        `;
    }

    function renderAnalysisNotes(item) {
        return `
            <h4>프로젝트 세부 입력 정보</h4>
            <div class="analysis-note-grid">
                <div class="detail-block">
                    <div class="detail-grid">
                        <div class="detail-item"><span>차수변경</span><strong>${detailValue(item.change_round)}</strong></div>
                        <div class="detail-item"><span>예상 A/S비</span><strong>${formatNumber(item.expected_as_cost || 0)}</strong></div>
                        <div class="detail-item"><span>예상 판관비</span><strong>${formatNumber(item.expected_sga_budget || 0)}</strong></div>
                        <div class="detail-item"><span>실집행 인건비</span><strong>${formatNumber((sumEntryAmounts(getActualGroupEntries(item, "labor", "실투입 인건비")) + sumEntryAmounts(getActualGroupEntries(item, "labor_joint", "실투입 인건비(합사)"))) || 0)}</strong></div>
                        <div class="detail-item"><span>실집행 외주비</span><strong>${formatNumber(item.actual_outsource || 0)}</strong></div>
                        <div class="detail-item"><span>항목별투입액</span><strong>${formatNumber(item.item_investment || 0)}</strong></div>
                        <div class="detail-item"><span>청구합계</span><strong>${formatNumber(item.billed_amount || 0)}</strong></div>
                        <div class="detail-item"><span>수금잔액</span><strong>${formatNumber(item.collection_balance_amount || 0)}</strong></div>
                        <div class="detail-item"><span>비고</span><strong>${detailValue(item.notes)}</strong></div>
                    </div>
                </div>
            </div>
        `;
    }

    function createSearchController(options) {
        const input = document.getElementById(options.inputId);
        const resultBox = document.getElementById(options.resultsId);
        const cardsBox = document.getElementById(options.cardsId);
        const emptyBox = document.getElementById(options.emptyId);
        const selectedCodes = new Set();

        function refreshEmptyState() {
            if (!emptyBox) return;
            emptyBox.style.display = cardsBox.children.length ? "none" : "";
        }

        function removeCard(code) {
            const card = cardsBox.querySelector(`[data-card-code="${code}"]`);
            if (card) {
                card.remove();
            }
            selectedCodes.delete(code);
            refreshEmptyState();
        }

        function attachCardEvents(card) {
            card.querySelector("[data-remove-code]")?.addEventListener("click", () => {
                removeCard(card.dataset.cardCode);
            });
        }

        function addItem(code) {
            if (selectedCodes.has(code)) return;
            const item = options.lookup(code);
            if (!item) return;
            const wrapper = document.createElement("div");
            wrapper.dataset.cardCode = code;
            wrapper.innerHTML = options.renderCard(item);
            cardsBox.appendChild(wrapper.firstElementChild);
            selectedCodes.add(code);
            attachCardEvents(cardsBox.lastElementChild);
            refreshEmptyState();
        }

        input?.addEventListener("input", () => {
            resultBox.innerHTML = buildSearchResults(input.value, options.dataset, options.renderResult);
            resultBox.querySelectorAll("[data-add-code]").forEach((button) => {
                button.addEventListener("click", () => {
                    addItem(button.dataset.addCode);
                });
            });
        });

        refreshEmptyState();
        return { addItem };
    }

    function detailValue(value, fallback = "-") {
        if (value === null || value === undefined || value === "") return fallback;
        return value;
    }

    function getProjectEditUrl(code) {
        return selectedYear ? `/projects?edit_code=${encodeURIComponent(code)}&year=${selectedYear}` : `/projects?edit_code=${encodeURIComponent(code)}`;
    }

    function formatYearRange(item) {
        const startYear = Number(item.min_year || item.year || 0);
        const endYear = Number(item.max_year || item.year || 0);
        if (!startYear && !endYear) return "-";
        if (startYear && endYear && startYear !== endYear) {
            return `${startYear}년 - ${endYear}년`;
        }
        return `${endYear || startYear}년`;
    }

    function aggregateProjectCostRows(rows) {
        const map = new Map();
        rows.forEach((item) => {
            const key = item.support_dept_code;
            const current = map.get(key) || {
                support_dept_code: item.support_dept_code,
                support_dept_name: item.support_dept_name,
                revenue_amount: 0,
                expense_amount: 0,
                min_year: item.year,
                max_year: item.year,
                matched_years: [],
            };
            current.revenue_amount += Number(item.revenue_amount || 0);
            current.expense_amount += Number(item.expense_amount || 0);
            current.min_year = Math.min(current.min_year ?? item.year, item.year);
            current.max_year = Math.max(current.max_year ?? item.year, item.year);
            current.matched_years.push(item.year);
            map.set(key, current);
        });
        return [...map.values()].sort((a, b) => {
            if ((b.max_year || 0) !== (a.max_year || 0)) return (b.max_year || 0) - (a.max_year || 0);
            return String(a.support_dept_code).localeCompare(String(b.support_dept_code));
        });
    }

    const aggregatedProjectCostDatasetCache = new Map();
    const aggregatedAllProjectCostRows = aggregateProjectCostRows(projectCostRows).map((item) => ({
        ...item,
        _searchText: `${item.support_dept_code || ""} ${item.support_dept_name || ""}`.toLowerCase(),
    }));
    const aggregatedAllProjectCostMap = new Map(
        aggregatedAllProjectCostRows.map((item) => [item.support_dept_code, item]),
    );

    function getAggregatedProjectCostDataset(yearFilterValue) {
        const cacheKey = String(yearFilterValue || "");
        if (aggregatedProjectCostDatasetCache.has(cacheKey)) {
            return aggregatedProjectCostDatasetCache.get(cacheKey);
        }
        const dataset = !yearFilterValue
            ? aggregatedAllProjectCostRows
            : aggregateProjectCostRows(
                projectCostRows.filter((item) => String(item.year) === String(yearFilterValue)),
            ).map((item) => ({
                ...item,
                _searchText: `${item.support_dept_code || ""} ${item.support_dept_name || ""}`.toLowerCase(),
            }));
        aggregatedProjectCostDatasetCache.set(cacheKey, dataset);
        return dataset;
    }

    function renderCostSearchItem(item) {
        const editUrl = getProjectEditUrl(item.support_dept_code);
        return `
            <div class="result-item">
                <div class="result-title">
                    <strong>${item.support_dept_name}</strong>
                    <span>${item.support_dept_code} · ${formatYearRange(item)} · 수익 ${formatDisplayAmount(item.revenue_amount)} · 지출 ${formatDisplayAmount(item.expense_amount)}</span>
                </div>
                <div class="actions">
                    <button type="button" class="button-icon button-secondary" data-add-code="${item.support_dept_code}" title="불러오기" aria-label="불러오기">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M12 5v14"></path>
                            <path d="M5 12h14"></path>
                        </svg>
                        <span class="sr-only">불러오기</span>
                    </button>
                    <a class="button-link button-secondary button-icon" href="${editUrl}" data-edit-code="${item.support_dept_code}" title="수정" aria-label="수정">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M4 20h4l10-10-4-4L4 16v4z"></path>
                            <path d="M13 7l4 4"></path>
                        </svg>
                        <span class="sr-only">수정</span>
                    </a>
                </div>
            </div>
        `;
    }

    function renderCostCard(item) {
        const margin = (item.revenue_amount || 0) - (item.expense_amount || 0);
        const editUrl = getProjectEditUrl(item.support_dept_code);
        return `
            <article class="compare-card" data-card-code="${item.support_dept_code}">
                <div class="compare-head">
                    <div>
                        <h3>${item.support_dept_name}</h3>
                        <p>${item.support_dept_code} · ${formatYearRange(item)} 프로젝트 현황</p>
                    </div>
                    <button type="button" class="remove-button" data-remove-code="${item.support_dept_code}" title="닫기" aria-label="닫기">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M6 6l12 12"></path>
                            <path d="M18 6L6 18"></path>
                        </svg>
                        <span class="sr-only">닫기</span>
                    </button>
                </div>
                <div class="info-metrics">
                    <div class="info-chip">
                        <div class="label">수익</div>
                        <div class="value">${formatDisplayAmount(item.revenue_amount)}</div>
                    </div>
                    <div class="info-chip">
                        <div class="label">지출액</div>
                        <div class="value">${formatDisplayAmount(item.expense_amount)}</div>
                    </div>
                    <div class="info-chip">
                        <div class="label">수익-지출</div>
                        <div class="value">${formatDisplayAmount(margin)}</div>
                    </div>
                </div>
                <div class="detail-block">
                    <h4>선택 범위 기준 원시값</h4>
                    <div class="detail-grid">
                        <div class="detail-item"><span>수익</span><strong>${formatNumber(item.revenue_amount)}</strong></div>
                        <div class="detail-item"><span>지출액</span><strong>${formatNumber(item.expense_amount)}</strong></div>
                    </div>
                </div>
                <div class="inline-actions">
                    <a class="button-link button-secondary button-icon" href="${editUrl}" data-edit-code="${item.support_dept_code}" title="수정" aria-label="수정">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M4 20h4l10-10-4-4L4 16v4z"></path>
                            <path d="M13 7l4 4"></path>
                        </svg>
                        <span class="sr-only">수정</span>
                    </a>
                </div>
            </article>
        `;
    }

    function renderDetailSearchItem(item) {
        return `
            <div class="result-item">
                <div class="result-title">
                    <strong>${item.support_dept_name}</strong>
                    <span>${item.support_dept_code} · 진행율 ${Number(item.progress_rate || 0).toFixed(1)}% · 수금 ${formatDisplayAmount(item.collection_amount)}</span>
                </div>
                <button type="button" class="button-icon button-secondary" data-add-code="${item.support_dept_code}" title="불러오기" aria-label="불러오기">
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                        <path d="M12 5v14"></path>
                        <path d="M5 12h14"></path>
                    </svg>
                    <span class="sr-only">불러오기</span>
                </button>
            </div>
        `;
    }

    function renderDetailCard(item) {
        const periodLabel = item.project_duration_days !== null && item.project_duration_days !== undefined
            ? `${item.project_duration_days}일`
            : "-";
        const editUrl = selectedYear ? `/projects?edit_code=${encodeURIComponent(item.support_dept_code)}&year=${selectedYear}` : `/projects?edit_code=${encodeURIComponent(item.support_dept_code)}`;
        return `
            <article class="compare-card" data-card-code="${item.support_dept_code}">
                <div class="compare-head">
                    <div>
                        <h3>${item.support_dept_name}</h3>
                        <p>${item.support_dept_code} · 최신 반영 ${detailValue(item.latest_year, "-")}년 ${detailValue(item.latest_month, "-")}월</p>
                    </div>
                    <button type="button" class="remove-button" data-remove-code="${item.support_dept_code}" title="닫기" aria-label="닫기">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M6 6l12 12"></path>
                            <path d="M18 6L6 18"></path>
                        </svg>
                        <span class="sr-only">닫기</span>
                    </button>
                </div>
                <div class="info-metrics">
                    <div class="info-chip">
                        <div class="label">진행율</div>
                        <div class="value">${Number(item.progress_rate || 0).toFixed(1)}%</div>
                    </div>
                    <div class="info-chip">
                        <div class="label">수금액</div>
                        <div class="value">${formatDisplayAmount(item.collection_amount)}</div>
                    </div>
                    <div class="info-chip">
                        <div class="label">종료여부</div>
                        <div class="value">${detailValue(item.completion_status)}</div>
                    </div>
                </div>
                <div class="detail-stack">
                    <div class="detail-block">
                        <h4>기본 정보</h4>
                        <div class="detail-grid">
                            <div class="detail-item"><span>시작일</span><strong>${detailValue(item.project_start_date)}</strong></div>
                            <div class="detail-item"><span>종료일</span><strong>${detailValue(item.project_end_date)}</strong></div>
                            <div class="detail-item"><span>진행 기간</span><strong>${periodLabel}</strong></div>
                            <div class="detail-item"><span>차수변경</span><strong>${detailValue(item.change_round)}</strong></div>
                        </div>
                    </div>
                    <div class="detail-block">
                        <h4>과업수행계획</h4>
                        <div class="detail-grid">
                            <div class="detail-item"><span>부서별 배분액</span><strong>${formatNumber(item.task_plan_department_budget)}</strong></div>
                            <div class="detail-item"><span>외주비</span><strong>${formatNumber(item.task_plan_outsource_budget)}</strong></div>
                            <div class="detail-item"><span>합사운영비</span><strong>${formatNumber(item.task_plan_joint_operating_cost)}</strong></div>
                            <div class="detail-item"><span>외주별 배분액</span><strong>${detailValue(item.task_plan_outsource_detail)}</strong></div>
                        </div>
                    </div>
                    <div class="detail-block">
                        <h4>실행예산</h4>
                        <div class="detail-grid">
                            <div class="detail-item"><span>직급별 인건비</span><strong>${formatNumber(item.exec_budget_labor_by_grade)}</strong></div>
                            <div class="detail-item"><span>외주비</span><strong>${formatNumber(item.exec_budget_outsource)}</strong></div>
                            <div class="detail-item"><span>원가계정별 비용계획</span><strong>${formatNumber(item.exec_budget_cost_plan)}</strong></div>
                            <div class="detail-item"><span>예상 A/S비</span><strong>${formatNumber(item.expected_as_cost)}</strong></div>
                            <div class="detail-item"><span>예상 판관비</span><strong>${formatNumber(item.expected_sga_budget)}</strong></div>
                            <div class="detail-item"><span>항목별투입액</span><strong>${formatNumber(item.item_investment)}</strong></div>
                        </div>
                    </div>
                    <div class="detail-block">
                        <h4>실제 집행</h4>
                        <div class="detail-grid">
                            <div class="detail-item"><span>실집행 수익</span><strong>${formatNumber(item.total_revenue)}</strong></div>
                            <div class="detail-item"><span>실집행 원가</span><strong>${formatNumber(item.total_cost)}</strong></div>
                            <div class="detail-item"><span>실집행 판관비</span><strong>${formatNumber(sumEntryAmounts(getActualGroupEntries(item, "sga", "실투입 판관비")))}</strong></div>
                            <div class="detail-item"><span>실집행 인건비</span><strong>${formatNumber(sumEntryAmounts(getActualGroupEntries(item, "labor", "실투입 인건비")) + sumEntryAmounts(getActualGroupEntries(item, "labor_joint", "실투입 인건비(합사)")))}</strong></div>
                            <div class="detail-item"><span>실집행 외주비</span><strong>${formatNumber(item.actual_outsource)}</strong></div>
                            <div class="detail-item"><span>비고</span><strong>${detailValue(item.notes)}</strong></div>
                        </div>
                    </div>
                </div>
                <div class="inline-actions">
                    <a class="button-link button-secondary button-icon" href="${editUrl}" title="수정" aria-label="수정">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M4 20h4l10-10-4-4L4 16v4z"></path>
                            <path d="M13 7l4 4"></path>
                        </svg>
                        <span class="sr-only">수정</span>
                    </a>
                </div>
            </article>
        `;
    }

    (() => {
        const input = document.getElementById("projectCostSearch");
        const yearSelect = document.getElementById("projectCostYearFilter");
        const contractFilterSelect = document.getElementById("projectContractFilter");
        const dropdownBox = document.getElementById("projectSearchDropdown");
        const listBox = document.getElementById("projectExplorerList");
        const emptyBox = document.getElementById("projectExplorerEmpty");
        const analysisContent = document.getElementById("projectAnalysisContent");
        const analysisToggle = document.getElementById("toggleProjectAnalysis");
        const savePageStateButton = document.getElementById("saveProjectPageState");
        const quickLinkBar = document.getElementById("projectQuickLinkBar");
        const relatedToolbarButton = document.getElementById("openRelatedProjectToolbar");
        const relatedBarBox = document.getElementById("projectRelatedBar");
        const heroBox = document.getElementById("projectAnalysisHero");
        const metricsBox = document.getElementById("projectAnalysisMetrics");
        const comparisonBox = document.getElementById("projectAnalysisComparison");
        const notesBox = document.getElementById("projectAnalysisNotes");
        const uncontractedBox = document.getElementById("projectUncontractedDashboard");
        const relatedModal = document.getElementById("relatedProjectModal");
        const relatedModalCloseButton = document.getElementById("closeRelatedProjectModal");
        const relatedSearchInput = document.getElementById("relatedProjectSearchInput");
        const relatedSearchResults = document.getElementById("relatedProjectSearchResults");
        const relatedSearchEmpty = document.getElementById("relatedProjectSearchEmpty");
        const RELATED_PROJECT_STORAGE_KEY = "project-related-selections-v1";
        const serverEditCode = String(projectEdit?.support_dept_code || "").trim();
        const initialFocusCode = String(serverFocusCode || "").trim();
        let selectedCode = initialFocusCode || serverEditCode;
        let analysisOpen = Boolean(initialFocusCode || serverEditCode);
        let projectQuickLinks = [];
        const relatedProjectSelections = loadRelatedProjectSelections();
        window.__projectRelatedSelections = relatedProjectSelections;

        function loadRelatedProjectSelections() {
            const merged = new Map();
            const mergeSource = (source) => {
                if (!source || typeof source !== "object") return;
                Object.entries(source).forEach(([code, values]) => {
                    if (typeof code !== "string" || !code.trim() || !Array.isArray(values)) return;
                    const current = new Set(merged.get(code) || []);
                    values.forEach((value) => {
                        if (typeof value === "string" && value.trim()) {
                            current.add(value.trim());
                        }
                    });
                    merged.set(code, current);
                });
            };
            mergeSource(persistedProjectRelatedLinks);
            mergeSource(persistedProjectPageState?.related_project_selections);
            try {
                const raw = window.localStorage.getItem(RELATED_PROJECT_STORAGE_KEY);
                if (raw) {
                    const parsed = JSON.parse(raw);
                    mergeSource(parsed);
                }
            } catch (error) {
                console.warn("Failed to load related project selections", error);
            }
            return merged;
        }

        function syncGlobalRelatedProjectSelections() {
            window.__projectRelatedSelections = relatedProjectSelections;
        }

        function persistRelatedProjectSelections() {
            try {
                const payload = Object.fromEntries(
                    [...relatedProjectSelections.entries()].map(([code, values]) => [code, [...values]]),
                );
                window.localStorage.setItem(RELATED_PROJECT_STORAGE_KEY, JSON.stringify(payload));
            } catch (error) {
                console.warn("Failed to persist related project selections", error);
            }
        }

        async function saveRelatedProjectSelectionsForCode(baseCode) {
            const normalizedBaseCode = String(baseCode || "").trim();
            if (!normalizedBaseCode) return;
            const relatedCodes = [...(relatedProjectSelections.get(normalizedBaseCode) || new Set())];
            const response = await fetch("/projects/related-links", {
                method: "POST",
                credentials: "same-origin",
                headers: {
                    "Content-Type": "application/json",
                    "Accept": "application/json",
                },
                body: JSON.stringify({
                    base_code: normalizedBaseCode,
                    related_codes: relatedCodes,
                }),
            });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            const result = await response.json();
            if (result?.error) {
                throw new Error(result.error);
            }
        }

        async function saveProjectPageStateToServer(showFeedback = false) {
            try {
                const uncontractedYearStart = document.getElementById("uncontractedYearStart")?.value || persistedUncontractedFilterState.start_year || "";
                const uncontractedYearEnd = document.getElementById("uncontractedYearEnd")?.value || persistedUncontractedFilterState.end_year || "";
                const payload = {
                    session_id: projectPageSessionId,
                    selected_code: selectedCode,
                    selected_year: yearSelect?.value || "",
                    analysis_open: analysisOpen,
                    uncontracted_year_start: uncontractedYearStart,
                    uncontracted_year_end: uncontractedYearEnd,
                    related_project_selections: Object.fromEntries(
                        [...relatedProjectSelections.entries()].map(([code, values]) => [code, [...values]]),
                    ),
                };
                const response = await fetch("/projects/page-state", {
                    method: "POST",
                    credentials: "same-origin",
                    headers: {
                        "Content-Type": "application/json",
                        "Accept": "application/json",
                    },
                    body: JSON.stringify(payload),
                });
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }
                const result = await response.json();
                if (result?.error) {
                    throw new Error(result.error);
                }
                if (showFeedback) {
                    window.alert("페이지 입력사항을 DB에 저장했습니다.");
                }
            } catch (error) {
                console.error(error);
                window.alert("페이지 입력사항을 DB에 저장하지 못했습니다.");
            }
        }

        async function loadProjectPageStateFromServer() {
            if (!projectPageSessionId) return;
            try {
                const response = await fetch(`/projects/page-state?session_id=${encodeURIComponent(projectPageSessionId)}`, {
                    method: "GET",
                    credentials: "same-origin",
                    headers: {
                        "Accept": "application/json",
                    },
                });
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }
                const state = await response.json();
                if (!state || typeof state !== "object" || state.error) {
                    return;
                }
                if (typeof state.selected_code === "string" && state.selected_code.trim()) {
                    selectedCode = state.selected_code.trim();
                    currentSelectedProjectCode = selectedCode;
                }
                analysisOpen = Boolean(state.analysis_open);
                if (typeof state.uncontracted_year_start === "string") {
                    persistedUncontractedFilterState.start_year = state.uncontracted_year_start;
                }
                if (typeof state.uncontracted_year_end === "string") {
                    persistedUncontractedFilterState.end_year = state.uncontracted_year_end;
                }
            } catch (error) {
                console.warn("Failed to load project page state", error);
            }
        }

        async function saveUncontractedFilterState() {
            const startYear = document.getElementById("uncontractedYearStart")?.value || "";
            const endYear = document.getElementById("uncontractedYearEnd")?.value || "";
            persistedUncontractedFilterState.start_year = startYear;
            persistedUncontractedFilterState.end_year = endYear;
            await saveProjectPageStateToServer(true);
        }

        function setAnalysisVisibility(nextOpen) {
            analysisOpen = Boolean(nextOpen);
            analysisContent?.classList.toggle("is-hidden", !analysisOpen);
            analysisToggle?.classList.toggle("is-open", analysisOpen);
            analysisToggle?.setAttribute("aria-expanded", analysisOpen ? "true" : "false");
            analysisToggle?.setAttribute("title", analysisOpen ? "프로젝트 상세 숨기기" : "프로젝트 상세 열기");
            analysisToggle?.setAttribute("aria-label", analysisOpen ? "프로젝트 상세 숨기기" : "프로젝트 상세 열기");
        }

        function renderSuggestionList(dataset, keyword) {
            const showDropdown = Boolean(keyword.trim());
            dropdownBox?.classList.toggle("open", showDropdown);
            if (!showDropdown) {
                listBox.innerHTML = "";
                emptyBox.textContent = "";
                return;
            }
            if (!dataset.length) {
                listBox.innerHTML = "";
                emptyBox.textContent = "검색어나 연도 조건에 맞는 프로젝트가 없습니다.";
                return;
            }

            const visibleItems = dataset.slice(0, 20);

            listBox.innerHTML = visibleItems.map((item) => `
                <article class="explorer-item ${selectedCode === item.support_dept_code ? "active" : ""}" data-code="${item.support_dept_code}">
                    <div class="explorer-head">
                        <div class="explorer-title">
                            <strong>${item.support_dept_name}</strong>
                            <span>${item.support_dept_code} · ${formatYearRange(item)}</span>
                            ${getProjectContractStateBadge(getAnalysisItem(item.support_dept_code) || item)}
                        </div>
                    </div>
                    <div class="explorer-tags">
                        <span class="mini-tag">수익 ${formatDisplayAmount(item.revenue_amount)}</span>
                        <span class="mini-tag">지출 ${formatDisplayAmount(item.expense_amount)}</span>
                        <span class="mini-tag">범위 ${formatYearRange(item)}</span>
                    </div>
                </article>
            `).join("");
            emptyBox.textContent = dataset.length > visibleItems.length
                ? `검색 결과 ${dataset.length}건 중 상위 ${visibleItems.length}건을 표시합니다.`
                : "";
        }

        function syncSelection(dataset, options = {}) {
            const { renderDetail = false } = options;
            if (!dataset.length) {
                relatedBarBox.innerHTML = "";
                heroBox.innerHTML = "";
                metricsBox.innerHTML = "";
                comparisonBox.innerHTML = "";
                notesBox.innerHTML = "";
                return;
            }

            if (!selectedCode || !dataset.some((item) => item.support_dept_code === selectedCode)) {
                selectedCode = dataset[0].support_dept_code;
            }
            currentSelectedProjectCode = selectedCode;
            if (renderDetail && analysisOpen) {
                renderAnalysis(getAnalysisItem(selectedCode));
            }
        }

        function getRelatedCodes(baseCode) {
            return getCurrentRelatedCodes(baseCode);
        }

        function getRelatedItems(baseCode) {
            return getRelatedCodes(baseCode)
                .map((code) => getAnalysisItem(code))
                .filter(Boolean);
        }

        function openRelatedProjectPicker() {
            if (!selectedCode) return;
            relatedModal?.classList.add("open");
            relatedSearchInput.value = "";
            refreshRelatedProjectResults();
            relatedSearchInput?.focus();
        }

    function closeRelatedProjectPicker() {
        relatedModal?.classList.remove("open");
        relatedSearchInput.value = "";
        relatedSearchResults.innerHTML = "";
        relatedSearchEmpty.textContent = "";
    }

    function openDetailYearSettingModal() {
        if (detailVisibleMinYearInput) {
            detailVisibleMinYearInput.value = String(Number(projectRuntimeSettings?.detail_visible_min_year || 2023));
        }
        detailYearSettingModal?.classList.add("open");
    }

    function closeDetailYearSettingModal() {
        detailYearSettingModal?.classList.remove("open");
    }

    function openExcludedDetailItemsModal() {
        const totalGap = latestExcludedDetailRows.reduce((sum, row) => sum + Number(row.expense_amount || 0), 0);
        excludedDetailItemsSummary.textContent = latestExcludedDetailRows.length
            ? `세부 내역에서 제외된 ${latestExcludedDetailRows.length}건의 발생 비용 합계는 ${formatNumber(totalGap)}원입니다.`
            : "현재 제외된 항목이 없습니다.";
        excludedDetailItemsRows.innerHTML = latestExcludedDetailRows.length
            ? latestExcludedDetailRows.map((row) => `
                <tr>
                    <td>${escapeHtml(row.support_dept_code)} · ${escapeHtml(row.support_dept_name)}</td>
                    <td>${detailValue(row.created_year, "-")}</td>
                    <td>${formatNumber(row.expense_amount || 0)}</td>
                    <td>${escapeHtml(row.reason || "-")}</td>
                </tr>
            `).join("")
            : `<tr><td colspan="4">표시할 제외 항목이 없습니다.</td></tr>`;
        excludedDetailItemsModal?.classList.add("open");
    }

    function closeExcludedDetailItemsModal() {
        excludedDetailItemsModal?.classList.remove("open");
    }

    async function saveDetailYearSetting() {
        const numericYear = Number(detailVisibleMinYearInput?.value || 2023);
        if (!numericYear || numericYear < 2000 || numericYear > 2099) {
            window.alert("세부내역 반영 시작 연도는 2000~2099 사이여야 합니다.");
            return;
        }
        try {
            const response = await fetch("/projects/runtime-setting", {
                method: "POST",
                credentials: "same-origin",
                headers: {
                    "Content-Type": "application/json",
                    "Accept": "application/json",
                },
                body: JSON.stringify({
                    item_key: "detail_visible_min_year",
                    value: String(numericYear),
                }),
            });
            const payload = await response.json();
            if (!response.ok || payload?.error) {
                throw new Error(payload?.error || `HTTP ${response.status}`);
            }
            Object.assign(projectRuntimeSettings, payload?.settings || {}, { detail_visible_min_year: String(numericYear) });
            closeDetailYearSettingModal();
            scheduleStandaloneUncontractedDashboardRender();
        } catch (error) {
            console.error(error);
            window.alert("세부내역 반영 연도 설정을 저장하지 못했습니다.");
        }
    }

        async function addRelatedProject(code) {
            if (!selectedCode || !code || code === selectedCode) return;
            const set = new Set(relatedProjectSelections.get(selectedCode) || []);
            set.add(code);
            relatedProjectSelections.set(selectedCode, set);
            syncGlobalRelatedProjectSelections();
            persistRelatedProjectSelections();
            try {
                await saveRelatedProjectSelectionsForCode(selectedCode);
            } catch (error) {
                console.error(error);
                window.alert("연관 프로젝트를 DB에 저장하지 못했습니다.");
            }
            closeRelatedProjectPicker();
            renderAnalysis(getAnalysisItem(selectedCode));
        }

        async function removeRelatedProject(code) {
            if (!selectedCode) return;
            const set = new Set(relatedProjectSelections.get(selectedCode) || []);
            set.delete(code);
            if (set.size) {
                relatedProjectSelections.set(selectedCode, set);
            } else {
                relatedProjectSelections.delete(selectedCode);
            }
            syncGlobalRelatedProjectSelections();
            persistRelatedProjectSelections();
            try {
                await saveRelatedProjectSelectionsForCode(selectedCode);
            } catch (error) {
                console.error(error);
                window.alert("연관 프로젝트 변경을 DB에 저장하지 못했습니다.");
            }
            renderAnalysis(getAnalysisItem(selectedCode));
        }

        function refreshRelatedProjectResults() {
            if (!selectedCode) {
                relatedSearchResults.innerHTML = "";
                relatedSearchEmpty.textContent = "먼저 프로젝트를 선택하세요.";
                return;
            }
            const keyword = (relatedSearchInput?.value || "").trim().toLowerCase();
            const alreadySelected = new Set(getRelatedCodes(selectedCode));
            const baseDataset = buildProjectCostDataset("", yearSelect?.value || "");
            const candidates = baseDataset.filter((item) => {
                if (item.support_dept_code === selectedCode) return false;
                if (alreadySelected.has(item.support_dept_code)) return false;
                if (!keyword) return true;
                return `${item.support_dept_code} ${item.support_dept_name}`.toLowerCase().includes(keyword);
            }).slice(0, 20);

            if (!candidates.length) {
                relatedSearchResults.innerHTML = "";
                relatedSearchEmpty.textContent = keyword ? "검색 결과가 없습니다." : "추가 가능한 연관 프로젝트가 없습니다.";
                return;
            }

            relatedSearchResults.innerHTML = candidates.map((item) => `
                <div class="related-search-item">
                    <div class="related-search-copy">
                        <strong>${escapeHtml(item.support_dept_name)}</strong>
                        <span>${escapeHtml(item.support_dept_code)} · ${escapeHtml(formatYearRange(item))} · 수익 ${escapeHtml(formatDisplayAmount(item.revenue_amount))} · 지출 ${escapeHtml(formatDisplayAmount(item.expense_amount))}</span>
                    </div>
                    <button type="button" class="button-icon button-secondary" data-add-related="${escapeHtml(item.support_dept_code)}" title="연관 프로젝트 추가" aria-label="연관 프로젝트 추가">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M12 5v14"></path>
                            <path d="M5 12h14"></path>
                        </svg>
                        <span class="sr-only">연관 프로젝트 추가</span>
                    </button>
                </div>
            `).join("");
            relatedSearchEmpty.textContent = "";
            relatedSearchResults.querySelectorAll("[data-add-related]").forEach((button) => {
                button.addEventListener("click", () => addRelatedProject(button.dataset.addRelated || ""));
            });
        }

        function renderProjectQuickLinks() {
            if (!quickLinkBar) return;
            const items = projectQuickLinks
                .map((code) => {
                    const item = getAnalysisItem(code) || projectStatusMap[code] || null;
                    if (item) {
                        return {
                            support_dept_code: code,
                            support_dept_name: item.support_dept_name || code,
                        };
                    }
                    return {
                        support_dept_code: code,
                        support_dept_name: code,
                    };
                })
                .slice(0, 6);
            quickLinkBar.innerHTML = items.map((item) => `
                <div class="project-quick-link" data-open-quick-link="${escapeHtml(item.support_dept_code)}" role="button" tabindex="0" title="${escapeHtml(item.support_dept_name)}" aria-label="${escapeHtml(item.support_dept_name)} 바로가기">
                    <span>${escapeHtml(item.support_dept_name)}</span>
                    <button type="button" class="project-quick-link-remove" data-remove-quick-link="${escapeHtml(item.support_dept_code)}" title="바로가기 제거" aria-label="바로가기 제거">
                        <svg viewBox="0 0 24 24" aria-hidden="true">
                            <path d="M6 6l12 12"></path>
                            <path d="M18 6L6 18"></path>
                        </svg>
                    </button>
                </div>
            `).join("");
        }

        async function loadProjectQuickLinksFromServer() {
            try {
                const response = await fetch("/projects/quick-links", {
                    method: "GET",
                    credentials: "same-origin",
                    headers: {
                        "Accept": "application/json",
                    },
                });
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }
                const payload = await response.json();
                if (payload?.error) {
                    throw new Error(payload.error);
                }
                projectQuickLinks = Array.isArray(payload?.codes) ? payload.codes.filter(Boolean) : [];
            } catch (error) {
                console.warn("Failed to load project quick links", error);
            }
            renderProjectQuickLinks();
        }

        async function saveProjectQuickLinksToServer(showFeedback = false) {
            try {
                const response = await fetch("/projects/quick-links", {
                    method: "POST",
                    credentials: "same-origin",
                    headers: {
                        "Content-Type": "application/json",
                        "Accept": "application/json",
                    },
                    body: JSON.stringify({
                        session_id: projectPageSessionId,
                        codes: projectQuickLinks,
                    }),
                });
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }
                const payload = await response.json();
                if (payload?.error) {
                    throw new Error(payload.error);
                }
                projectQuickLinks = Array.isArray(payload?.codes) ? payload.codes.filter(Boolean) : projectQuickLinks;
                renderProjectQuickLinks();
                if (showFeedback) {
                    window.alert("프로젝트 바로가기를 저장했습니다.");
                }
            } catch (error) {
                console.error(error);
                window.alert("프로젝트 바로가기를 저장하지 못했습니다.");
            }
        }

        async function addProjectQuickLink(code) {
            const normalizedCode = String(code || "").trim();
            if (!normalizedCode) return;
            projectQuickLinks = [normalizedCode, ...projectQuickLinks.filter((item) => item !== normalizedCode)].slice(0, 6);
            renderProjectQuickLinks();
            await saveProjectQuickLinksToServer(true);
        }

        async function removeProjectQuickLink(code) {
            const normalizedCode = String(code || "").trim();
            if (!normalizedCode) return;
            projectQuickLinks = projectQuickLinks.filter((item) => item !== normalizedCode);
            renderProjectQuickLinks();
            await saveProjectQuickLinksToServer(false);
        }

        async function saveComparisonNoteToServer(code, itemKey, note) {
            const response = await fetch("/projects/comparison-note", {
                method: "POST",
                credentials: "same-origin",
                headers: {
                    "Content-Type": "application/json",
                    "Accept": "application/json",
                },
                body: JSON.stringify({
                    support_dept_code: code,
                    item_key: itemKey,
                    note,
                }),
            });
            const payload = await response.json().catch(() => ({}));
            if (!response.ok || payload?.error) {
                throw new Error(payload?.error || "비고를 저장하지 못했습니다.");
            }
            projectComparisonNotes[code] = projectComparisonNotes[code] || {};
            projectComparisonNotes[code][itemKey] = note;
        }

        function renderAnalysis(item) {
            if (!item) {
                relatedBarBox.innerHTML = "";
                heroBox.innerHTML = "";
                metricsBox.innerHTML = "";
                comparisonBox.innerHTML = "";
                notesBox.innerHTML = "";
                scheduleStandaloneUncontractedDashboardRender();
                return;
            }
            const relatedItems = getRelatedItems(item.support_dept_code);
            const aggregateItem = aggregateAnalysisItem(item, relatedItems);
            relatedBarBox.innerHTML = renderRelatedProjectBar(item, relatedItems);
            heroBox.innerHTML = renderAnalysisHero(aggregateItem);
            metricsBox.innerHTML = renderAnalysisMetrics(aggregateItem);
            comparisonBox.innerHTML = renderAnalysisComparison(aggregateItem);
            notesBox.innerHTML = renderAnalysisNotes(aggregateItem);
            heroBox.querySelectorAll("[data-set-quick-link]").forEach((button) => {
                button.addEventListener("click", async () => {
                    await addProjectQuickLink(button.dataset.setQuickLink || "");
                });
            });

            relatedBarBox.querySelectorAll("[data-remove-related]").forEach((button) => {
                button.addEventListener("click", () => removeRelatedProject(button.dataset.removeRelated || ""));
            });
            comparisonBox.querySelectorAll(".comparison-main-row[data-detail-target]").forEach((row) => {
                row.addEventListener("click", () => {
                    const key = row.dataset.detailTarget;
                    const detailRow = comparisonBox.querySelector(`[data-detail-row="${key}"]`);
                    const isOpen = detailRow?.classList.contains("is-open");
                    row.classList.toggle("is-open", !isOpen);
                    detailRow?.classList.toggle("is-open", !isOpen);
                });
            });
        }

        window.__projectPageHelpers = {
            upsertProjectStatusRow,
            refreshAnalysisForCode(code) {
                const normalizedCode = String(code || "").trim();
                if (!normalizedCode) return;
                selectedCode = normalizedCode;
                currentSelectedProjectCode = normalizedCode;
                const nextItem = getAnalysisItem(normalizedCode);
                if (nextItem) {
                    if (input) {
                        input.value = nextItem.support_dept_name || "";
                    }
                    setAnalysisVisibility(true);
                    renderAnalysis(nextItem);
                }
            },
        };

        function refresh(options = {}) {
            const { renderDetail = false } = options;
            const keyword = input?.value || "";
            const dataset = filterProjectCostDatasetByContractState(
                buildProjectCostDataset(keyword, yearSelect?.value || ""),
                contractFilterSelect?.value || "",
            );
            syncSelection(dataset, {
                renderDetail: renderDetail && !keyword.trim(),
            });
            renderSuggestionList(dataset, keyword);
        }

        let refreshTimer = null;
        input?.addEventListener("input", () => {
            window.clearTimeout(refreshTimer);
            refreshTimer = window.setTimeout(() => refresh({ renderDetail: false }), 90);
        });
        yearSelect?.addEventListener("change", () => refresh({ renderDetail: true }));
        contractFilterSelect?.addEventListener("change", () => refresh({ renderDetail: true }));
        input?.addEventListener("focus", () => refresh({ renderDetail: false }));
        function renderStandaloneUncontractedDashboard() {
            if (!uncontractedBox) return;
            try {
                uncontractedBox.innerHTML = renderUncontractedDashboard();
            } catch (error) {
                console.error("미계약 비용 발생 현황 렌더 에러", error);
                uncontractedBox.innerHTML = `
                    <div class="uncontracted-dashboard">
                        <div class="toolbar-head">
                            <h3>미계약 비용 발생 현황</h3>
                        </div>
                        <div class="detail-block">
                            <p class="stacked-note">미계약 비용 발생 현황을 불러오는 중 문제가 발생했습니다. 페이지를 새로고침한 뒤 다시 확인해 주세요.</p>
                        </div>
                    </div>
                `;
            }
        }

        let uncontractedDashboardRenderToken = null;
        function scheduleStandaloneUncontractedDashboardRender(options = {}) {
            const { urgent = false } = options;
            if (uncontractedDashboardRenderToken) {
                if (typeof uncontractedDashboardRenderToken === "number") {
                    window.clearTimeout(uncontractedDashboardRenderToken);
                } else if (typeof window.cancelIdleCallback === "function") {
                    window.cancelIdleCallback(uncontractedDashboardRenderToken);
                }
                uncontractedDashboardRenderToken = null;
            }
            const runner = () => {
                uncontractedDashboardRenderToken = null;
                renderStandaloneUncontractedDashboard();
            };
            if (urgent) {
                uncontractedDashboardRenderToken = window.setTimeout(runner, 0);
                return;
            }
            if (typeof window.requestIdleCallback === "function") {
                uncontractedDashboardRenderToken = window.requestIdleCallback(runner, { timeout: 600 });
                return;
            }
            uncontractedDashboardRenderToken = window.setTimeout(runner, 120);
        }
        uncontractedBox?.addEventListener("change", async (event) => {
            const startSelect = event.target.closest("#uncontractedYearStart");
            const endSelect = event.target.closest("#uncontractedYearEnd");
            const classificationSelect = event.target.closest("[data-uncontracted-classification]");
            if (!startSelect && !endSelect && !classificationSelect) return;
            if (classificationSelect) {
                const supportDeptCode = classificationSelect.dataset.uncontractedClassification || "";
                const category = classificationSelect.value || "general";
                try {
                    const response = await fetch("/projects/uncontracted-category", {
                        method: "POST",
                        credentials: "same-origin",
                        headers: {
                            "Content-Type": "application/json",
                            "Accept": "application/json",
                        },
                        body: JSON.stringify({
                            support_dept_code: supportDeptCode,
                            category,
                        }),
                    });
                    const payload = await response.json();
                    if (!response.ok || payload?.error) {
                        throw new Error(payload?.error || `HTTP ${response.status}`);
                    }
                    persistedUncontractedCategoryOverrides[supportDeptCode] = category;
                    scheduleStandaloneUncontractedDashboardRender({ urgent: true });
                } catch (error) {
                    console.error(error);
                    window.alert("미계약 섹터를 저장하지 못했습니다.");
                }
                return;
            }
            const startYear = Number(document.getElementById("uncontractedYearStart")?.value || 0);
            const endYear = Number(document.getElementById("uncontractedYearEnd")?.value || 0);
            if (startYear && endYear && startYear > endYear) {
                if (startSelect) {
                    document.getElementById("uncontractedYearEnd").value = String(startYear);
                } else {
                    document.getElementById("uncontractedYearStart").value = String(endYear);
                }
            }
            uncontractedDetailSelection = { type: "", year: null, month: null };
            uncontractedDetailVisible = false;
            scheduleStandaloneUncontractedDashboardRender({ urgent: true });
        });
        uncontractedBox?.addEventListener("click", (event) => {
            const yearRow = event.target.closest("[data-uncontracted-year]");
            const monthRow = event.target.closest("[data-uncontracted-month]");
            const categoryButton = event.target.closest("[data-uncontracted-category]");
            const toggleButton = event.target.closest("#toggleUncontractedDetail");
            const saveFilterButton = event.target.closest("#saveUncontractedFilterState");
            const excludedItemsButton = event.target.closest("#openExcludedDetailItems");
            const openProjectRow = event.target.closest("[data-open-project-code]");
            if (toggleButton) {
                uncontractedDetailVisible = !uncontractedDetailVisible;
                scheduleStandaloneUncontractedDashboardRender({ urgent: true });
                return;
            }
            if (saveFilterButton) {
                saveUncontractedFilterState();
                return;
            }
            if (excludedItemsButton) {
                openExcludedDetailItemsModal();
                return;
            }
            if (categoryButton) {
                uncontractedDetailSelection = {
                    type: "category",
                    category: categoryButton.dataset.uncontractedCategory || "general",
                    year: null,
                    month: null,
                };
                uncontractedDetailVisible = true;
                scheduleStandaloneUncontractedDashboardRender({ urgent: true });
                return;
            }
            if (yearRow) {
                uncontractedDetailSelection = {
                    type: "year",
                    year: Number(yearRow.dataset.uncontractedYear || 0),
                    month: null,
                };
                uncontractedDetailVisible = true;
                scheduleStandaloneUncontractedDashboardRender({ urgent: true });
                return;
            }
            if (monthRow) {
                uncontractedDetailSelection = {
                    type: "month",
                    year: Number(monthRow.dataset.uncontractedMonthYear || 0),
                    month: Number(monthRow.dataset.uncontractedMonth || 0),
                };
                uncontractedDetailVisible = true;
                scheduleStandaloneUncontractedDashboardRender({ urgent: true });
                return;
            }
            if (openProjectRow) {
                if (event.target.closest(".detail-classification-select")) return;
                selectedCode = openProjectRow.dataset.openProjectCode || "";
                currentSelectedProjectCode = selectedCode;
                setAnalysisVisibility(true);
                renderAnalysis(getAnalysisItem(selectedCode));
            }
        });
        uncontractedBox?.addEventListener("keydown", (event) => {
            const categoryButton = event.target.closest("[data-uncontracted-category]");
            const excludedItemsButton = event.target.closest("#openExcludedDetailItems");
            if (categoryButton) {
                if (event.key !== "Enter" && event.key !== " ") return;
                event.preventDefault();
                categoryButton.click();
                return;
            }
            if (!excludedItemsButton) return;
            if (event.key !== "Enter" && event.key !== " ") return;
            event.preventDefault();
            excludedItemsButton.click();
        });
        listBox?.addEventListener("click", (event) => {
            const itemEl = event.target.closest(".explorer-item");
            if (!itemEl) return;
            selectedCode = itemEl.dataset.code || "";
            currentSelectedProjectCode = selectedCode;
            input.value = itemEl.querySelector("strong")?.textContent || input.value;
            dropdownBox?.classList.remove("open");
            setAnalysisVisibility(true);
            renderSuggestionList([], "");
            renderAnalysis(getAnalysisItem(selectedCode));
        });
        quickLinkBar?.addEventListener("click", (event) => {
            const removeButton = event.target.closest("[data-remove-quick-link]");
            if (removeButton) {
                event.stopPropagation();
                removeProjectQuickLink(removeButton.dataset.removeQuickLink || "");
                return;
            }
            const quickLinkButton = event.target.closest("[data-open-quick-link]");
            if (!quickLinkButton) return;
            selectedCode = quickLinkButton.dataset.openQuickLink || "";
            currentSelectedProjectCode = selectedCode;
            setAnalysisVisibility(true);
            renderAnalysis(getAnalysisItem(selectedCode));
        });
        quickLinkBar?.addEventListener("keydown", (event) => {
            const quickLinkButton = event.target.closest("[data-open-quick-link]");
            if (!quickLinkButton) return;
            if (event.key !== "Enter" && event.key !== " ") return;
            if (event.target.closest("[data-remove-quick-link]")) return;
            event.preventDefault();
            quickLinkButton.click();
        });
        relatedSearchInput?.addEventListener("input", refreshRelatedProjectResults);
        relatedModalCloseButton?.addEventListener("click", closeRelatedProjectPicker);
        relatedModal?.addEventListener("click", (event) => {
            if (event.target === relatedModal) {
                closeRelatedProjectPicker();
            }
        });
        closeDetailYearSettingModalButton?.addEventListener("click", closeDetailYearSettingModal);
        saveDetailYearSettingButton?.addEventListener("click", saveDetailYearSetting);
        detailYearSettingModal?.addEventListener("click", (event) => {
            if (event.target === detailYearSettingModal) {
                closeDetailYearSettingModal();
            }
        });
        closeExcludedDetailItemsModalButton?.addEventListener("click", closeExcludedDetailItemsModal);
        excludedDetailItemsModal?.addEventListener("click", (event) => {
            if (event.target === excludedDetailItemsModal) {
                closeExcludedDetailItemsModal();
            }
        });
        analysisToggle?.addEventListener("click", () => {
            const nextOpen = !analysisOpen;
            setAnalysisVisibility(nextOpen);
            if (nextOpen && selectedCode) {
                renderAnalysis(getAnalysisItem(selectedCode));
            }
        });

        savePageStateButton?.addEventListener("click", async () => {
            await saveProjectPageStateToServer(true);
        });

        relatedToolbarButton?.addEventListener("click", () => {
            openRelatedProjectPicker();
        });
        document.addEventListener("click", (event) => {
            if (!dropdownBox?.contains(event.target) && event.target !== input) {
                dropdownBox?.classList.remove("open");
            }
        });
        uncontractedDetailSelection = { type: "", year: null, month: null };
        uncontractedDetailVisible = false;
        (async () => {
            await loadProjectPageStateFromServer();
            await loadProjectQuickLinksFromServer();
            if (serverEditCode) {
                selectedCode = serverEditCode;
                currentSelectedProjectCode = serverEditCode;
                analysisOpen = true;
            } else if (initialFocusCode) {
                selectedCode = initialFocusCode;
                currentSelectedProjectCode = initialFocusCode;
                analysisOpen = true;
            }
            if (!analysisOpen) {
                // Keep the first view lightweight unless the current browser
                // session explicitly saved an open detail state.
                setAnalysisVisibility(false);
            } else {
                setAnalysisVisibility(true);
            }
            refresh({ renderDetail: false });
            const startupFocusCode = serverEditCode || initialFocusCode;
            if (startupFocusCode) {
                const initialItem = getAnalysisItem(startupFocusCode);
                if (initialItem) {
                    if (input) {
                        input.value = initialItem.support_dept_name || "";
                    }
                    setAnalysisVisibility(true);
                    renderAnalysis(initialItem);
                }
            }
            scheduleStandaloneUncontractedDashboardRender();
        })();
        if (openModalButton) {
            openModalButton.addEventListener("click", async () => {
                openProjectModalForFreshEntry("");
            });
        }

        document.addEventListener("click", async (event) => {
            const editButton = event.target.closest("[data-edit-code]");
            if (!editButton) return;
            event.preventDefault();
            const fallbackHref = editButton.getAttribute("href") || "";
            const targetCode = editButton.dataset.editCode || "";
            try {
                await openProjectModalForCode(targetCode);
            } catch (error) {
                console.error(error);
            }
            if (!modal?.classList.contains("open") && fallbackHref) {
                window.location.href = fallbackHref;
            }
        });

        document.addEventListener("focusout", async (event) => {
            const noteInput = event.target.closest(".comparison-note-input");
            if (!noteInput) return;
            const code = noteInput.dataset.comparisonNoteCode || "";
            const itemKey = noteInput.dataset.comparisonNoteKey || "";
            const note = noteInput.value.trim();
            const previous = (projectComparisonNotes?.[code] || {})[itemKey] || "";
            if (note === previous) return;
            try {
                await saveComparisonNoteToServer(code, itemKey, note);
            } catch (error) {
                console.error(error);
                alert(error.message || "비고를 저장하지 못했습니다.");
                noteInput.value = previous;
            }
        });

        if (modal?.classList.contains("open") && projectEdit.support_dept_code) {
            modalOriginalEdit = cloneProjectEditData(projectEdit);
            applyProjectEditData(projectEdit);
        }

        if (closeModalButton) {
            closeModalButton.addEventListener("click", () => {
                closeProjectModal();
            });
        }

        modal?.addEventListener("click", (event) => {
            if (event.target === modal) {
                closeProjectModal();
            }
        });

        renderRevenueChart();
    })();
</script>
{% endblock %}
